
# Git LFS

Git Large File Storage (LFS) înlocuiește fișierele mari din depozit cu fișiere pointer mici. Conținutul real al fișierelor este stocat pe un server LFS separat. Pitmaster poate analiza și crea pointere LFS și poate comunica cu serverele LFS prin API batch. Nu citește `.gitattributes` și nu rulează filtrele LFS clean și smudge: `add()` stochează fișierele așa cum sunt, iar `checkout()` scrie fișiere pointer. Preluarea conținutului real revine codului dumneavoastră, după cum se arată mai jos.

## Fișiere pointer LFS

Când un fișier este urmărit de LFS, git stochează un fișier pointer în locul conținutului real:

```
version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
size 12345
```

Pointerul conține:
- `version`: versiunea specificației LFS URL
- `oid`: hash-ul SHA-256 al conținutului real al fișierului (cu prefixul `sha256:`)
- `size`: dimensiunea reală a fișierului în octeți

### Analizarea pointerelor

```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()` returnează `null` dacă lipsește linia `version`, `oid sha256:` sau `size`. Nu validează valorile (un OID non-hexazecimal sau o dimensiune nenumerică este acceptată).

### Detectarea pointerelor

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

`isPointer()` verifică prezența câmpurilor versiune URL, `oid sha256:` și `size` fără analizare completă.

### Crearea pointerelor

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

### Serializarea pointerelor

```php
$content = $pointer->serialize();
// "version https://git-lfs.github.com/spec/v1\noid sha256:4d7a21...\nsize 12345\n"
```

## Client LFS

`LfsClient` comunică cu serverele LFS folosind API batch (POST către `<url>/objects/batch`).

### Crearea unui client

```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);
```

Metoda `fromRemoteUrl()` urmează convenția standard: adaugă `/info/lfs` la git URL (adăugând `.git` dacă lipsește). Numai URL-urile remote HTTP(S) oferă un URL LFS utilizabil; remote-urile în stil SCP SSH (`git@host:repo.git`) nu oferă unul.

`LfsClient` folosește wrapper-ul de flux HTTP din PHP (trebuie activat `allow_url_fopen`). Nu trimite credențiale și ignoră maparea `header` returnată pentru fiecare acțiune, deci funcționează doar cu servere LFS și URL-uri de stocare care nu necesită autentificare. Nu este legat de un handle de depozit, așa că efectuează cereri de rețea chiar și când lucrați cu un depozit deschis cu `Pitmaster::processFreeOptions()`.

### Descărcarea fișierelor

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

În fundal, `download()`:
1. Trimite o cerere batch pentru operațiunea `download`
2. Primește de la server un URL de descărcare
3. Preia conținutul fișierului de la acel URL

Octeții descărcați nu sunt verificați în raport cu OID-ul sau dimensiunea.

### Descărcare batch

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

### Încărcarea fișierelor

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

Încărcarea trimite o cerere batch pentru operațiunea `upload`. Dacă serverul are deja obiectul, nu returnează niciun URL de încărcare, iar operațiunea nu face nimic. Rezultatul cererii de încărcare în sine nu este verificat, așa că o încărcare eșuată nu este raportată, iar acțiunea `verify` nu este apelată.

### Încărcare batch

```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
}
```

## Protocolul API batch

Batch-ul LFS API folosește JSON prin HTTP:

### Cerere

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

### Răspuns

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

## Integrarea LFS cu operațiunile asupra depozitului

Când citiți un depozit care folosește LFS, conținutul blob-urilor poate consta în pointere LFS în locul datelor reale ale fișierelor. Pentru a obține conținutul real:

```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;
}
```

## Tratarea erorilor

O cerere batch eșuată sau nevalidă, precum și o cerere de descărcare eșuată, aruncă `ProtocolException`. `download()` aruncă, de asemenea, o excepție când serverul nu returnează niciun URL de descărcare pentru obiect, fără mesajul de eroare al serverului. `batchDownload()` și `batchUpload()` nu aruncă excepții pentru erori la nivel de obiect; le returnează în câmpul `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";
}
```

## Proprietățile pointerelor LFS

Clasa `LfsPointer` este numai pentru citire:

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