본문으로 건너뛰기

클론, 페치, 푸시

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

세 작업 모두 네트워크 전송을 시작하므로 ['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).

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)
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). 증분 페치에 thin pack으로 응답하는 서버를 만나면 fetch()은 PackParseException을 던집니다.

이미 최신 상태

원격 ref가 가리키는 모든 객체가 이미 로컬에 있으면 pack을 요청하지 않고 ref 검색 후 fetch()이 반환됩니다. 이 경우 원격 브랜치가 이미 보유한 커밋으로 이동했더라도 추적 ref는 업데이트되지 않습니다(#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 기록 시 델타 압축은 향후 최적화로 계획되어 있습니다.