
# 클론, 페치, 푸시

Pitmaster은 git 네트워크의 세 가지 기본 작업인 clone(최초 다운로드), fetch(증분 다운로드), push(업로드)를 지원합니다. SSH URL(`ssh://...`, `user@host:path`)은 SSH 전송 방식을 사용합니다. 그 외 URL은 smart HTTP을 사용하며, smart 교환에 실패하면 clone과 fetch는 dumb HTTP로 대체합니다([프로토콜](https://pitmaster.inline0.dev/ko/docs/network/protocols) 참조).

세 작업 모두 네트워크 전송을 시작하므로 `['processes' => false]` 또는 `Pitmaster::processFreeOptions()`로 연 핸들에서는 `RuntimeException`을 던집니다.

## 클론

```php
use Pitmaster\Pitmaster;

// public static function clone(string $url, string $path, ?int $depth = null, array $options = []): Repository
$repo = Pitmaster::clone('https://github.com/user/repo.git', '/path/to/destination');
$repo = Pitmaster::clone('https://github.com/user/repo.git', '/path/to/shallow', 1);
```

새 저장소는 항상 SHA-1을 사용합니다. 클론에 실패하고 대상 경로가 이전에 존재하지 않았다면, 일부 생성된 디렉터리를 제거합니다.

### 클론 흐름

1. **초기화**: 대상 경로에 새 빈 저장소를 만듭니다
2. **원격 저장소 설정**: URL을 `remote.origin.url`로 저장하고 페치 refspec을 설정합니다
3. **ref 검색**: 서버에 있는 항목을 파악하기 위해 `GET /info/refs?service=git-upload-pack`를 실행합니다
4. **협상**: 모든 원격 ref에 대해 `want` 행을 보냅니다(해시 기준으로 중복 제거)
5. **pack 수신**: 요청한 객체가 모두 들어 있는 pack 파일을 다운로드합니다
6. **pack 기록**: pack 파일을 `.git/objects/pack/`에 저장합니다
7. **pack 인덱싱**: 빠른 조회를 위해 `.idx` 파일을 생성합니다
8. **ref 설정**: 원격 추적 브랜치(`refs/remotes/origin/*`)와 태그를 만듭니다
9. **HEAD 설정**: 서버의 `symref=HEAD:...` 기능에서 가져온 기본 브랜치를 가리키도록 HEAD를 설정하거나 서버가 이를 보내지 않으면 `refs/heads/main`을 사용한 뒤, `branch.<name>.remote` 및 `branch.<name>.merge`을 설정합니다
10. **체크아웃**: 해당 브랜치를 체크아웃하여 작업 트리와 인덱스를 기록합니다

최종 체크아웃 중 발생한 오류는 잡아서 무시하므로, 클론이 비어 있거나 일부만 생성된 작업 트리를 반환할 수 있습니다([#19](https://github.com/inline0/pitmaster/issues/19)).

```php
$repo = Pitmaster::clone($url, $path);
$head = $repo->head();
echo $head->message;

$branches = $repo->branches();
$files = $repo->status();
```

### 작업 트리 구성

pack을 받은 후 Pitmaster은 기본 브랜치에 대해 `$repo->checkout()`을 호출합니다. 그러면 HEAD 커밋 트리에 있는 모든 파일이 작업 디렉터리에 기록되고 트리 항목으로 인덱스가 생성되므로, `status()`은 작업 트리가 깨끗하다고 보고합니다.

## 페치

```php
// public function fetch(string $remote = 'origin', ?int $depth = null): void
$repo->fetch();              // Fetch from 'origin'
$repo->fetch('upstream');    // Fetch from a named remote
$repo->fetch('origin', 1);   // Shallow fetch
```

### 페치 흐름

1. **원격 URL 읽기**: `.git/config`에서 읽습니다(`remote.<name>.url`)
2. **ref 검색**: 모든 원격 ref의 현재 상태를 가져옵니다
3. **want/have 협상**:
   - Want: 로컬에 없는 객체를 가리키며 `remote.<name>.fetch` refspec과 일치하는 원격 ref(기본값 `+refs/heads/*:refs/remotes/<name>/*`). 깊이를 지정하면 해당 ref가 모두 포함됩니다
   - Have: 모든 로컬 ref의 끝점
4. **pack 수신**: 새 객체만 다운로드합니다
5. **pack 기록**: `.git/objects/pack/`에 저장하고 인덱스를 생성합니다
6. **추적 ref 업데이트**: 일치하는 각 원격 브랜치의 `refs/remotes/<remote>/<branch>`을 업데이트하고, 알린 모든 태그 ref를 기록합니다. 태그 객체는 별도로 요청하지 않으므로, 새 주석 태그가 다운로드되지 않은 객체를 가리키는 ref로 기록될 수 있습니다([#24](https://github.com/inline0/pitmaster/issues/24))

```php
// Fetch is incremental: only downloads what's new
$repo->fetch();

// After fetching, merge or rebase to incorporate changes (use the full ref name)
$result = $repo->merge('refs/remotes/origin/main');
```

`resolve()`은 이름을 `refs/heads/<name>`, `refs/tags/<name>`, 이름 자체 순으로 조회하므로 짧은 형식인 `origin/main`은 찾지 못합니다.

Pitmaster은 아직 인덱싱할 수 없는 thin pack을 요청합니다([#16](https://github.com/inline0/pitmaster/issues/16)). 증분 페치에 thin pack으로 응답하는 서버를 만나면 `fetch()`은 `PackParseException`을 던집니다.

### 이미 최신 상태

원격 ref가 가리키는 모든 객체가 이미 로컬에 있으면 pack을 요청하지 않고 ref 검색 후 `fetch()`이 반환됩니다. 이 경우 원격 브랜치가 이미 보유한 커밋으로 이동했더라도 추적 ref는 업데이트되지 않습니다([#23](https://github.com/inline0/pitmaster/issues/23)).

## 푸시

```php
$repo->push();                        // Push current branch to 'origin'
$repo->push('origin', 'main');        // Push a specific branch
$repo->push('upstream', 'feature');   // Push to a named remote
```

### 푸시 흐름

1. **원격 URL 읽기**: `.git/config`에서 읽습니다
2. **푸시할 항목 결정**: 로컬 브랜치를 해당 커밋 해시로 해석합니다
3. **원격 ref 검색**: 원격 브랜치의 현재 상태를 가져와 이전 해시를 계산합니다
4. **빨리 감기 확인**: 원격 브랜치가 로컬 브랜치의 조상이 아니면 아무것도 보내기 전에 `RuntimeException("Push rejected: non-fast-forward update to <branch>")`을 던집니다
5. **ref 업데이트 전송**: `<old-hash> <new-hash> refs/heads/<branch>`
6. **pack 데이터 전송**: 원격에 필요한 객체를 담은 pack을 보냅니다
7. **결과 확인**: 서버가 각 ref의 성공 또는 실패를 응답합니다

훅이 비활성화되어 있지 않으면 업데이트 전송 전에 `pre-push` 훅이 실행됩니다.

```php
// Push the current branch
$repo->push();

// Push a specific branch to a specific remote
$repo->push('origin', 'feature/login');
```

### 푸시 요구 사항

- 현재 브랜치가 로컬에 있어야 합니다
- 원격 저장소에 URL이 설정되어 있어야 합니다
- 원격 저장소가 푸시를 허용해야 합니다(인증, 권한, 빨리 감기가 아닌 업데이트 정책)

### 강제 푸시

Pitmaster은 원격의 실시간 ref 알림에서 이전 해시를 가져오고, `push()`은 로컬에서 빨리 감기가 아닌 업데이트를 거부합니다. 원격 브랜치를 덮어쓰려면 force-with-lease를 사용하세요:

```php
// Lease defaults to refs/remotes/<remote>/<branch>; pass an ObjectId to set it explicitly
$repo->pushForceWithLease('origin', 'main');
$repo->pushForceWithLease('origin', 'main', $repo->resolve('refs/remotes/origin/main'));

// Update several branches in one atomic request
$repo->pushAtomic('origin', ['main', 'release']);

// Make the remote mirror local refs (also deletes remote refs that no longer exist locally)
$repo->pushMirror('origin');
```

## UploadPackClient(페치 내부 처리)

```php
use Pitmaster\Protocol\UploadPackClient;
use Pitmaster\Protocol\SmartHttpClient;

$http = new SmartHttpClient();
$uploadPack = new UploadPackClient($http);

// Fetch with explicit wants and haves
$packData = $uploadPack->fetch($url, $wants, $haves);
```

want/have 협상은 pkt-line 요청을 구성합니다:

```
want <hash1> <capabilities>\n
want <hash2>\n
deepen <depth>\n        (only when a depth is given)
0000
have <hash3>\n
have <hash4>\n
done\n
```

서버는 승인 응답 후 pack 데이터를 반환합니다(사이드 밴드로 래핑될 수 있음).

## ReceivePackClient(푸시 내부 처리)

```php
use Pitmaster\Protocol\ReceivePackClient;
use Pitmaster\Protocol\SmartHttpClient;

$http = new SmartHttpClient();
$receivePack = new ReceivePackClient($http);

$receivePack->push($url, [
    [
        'old' => $oldId,      // Current remote hash (or zero hash for new branch)
        'new' => $localId,    // Local commit hash to push
        'ref' => 'refs/heads/main',
    ],
], $packData);
```

푸시 요청 형식:

```
<old-hash> <new-hash> refs/heads/main\0<capabilities>\n
0000
PACK<pack data>
```

## 오류 처리

```php
use Pitmaster\Exceptions\ProtocolException;

try {
    $repo->fetch();
} catch (ProtocolException $e) {
    // Network unreachable, auth failed, server error, etc.
    echo $e->getMessage();
}

try {
    $repo->push();
} catch (\RuntimeException $e) {
    // Remote not configured, not on a branch, branch not found, non-fast-forward
    echo $e->getMessage();
}
```

`fetch()`은 일반 `RuntimeException`(알 수 없는 원격, 프로세스를 시작하지 않는 핸들) 또는 `PackParseException`(인덱싱할 수 없는 pack)도 던질 수 있습니다. `ProtocolException` 및 `PackParseException`은 모두 `\RuntimeException`을 확장하므로 `\RuntimeException`을 잡으면 이 오류를 모두 처리할 수 있습니다.

## 원격 설정

원격 저장소는 `.git/config`에 저장됩니다:

```ini
[remote "origin"]
    url = https://github.com/user/repo.git
    fetch = +refs/heads/*:refs/remotes/origin/*
```

설정 API을 통해 접근합니다:

```php
$config = $repo->config();
$url = $config->get('remote.origin.url');
$config->set('remote.upstream.url', 'https://github.com/other/repo.git');
$config->set('remote.upstream.fetch', '+refs/heads/*:refs/remotes/upstream/*');

// set() changes the in-memory config only; write it to persist the change
$config->writeToFile($repo->commonGitDir() . '/config');
```

## 푸시용 pack 데이터

현재 Pitmaster은 푸시할 커밋에서 도달할 수 있지만 원격의 현재 ref에서는 도달할 수 없는 객체를 모아, 델타 압축 없이 기본 객체로 pack에 기록합니다. 기능상 정확하지만 `git push`보다 pack 크기가 커집니다. pack 기록 시 델타 압축은 향후 최적화로 계획되어 있습니다.
