Saltar al contenido

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:

Código
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

Código
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