
# Git LFS

Git Large File Storage (LFS) remplace les fichiers volumineux de votre dépôt par de petits fichiers pointeurs. Le contenu réel des fichiers est stocké sur un serveur LFS distinct. Pitmaster peut analyser et créer des pointeurs LFS et communiquer avec les serveurs LFS via le API par lots. Il ne lit pas `.gitattributes` et n’exécute pas les filtres clean et smudge de LFS : `add()` stocke les fichiers tels quels et `checkout()` écrit des fichiers pointeurs. Il revient à votre code de récupérer le contenu réel, comme illustré ci-dessous.

## Fichiers pointeurs LFS

Lorsqu’un fichier est suivi par LFS, git stocke un fichier pointeur au lieu du contenu réel :

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

Le pointeur contient :
- `version` : la version de la spécification LFS URL
- `oid` : le hash SHA-256 du contenu réel du fichier (préfixé par `sha256:`)
- `size` : la taille réelle du fichier en octets

### Analyser les pointeurs

```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()` renvoie `null` si la ligne `version`, `oid sha256:` ou `size` est absente. Il ne valide pas les valeurs (un OID non hexadécimal ou une taille non numérique est accepté).

### Détecter les pointeurs

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

`isPointer()` vérifie la présence des champs version URL, `oid sha256:` et `size` sans analyse complète.

### Créer des pointeurs

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

### Sérialiser les pointeurs

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

## Client LFS

Le `LfsClient` communique avec les serveurs LFS à l’aide du API par lots (POST vers `<url>/objects/batch`).

### Créer 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);
```

La méthode `fromRemoteUrl()` suit la convention habituelle : elle ajoute `/info/lfs` à l’URL git (en ajoutant `.git` si nécessaire). Seules les URL distantes HTTP(S) fournissent un URL LFS utilisable ; les dépôts distants au format SCP SSH (`git@host:repo.git`) ne fonctionnent pas.

`LfsClient` utilise le wrapper de flux HTTP de PHP (`allow_url_fopen` doit être activé). Il n’envoie aucun identifiant et ignore la map `header` renvoyée avec chaque action ; il ne fonctionne donc qu’avec les serveurs LFS et les URL de stockage qui ne nécessitent aucune authentification. Il n’est pas lié à un handle de dépôt et effectue donc des requêtes réseau même lorsque vous travaillez avec un dépôt ouvert avec `Pitmaster::processFreeOptions()`.

### Télécharger des fichiers

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

En interne, `download()` :
1. Envoie une requête par lots pour l’opération `download`
2. Reçoit une URL de téléchargement du serveur
3. Récupère le contenu du fichier depuis cette URL

Les octets téléchargés ne sont pas vérifiés par rapport à l’OID ou à la taille.

### Téléchargement par lots

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

### Téléverser des fichiers

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

Le téléversement envoie une requête par lots pour l’opération `upload`. Si le serveur possède déjà l’objet, il ne renvoie aucune URL de téléversement et l’opération ne fait rien. Le résultat de la requête de téléversement n’est pas vérifié ; un échec n’est donc pas signalé et l’action `verify` n’est pas appelée.

### Téléversement par lots

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

## Protocole API par lots

Le API par lots de LFS utilise JSON sur HTTP :

### Requête

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

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

## Intégrer LFS aux opérations du dépôt

Lors de la lecture d’un dépôt utilisant LFS, le contenu des blobs peut être constitué de pointeurs LFS plutôt que des données réelles des fichiers. Pour obtenir le contenu réel :

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

## Gestion des erreurs

Une requête par lots invalide ou en échec, ainsi qu’une requête de téléchargement en échec, provoquent `ProtocolException`. `download()` provoque aussi une erreur lorsque le serveur ne renvoie aucune URL de téléchargement pour l’objet, sans inclure le message d’erreur du serveur. `batchDownload()` et `batchUpload()` ne provoquent pas d’erreur pour les erreurs par objet ; elles les renvoient dans le champ `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";
}
```

## Propriétés des pointeurs LFS

La classe `LfsPointer` est en lecture seule :

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