Git LFS
Git 大文件存储(LFS)会将仓库中的大文件替换为小型指针文件。实际文件内容存储在单独的 LFS 服务器上。Pitmaster 可以解析和创建 LFS 指针,并通过批处理 API 与 LFS 服务器通信。它不会读取 .gitattributes,也不会运行 LFS clean 和 smudge 过滤器:add() 会原样存储文件,而 checkout() 会写入指针文件。获取实际内容由你的代码负责,如下所示。
LFS 指针文件
跟踪文件时,LFS 会用指针文件代替实际内容,git 会存储该指针文件:
version https://git-lfs.github.com/spec/v1
oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393
size 12345
指针包含:
version:LFS 规范版本 URLoid:实际文件内容的 SHA-256 哈希(前缀为sha256:)size:实际文件大小,单位为字节
解析指针
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";
}
如果缺少 version、oid sha256: 或 size 行,parse() 会返回 null。它不会验证值(非十六进制 OID 或非数字大小也会被接受)。
检测指针
if (LfsPointer::isPointer($blobContent)) {
// This blob is an LFS pointer, not actual file content
$pointer = LfsPointer::parse($blobContent);
}
isPointer() 会检查版本 URL、oid sha256: 和 size 字段是否存在,但不会完整解析。
创建指针
// 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
序列化指针
$content = $pointer->serialize();
// "version https://git-lfs.github.com/spec/v1\noid sha256:4d7a21...\nsize 12345\n"
LFS 客户端
LfsClient 使用批处理 API 与 LFS 服务器通信(向 <url>/objects/batch 发送 POST 请求)。
创建客户端
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);
fromRemoteUrl() 方法遵循标准惯例:将 /info/lfs 附加到 git URL(如果尚不存在,则添加 .git)。只有 HTTP(S) 远程 URL 才能提供可用的 LFS URL;SCP 风格的 SSH 远程地址(git@host:repo.git)则不行。
LfsClient 使用 PHP 的 HTTP 流包装器(必须启用 allow_url_fopen)。它不会发送凭据,也会忽略每个操作返回的 header 映射,因此仅适用于无需身份验证的 LFS 服务器和存储 URL。它不依赖仓库句柄,因此即使你使用的是通过 Pitmaster::processFreeOptions() 打开的仓库,它也会发出网络请求。
下载文件
// Single file download
$content = $client->download($pointer->oid, $pointer->size);
file_put_contents('large-file.bin', $content);
在底层,download() 会:
- 为
download操作发送批处理请求 - 从服务器接收下载 URL
- 从该 URL 获取文件内容
下载的字节不会与 OID 或大小进行比对。
批量下载
$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']
}
}
上传文件
$client->upload($pointer->oid, $pointer->size, $fileContent);
上传会为 upload 操作发送批处理请求。如果服务器已拥有该对象,就不会返回上传 URL,操作也不会执行任何操作。上传请求本身的结果不会被检查,因此上传失败不会被报告,也不会调用 verify 操作。
批量上传
$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
}
批处理 API 协议
LFS 批处理 API 使用 JSON,通过 HTTP 传输:
请求
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}
]
}
响应
{
"objects": [
{
"oid": "abc123...",
"size": 12345,
"actions": {
"download": {
"href": "https://storage.example.com/abc123...",
"header": {
"Authorization": "Bearer token"
}
}
}
}
]
}
将 LFS 集成到仓库操作中
读取使用 LFS 的仓库时,blob 内容可能是 LFS 指针,而非实际文件数据。要获取实际内容:
$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;
}
错误处理
批处理请求失败或无效,以及下载请求失败,都会抛出 ProtocolException。如果服务器没有为对象返回下载 URL,download() 也会抛出错误,但不会包含服务器的错误消息。batchDownload() 和 batchUpload() 不会因单个对象错误而抛出异常;它们会将错误放在 error 字段中返回。
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";
}
LFS 指针属性
LfsPointer 类为只读:
$pointer->oid; // string: SHA-256 hash (hex)
$pointer->size; // int: file size in bytes
$pointer->version; // string: spec version URL