
# Git LFS

Git Large File Storage (LFS) sostituisce i file di grandi dimensioni nel repository con piccoli file puntatore. Il contenuto effettivo viene archiviato su un server LFS separato. Pitmaster può analizzare e creare puntatori LFS e comunicare con i server LFS tramite l’API batch. Non legge `.gitattributes` né esegue i filtri clean e smudge di LFS: `add()` archivia i file così come sono e `checkout()` scrive i file puntatore. Il recupero del contenuto effettivo spetta al vostro codice, come mostrato sotto.

## File puntatore LFS

Quando un file è tracciato da LFS, git archivia un file puntatore invece del contenuto effettivo:

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

Il puntatore contiene:
- `version`: la versione della specifica LFS URL
- `oid`: hash SHA-256 del contenuto effettivo del file (preceduto da `sha256:`)
- `size`: la dimensione effettiva del file in byte

### Analizzare i puntatori

```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()` restituisce `null` se manca la riga `version`, `oid sha256:` o `size`. Non convalida i valori (accetta un OID non esadecimale o una dimensione non numerica).

### Rilevare i puntatori

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

`isPointer()` verifica la presenza dei campi version URL, `oid sha256:` e `size` senza eseguire un’analisi completa.

### Creare puntatori

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

### Serializzare i puntatori

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

## Client LFS

Il `LfsClient` comunica con i server LFS usando l’API batch (POST a `<url>/objects/batch`).

### Creare un 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);
```

Il metodo `fromRemoteUrl()` segue la convenzione standard: aggiunge `/info/lfs` all’git URL (aggiungendo `.git` se non è presente). Solo gli URL remoti HTTP(S) forniscono un URL LFS utilizzabile; i remoti SSH in stile SCP (`git@host:repo.git`) non lo fanno.

`LfsClient` usa il wrapper di stream HTTP di PHP (`allow_url_fopen` deve essere abilitato). Non invia credenziali e ignora la mappa `header` restituita con ogni azione, quindi funziona solo con server LFS e URL di archiviazione che non richiedono autenticazione. Non è associato a un handle del repository, quindi invia richieste di rete anche quando si lavora con un repository aperto con `Pitmaster::processFreeOptions()`.

### Scaricare file

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

In pratica, `download()`:
1. Invia una richiesta batch per l’operazione `download`
2. Riceve dal server un URL di download
3. Recupera il contenuto del file da quell’URL

I byte scaricati non vengono verificati rispetto all’OID o alla dimensione.

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

### Caricare file

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

Il caricamento invia una richiesta batch per l’operazione `upload`. Se il server possiede già l’oggetto, non restituisce alcun URL di caricamento e l’operazione non fa nulla. Il risultato della richiesta di caricamento non viene verificato, quindi un errore di caricamento non viene segnalato e l’azione `verify` non viene chiamata.

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

## Protocollo batch API

L’API batch LFS usa JSON su HTTP:

### Richiesta

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

### Risposta

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

## Integrare LFS con le operazioni sul repository

Quando si legge un repository che usa LFS, il contenuto dei blob può essere costituito da puntatori LFS anziché dai dati effettivi dei file. Per ottenere il contenuto effettivo:

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

## Gestione degli errori

Una richiesta batch non riuscita o non valida, e una richiesta di download non riuscita, generano `ProtocolException`. Anche `download()` genera un errore quando il server non restituisce alcun URL di download per l’oggetto, senza includere il messaggio di errore del server. `batchDownload()` e `batchUpload()` non generano errori per gli errori dei singoli oggetti, ma li restituiscono nel campo `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à dei puntatori LFS

La classe `LfsPointer` è di sola lettura:

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