
# Git LFS

Git Large File Storage (LFS) reemplaza los archivos grandes del repositorio por pequeños archivos de puntero. El contenido real se almacena en un servidor LFS independiente. Pitmaster puede analizar y crear punteros LFS y comunicarse con servidores LFS mediante el API por lotes. No lee `.gitattributes` ni ejecuta los filtros clean y smudge de LFS: `add()` almacena los archivos tal cual y `checkout()` escribe archivos de puntero. Tu código debe obtener el contenido real, como se muestra a continuación.

## Archivos de puntero LFS

Cuando un archivo está bajo seguimiento de LFS, git almacena un archivo de puntero en vez del contenido real:

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

El puntero contiene:
- `version`: la versión de la especificación LFS URL
- `oid`: hash SHA-256 del contenido real del archivo (con el prefijo `sha256:`)
- `size`: el tamaño real del archivo en bytes

### Analizar punteros

```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()` devuelve `null` si falta la línea `version`, `oid sha256:` o `size`. No valida los valores (se aceptan un OID no hexadecimal o un tamaño no numérico).

### Detectar punteros

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

`isPointer()` comprueba la presencia de los campos de versión URL, `oid sha256:` y `size` sin realizar un análisis completo.

### Crear punteros

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

### Serializar punteros

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

## Cliente LFS

El `LfsClient` se comunica con los servidores LFS mediante el API por lotes (POST a `<url>/objects/batch`).

### Crear un cliente

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

El método `fromRemoteUrl()` sigue la convención estándar: añade `/info/lfs` al URL de git (añade `.git` si no está presente). Solo las URL de remotos HTTP(S) proporcionan un URL LFS utilizable; los remotos de estilo SCP SSH (`git@host:repo.git`) no.

`LfsClient` usa el contenedor de flujos HTTP de PHP (debe estar habilitado `allow_url_fopen`). No envía credenciales e ignora el mapa `header` que devuelve cada acción, por lo que solo funciona con servidores LFS y URL de almacenamiento que no requieren autenticación. No está vinculado a un identificador de repositorio, así que realiza solicitudes de red incluso si trabajas con un repositorio abierto con `Pitmaster::processFreeOptions()`.

### Descargar archivos

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

En segundo plano, `download()`:
1. Envía una solicitud por lotes para la operación `download`
2. Recibe del servidor una URL de descarga
3. Obtiene el contenido del archivo desde esa URL

Los bytes descargados no se verifican con el OID ni con el tamaño.

### Descarga por lotes

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

### Subir archivos

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

La carga envía una solicitud por lotes para la operación `upload`. Si el servidor ya tiene el objeto, no devuelve ninguna URL de carga y la operación no hace nada. El resultado de la solicitud de carga no se comprueba, por lo que los errores de carga no se notifican y no se llama a la acción `verify`.

### Carga por lotes

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

## Protocolo de API por lotes

El API por lotes de LFS usa JSON sobre HTTP:

### Solicitud

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

### Respuesta

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

## Integrar LFS con las operaciones del repositorio

Al leer un repositorio que usa LFS, el contenido de los blobs puede ser punteros LFS en vez de los datos reales del archivo. Para obtener el contenido 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;
}
```

## Gestión de errores

Una solicitud por lotes fallida o no válida, y una solicitud de descarga fallida, generan `ProtocolException`. `download()` también genera un error cuando el servidor no devuelve ninguna URL de descarga para el objeto, sin incluir el mensaje de error del servidor. `batchDownload()` y `batchUpload()` no generan errores por errores de objetos individuales; los devuelven en el 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";
}
```

## Propiedades de los punteros LFS

La clase `LfsPointer` es de solo lectura:

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