クローン、フェッチ、プッシュ
Pitmasterは、gitの基本的な3つのネットワーク操作、clone(初回ダウンロード)、fetch(差分ダウンロード)、push(アップロード)をサポートします。SSH URL(ssh://...、user@host:path)ではSSHトランスポートを使用し、その他のURLではスマートHTTPを使用します。スマートな通信に失敗すると、cloneとfetchはダムHTTPにフォールバックします(プロトコルを参照)。
3つの操作はいずれもネットワークトランスポートを開始するため、['processes' => false]またはPitmaster::processFreeOptions()で開いたハンドルではRuntimeExceptionをスローします。
クローン
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を使用します。クローンに失敗し、実行前に宛先が存在していなかった場合は、作成途中のディレクトリを削除します。
クローンの流れ
- 初期化:宛先パスに新しい空のリポジトリを作成
- リモートを設定:URLを
remote.origin.urlとして保存し、フェッチ用のrefspecを設定 - refを検出:
GET /info/refs?service=git-upload-packを実行してサーバー上の状態を確認 - ネゴシエーション:すべてのリモートrefについて
want行を送信(ハッシュで重複を排除) - packを受信:要求したすべてのオブジェクトを含むpackファイルをダウンロード
- packを書き込み:packファイルを
.git/objects/pack/に保存 - packにインデックスを作成:高速検索用の
.idxファイルを生成 - refを設定:リモート追跡ブランチ(
refs/remotes/origin/*)とタグを作成 - HEADを設定:デフォルトブランチをHEADの参照先に設定(サーバーの
symref=HEAD:...機能から取得し、サーバーが何も送らない場合はrefs/heads/mainを使用)。branch.<name>.remoteとbranch.<name>.mergeも設定 - チェックアウト:そのブランチをチェックアウトし、作業ツリーとインデックスを書き込む
最後のチェックアウト中に発生したエラーは捕捉して無視されるため、クローン後の作業ツリーが空または不完全な場合があります(#19)。
$repo = Pitmaster::clone($url, $path);
$head = $repo->head();
echo $head->message;
$branches = $repo->branches();
$files = $repo->status();
作業ツリーの展開
packの受信後、Pitmasterはデフォルトブランチに対して$repo->checkout()を呼び出します。これによりHEADコミットのツリー内のすべてのファイルが作業ディレクトリに書き込まれ、ツリーエントリからインデックスが作成されるため、status()は作業ツリーに変更がないと報告します。
フェッチ
// 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
フェッチの流れ
- リモートURLを読み込む:
.git/configから取得(remote.<name>.url) - refを検出:すべてのリモートrefの現在の状態を取得
- want/haveをネゴシエート:
- Want:
remote.<name>.fetchのrefspec(デフォルトは+refs/heads/*:refs/remotes/<name>/*)に一致し、オブジェクトがローカルに存在しないリモートref(深さを指定した場合はすべて) - Have:すべてのローカルrefの先端
- Want:
- packを受信:新しいオブジェクトのみをダウンロード
- packを書き込み:
.git/objects/pack/に保存し、インデックスを作成 - 追跡refを更新:一致した各リモートブランチの
refs/remotes/<remote>/<branch>を更新し、通知されたすべてのタグrefを書き込む。タグオブジェクトは個別に要求しないため、新しい注釈付きタグがダウンロードされていないオブジェクトへのrefとして書き込まれる場合があります(#24)
// 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は薄いpackを要求しますが、現時点ではインデックスを作成できません(#16)。差分フェッチに対してサーバーが薄いpackを返すと、fetch()はPackParseExceptionをスローします。
最新の状態
リモートrefが指すすべてのオブジェクトがすでにローカルに存在する場合、packは要求されず、refの検出後にfetch()は戻ります。この場合、すでに取得済みのコミットへリモートブランチが移動していても、追跡refは更新されません(#23)。
プッシュ
$repo->push(); // Push current branch to 'origin'
$repo->push('origin', 'main'); // Push a specific branch
$repo->push('upstream', 'feature'); // Push to a named remote
プッシュの流れ
- リモートURLを読み込む:
.git/configから取得 - プッシュする内容を決定:ローカルブランチをコミットハッシュに解決
- リモートrefを検出:リモートブランチの現在の状態を取得し、古いハッシュを計算
- ファストフォワードを確認:リモートブランチがローカルブランチの祖先でない場合、何も送信する前に
RuntimeException("Push rejected: non-fast-forward update to <branch>")をスロー - ref更新を送信:
<old-hash> <new-hash> refs/heads/<branch> - packデータを送信:リモートに必要なオブジェクトを含むpack
- 結果を確認:サーバーが各refの成功または失敗を応答
フックが無効でない限り、更新の送信前にpre-pushフックが実行されます。
// 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を使用してください。
// 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(フェッチの内部処理)
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(プッシュの内部処理)
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>
エラー処理
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に保存されます。
[remote "origin"]
url = https://github.com/user/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
設定APIからアクセスします。
$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書き込み時のデルタ圧縮は今後の最適化として予定されています。