Git LFS
Git Large File Storage (LFS)は、リポジトリ内の大きなファイルを小さなポインターファイルに置き換えます。実際のファイル内容は別のLFSサーバーに保存されます。PitmasterはLFSポインターを解析・作成し、バッチAPIを介してLFSサーバーと通信できます。.gitattributesを読み取ったり、LFSのcleanおよびsmudgeフィルターを実行したりはしません。add()はファイルをそのまま保存し、checkout()はポインターファイルを書き込みます。実際の内容の取得は呼び出し側のコードで行います。以下に例を示します。
LFSポインターファイル
ファイルがLFSで追跡されている場合、gitは実際の内容の代わりにポインターファイルを保存します。
version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
size 12345
ポインターには次の情報が含まれます。
version: LFS仕様のバージョンURLoid: 実際のファイル内容のSHA-256ハッシュ(sha256:が先頭に付きます)size: 実際のファイルサイズ(バイト単位)
ポインターの解析
use Pitmaster\Lfs\LfsPointer;
$content = file_get_contents('.git-lfs-tracked-file');
$pointer = LfsPointer::parse($content);
if ($pointer !== null) {
echo "OID: {$pointer->oid}\n";
echo "Size: {$pointer->size}\n";
echo "Version: {$pointer->version}\n";
}
parse()は、version、oid sha256:、またはsize行がない場合にnullを返します。値の検証は行わないため、16進数でないOIDや数値でないサイズも受け付けます。
ポインターの検出
if (LfsPointer::isPointer($blobContent)) {
// This blob is an LFS pointer, not actual file content
$pointer = LfsPointer::parse($blobContent);
}
isPointer()は、完全な解析を行わずに、versionURL、oid sha256:、sizeフィールドの有無を確認します。
ポインターの作成
// From known OID and size
$pointer = new LfsPointer(
oid: '4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393',
size: 12345,
);
// From file content (computes SHA-256 hash)
$pointer = LfsPointer::forContent($actualFileContent);
echo $pointer->oid; // SHA-256 hash
echo $pointer->size; // Content length
ポインターのシリアライズ
$content = $pointer->serialize();
// "version https://git-lfs.github.com/spec/v1\noid sha256:4d7a21...\nsize 12345\n"
LFSクライアント
LfsClientはバッチAPI(<url>/objects/batchへのPOST)を使ってLFSサーバーと通信します。
クライアントの作成
use Pitmaster\Lfs\LfsClient;
// From a git remote URL (appends /info/lfs)
$client = LfsClient::fromRemoteUrl('https://github.com/user/repo.git');
// LFS URL: https://github.com/user/repo.git/info/lfs
// Direct URL
$client = new LfsClient('https://lfs.example.com/repo', timeout: 60);
fromRemoteUrl()メソッドは標準的な規則に従います。gitURLに/info/lfsを追加し、末尾に.gitがなければ追加します。利用可能なLFSURLを得られるのはHTTP(S)リモートURLだけです。SCP形式のSSHリモート(git@host:repo.git)は使えません。
LfsClientはPHPのHTTPストリームラッパーを使用します(allow_url_fopenを有効にする必要があります)。認証情報は送信せず、各アクションとともに返されるheaderマップを無視するため、認証不要のLFSサーバーとストレージURLでのみ機能します。リポジトリハンドルには紐付いていないため、Pitmaster::processFreeOptions()で開いたリポジトリを操作している場合でもネットワークリクエストを送信します。
ファイルのダウンロード
// Single file download
$content = $client->download($pointer->oid, $pointer->size);
file_put_contents('large-file.bin', $content);
内部では、download()は次の処理を行います。
download操作のバッチリクエストを送信する- サーバーからダウンロードURLを受け取る
- そのURLからファイル内容を取得する
ダウンロードしたバイト列は、OIDやサイズと照合されません。
バッチダウンロード
$results = $client->batchDownload([
['oid' => 'abc123...', 'size' => 12345],
['oid' => 'def456...', 'size' => 67890],
]);
foreach ($results as $result) {
if ($result['error'] !== null) {
echo "Error: {$result['error']}\n";
continue;
}
if ($result['href'] !== null) {
// Download from $result['href']
}
}
ファイルのアップロード
$client->upload($pointer->oid, $pointer->size, $fileContent);
アップロードでは、upload操作のバッチリクエストを送信します。サーバーにオブジェクトがすでにある場合、アップロードURLは返されず、処理は何も行いません。アップロードリクエスト自体の結果は確認されないため、アップロードに失敗しても報告されず、verifyアクションも呼び出されません。
バッチアップロード
$results = $client->batchUpload([
['oid' => 'abc123...', 'size' => 12345],
]);
foreach ($results as $result) {
if ($result['href'] !== null) {
// Server needs this object, upload to $result['href']
}
// No href means server already has it
}
バッチAPIプロトコル
LFSバッチAPIはHTTP上でJSONを使用します。
リクエスト
POST <lfs-url>/objects/batch
Content-Type: application/vnd.git-lfs+json
Accept: application/vnd.git-lfs+json
{
"operation": "download",
"transfers": ["basic"],
"objects": [
{"oid": "abc123...", "size": 12345}
]
}
レスポンス
{
"objects": [
{
"oid": "abc123...",
"size": 12345,
"actions": {
"download": {
"href": "https://storage.example.com/abc123...",
"header": {
"Authorization": "Bearer token"
}
}
}
}
]
}
リポジトリ操作へのLFSの統合
LFSを使用するリポジトリを読み込むと、blobの内容が実際のファイルデータではなくLFSポインターの場合があります。実際の内容を取得するには、次のようにします。
$blob = $repo->readObject($hash);
if (LfsPointer::isPointer($blob->content)) {
$pointer = LfsPointer::parse($blob->content);
$remoteUrl = $repo->config()->get('remote.origin.url');
$client = LfsClient::fromRemoteUrl($remoteUrl);
$realContent = $client->download($pointer->oid, $pointer->size);
} else {
$realContent = $blob->content;
}
エラー処理
バッチリクエストが失敗または無効な場合や、ダウンロードリクエストが失敗した場合は、ProtocolExceptionがスローされます。オブジェクトのダウンロードURLがサーバーから返されない場合、download()もサーバーのエラーメッセージなしでスローされます。batchDownload()とbatchUpload()はオブジェクト単位のエラーをスローせず、errorフィールドに返します。
use Pitmaster\Exceptions\ProtocolException;
try {
$content = $client->download($oid, $size);
} catch (ProtocolException $e) {
// Server unreachable, invalid response, no download URL
echo "LFS error: {$e->getMessage()}\n";
}
LFSポインターのプロパティ
LfsPointerクラスは読み取り専用です。
$pointer->oid; // string: SHA-256 hash (hex)
$pointer->size; // int: file size in bytes
$pointer->version; // string: spec version URL