
# 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仕様のバージョンURL
- `oid`: 実際のファイル内容のSHA-256ハッシュ（`sha256:`が先頭に付きます）
- `size`: 実際のファイルサイズ（バイト単位）

### ポインターの解析

```php
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や数値でないサイズも受け付けます。

### ポインターの検出

```php
if (LfsPointer::isPointer($blobContent)) {
    // This blob is an LFS pointer, not actual file content
    $pointer = LfsPointer::parse($blobContent);
}
```

`isPointer()`は、完全な解析を行わずに、versionURL、`oid sha256:`、`size`フィールドの有無を確認します。

### ポインターの作成

```php
// 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
```

### ポインターのシリアライズ

```php
$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サーバーと通信します。

### クライアントの作成

```php
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()`で開いたリポジトリを操作している場合でもネットワークリクエストを送信します。

### ファイルのダウンロード

```php
// Single file download
$content = $client->download($pointer->oid, $pointer->size);
file_put_contents('large-file.bin', $content);
```

内部では、`download()`は次の処理を行います。
1. `download`操作のバッチリクエストを送信する
2. サーバーからダウンロードURLを受け取る
3. そのURLからファイル内容を取得する

ダウンロードしたバイト列は、OIDやサイズと照合されません。

### バッチダウンロード

```php
$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']
    }
}
```

### ファイルのアップロード

```php
$client->upload($pointer->oid, $pointer->size, $fileContent);
```

アップロードでは、`upload`操作のバッチリクエストを送信します。サーバーにオブジェクトがすでにある場合、アップロードURLは返されず、処理は何も行いません。アップロードリクエスト自体の結果は確認されないため、アップロードに失敗しても報告されず、`verify`アクションも呼び出されません。

### バッチアップロード

```php
$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}
    ]
}
```

### レスポンス

```json
{
    "objects": [
        {
            "oid": "abc123...",
            "size": 12345,
            "actions": {
                "download": {
                    "href": "https://storage.example.com/abc123...",
                    "header": {
                        "Authorization": "Bearer token"
                    }
                }
            }
        }
    ]
}
```

## リポジトリ操作へのLFSの統合

LFSを使用するリポジトリを読み込むと、blobの内容が実際のファイルデータではなくLFSポインターの場合があります。実際の内容を取得するには、次のようにします。

```php
$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`フィールドに返します。

```php
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`クラスは読み取り専用です。

```php
$pointer->oid;      // string: SHA-256 hash (hex)
$pointer->size;     // int: file size in bytes
$pointer->version;  // string: spec version URL
```
