Git LFS
يستبدل Git Large File Storage (LFS) الملفات الكبيرة في مستودعك بملفات مؤشرات صغيرة. ويُخزَّن محتوى الملف الفعلي على خادم LFS منفصل. يمكن لـ Pitmaster تحليل مؤشرات LFS وإنشاؤها والتواصل مع خوادم LFS عبر API الدفعي. لا يقرأ .gitattributes ولا يشغّل مرشحي clean وsmudge في LFS: يخزّن 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";
}
يعيد parse() القيمة null عند غياب سطر version أو oid sha256: أو size. ولا يتحقق من صحة القيم (إذ يقبل 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 مع خوادم LFS باستخدام API الدفعي (POST إلى <url>/objects/batch).
إنشاء عميل
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 إن لم يكن موجودًا). وحدها عناوين URL البعيدة من نوع HTTP(S) توفر URL صالحًا للاستخدام مع LFS؛ أما عناوين SSH البعيدة بصيغة SCP (git@host:repo.git) فلا تفعل ذلك.
يستخدم LfsClient غلاف التدفق HTTP الخاص بـPHP (يجب تفعيل 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 الدفعي
يستخدم API الدفعي في LFS 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، قد يكون محتوى الكائن الثنائي مؤشرات 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. ويطرح download() أيضًا خطأً عندما لا يعيد الخادم URL للتنزيل الخاص بالكائن، دون رسالة الخطأ من الخادم. أما 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