
# Git LFS

Git 대용량 파일 저장소(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()`은 전체 파싱 없이 버전 URL, `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을 사용해 LFS 서버와 통신합니다(`<url>/objects/batch`로 POST).

### 클라이언트 생성

```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()` 메서드는 표준 관례를 따릅니다. git URL에 `/info/lfs`을 덧붙이며, 없으면 `.git`을 추가합니다. HTTP(S) 원격 URL만 사용할 수 있는 LFS 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를 사용하는 저장소를 읽을 때 블롭 콘텐츠가 실제 파일 데이터가 아닌 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
```
