استنساخ وجلب ودفع
يدعم Pitmaster عمليات الشبكة الثلاث الأساسية في git: الاستنساخ (التنزيل الأولي)، والجلب (التنزيل التدريجي)، والدفع (الرفع). تستخدم عناوين SSH (ssh://...، user@host:path) نقل SSH؛ وتستخدم العناوين الأخرى HTTP الذكي، كما يعود الاستنساخ والجلب إلى HTTP غير الذكي إذا فشل التبادل الذكي (راجع البروتوكولات).
تبدأ العمليات الثلاث كلها عمليات نقل عبر الشبكة، لذا فإنها تطرح RuntimeException عند استخدامها مع مقبض فُتح باستخدام ['processes' => false] أو Pitmaster::processFreeOptions().
الاستنساخ
use Pitmaster\Pitmaster;
// public static function clone(string $url, string $path, ?int $depth = null, array $options = []): Repository
$repo = Pitmaster::clone('https://github.com/user/repo.git', '/path/to/destination');
$repo = Pitmaster::clone('https://github.com/user/repo.git', '/path/to/shallow', 1);
يستخدم المستودع الجديد دائمًا SHA-1. إذا فشل الاستنساخ ولم يكن مسار الوجهة موجودًا من قبل، فسيُحذف الدليل الذي أُنشئ جزئيًا.
خطوات الاستنساخ
- التهيئة: أنشئ مستودعًا جديدًا فارغًا في مسار الوجهة
- إعداد المستودع البعيد: احفظ URL باسم
remote.origin.urlوأعدّ مواصفة الجلب - اكتشاف المراجع: استخدم
GET /info/refs?service=git-upload-packلمعرفة ما لدى الخادم - التفاوض: أرسل أسطر
wantلكل المراجع البعيدة (مع إزالة التكرار حسب التجزئة) - استلام الحزمة: نزّل ملف الحزمة الذي يحتوي على كل الكائنات المطلوبة
- كتابة الحزمة: احفظ ملف الحزمة في
.git/objects/pack/ - فهرسة الحزمة: أنشئ ملف
.idxلإجراء عمليات بحث سريعة - إعداد المراجع: أنشئ فروع تتبع المستودع البعيد (
refs/remotes/origin/*) والوسوم - تعيين HEAD: وجّه HEAD إلى الفرع الافتراضي (وفق إمكانية
symref=HEAD:...التي يعلنها الخادم، أوrefs/heads/mainإذا لم يرسل الخادم أيًا منها)، واضبطbranch.<name>.remoteوbranch.<name>.merge - استخراج نسخة العمل: استخرج ذلك الفرع، ما يكتب شجرة العمل والفهرس
تُلتقط الأخطاء أثناء استخراج نسخة العمل النهائي ويتم تجاهلها، لذا قد ينتهي الاستنساخ بشجرة عمل فارغة أو جزئية (#19).
$repo = Pitmaster::clone($url, $path);
$head = $repo->head();
echo $head->message;
$branches = $repo->branches();
$files = $repo->status();
إنشاء شجرة العمل
بعد استلام الحزمة، يستدعي Pitmaster الدالة $repo->checkout() للفرع الافتراضي. تكتب هذه الدالة كل ملف في شجرة الالتزام المشار إليه بـ HEAD إلى دليل العمل، وتنشئ الفهرس من إدخالات الشجرة، لذا يعرض status() أن شجرة العمل نظيفة.
الجلب
// public function fetch(string $remote = 'origin', ?int $depth = null): void
$repo->fetch(); // Fetch from 'origin'
$repo->fetch('upstream'); // Fetch from a named remote
$repo->fetch('origin', 1); // Shallow fetch
خطوات الجلب
- قراءة URL البعيد: من
.git/config(remote.<name>.url) - اكتشاف المراجع: احصل على الحالة الحالية لكل المراجع البعيدة
- التفاوض على المطلوب والمتاح:
- المطلوب: المراجع البعيدة المطابقة لمواصفات المرجع
remote.<name>.fetch(الافتراضي+refs/heads/*:refs/remotes/<name>/*) التي لا توجد كائناتها محليًا (كلها عند تحديد عمق) - المتاح: رؤوس كل المراجع المحلية
- المطلوب: المراجع البعيدة المطابقة لمواصفات المرجع
- استلام الحزمة: نزّل الكائنات الجديدة فقط
- كتابة الحزمة: احفظها في
.git/objects/pack/وأنشئ فهرسها - تحديث مراجع التتبع: حدّث
refs/remotes/<remote>/<branch>لكل فرع بعيد مطابق، واكتب كل مرجع وسم معلن. لا تُطلب كائنات الوسوم على نحو منفصل، لذا قد يُكتب وسم مشروح جديد كمرجع إلى كائن لم يُنزّل (#24)
// Fetch is incremental: only downloads what's new
$repo->fetch();
// After fetching, merge or rebase to incorporate changes (use the full ref name)
$result = $repo->merge('refs/remotes/origin/main');
يبحث resolve() عن الأسماء بصيغ refs/heads/<name> وrefs/tags/<name> والاسم نفسه، لذا لا يعثر على الصيغة المختصرة origin/main.
يطلب Pitmaster حزمًا رقيقة لا يستطيع فهرستها بعد (#16). إذا استجاب الخادم لجلب تدريجي بحزمة رقيقة، فسيطرح fetch() الخطأ PackParseException.
محدّث بالفعل
إذا كان كل كائن تشير إليه المراجع البعيدة موجودًا محليًا بالفعل، فلن تُطلب أي حزمة، وسيعود fetch() بعد اكتشاف المراجع. في هذه الحالة لن تُحدّث مراجع التتبع أيضًا، حتى إن انتقلت الفروع البعيدة إلى التزامات موجودة لديك بالفعل (#23).
الدفع
$repo->push(); // Push current branch to 'origin'
$repo->push('origin', 'main'); // Push a specific branch
$repo->push('upstream', 'feature'); // Push to a named remote
خطوات الدفع
- قراءة URL البعيد: من
.git/config - تحديد ما سيُدفع: حلّ الفرع المحلي للوصول إلى تجزئة الالتزام
- اكتشاف المراجع البعيدة: احصل على الحالة الحالية للفرع البعيد (لحساب التجزئة القديمة)
- التحقق من التقديم السريع: إذا لم يكن الفرع البعيد سلفًا للفرع المحلي، فاطرح
RuntimeException("Push rejected: non-fast-forward update to <branch>")قبل إرسال أي شيء - إرسال تحديث المرجع:
<old-hash> <new-hash> refs/heads/<branch> - إرسال بيانات الحزمة: حزمة تضم الكائنات التي يحتاج إليها المستودع البعيد
- التحقق من النتيجة: يرد الخادم بالنجاح أو الإخفاق لكل مرجع
تُشغّل الخطّافة pre-push قبل إرسال التحديث، ما لم تُعطّل الخطّافات.
// Push the current branch
$repo->push();
// Push a specific branch to a specific remote
$repo->push('origin', 'feature/login');
متطلبات الدفع
- يجب أن يكون الفرع الحالي موجودًا محليًا
- يجب إعداد مستودع بعيد مع URL
- يجب أن يقبل المستودع البعيد الدفع (المصادقة، والأذونات، وسياسات عدم التقديم السريع)
الدفع القسري
يرسل Pitmaster التجزئة القديمة من إعلان المرجع المباشر للمستودع البعيد، ويرفض push() محليًا التحديثات التي لا تحقق التقديم السريع. للكتابة فوق فرع بعيد، استخدم force-with-lease:
// Lease defaults to refs/remotes/<remote>/<branch>; pass an ObjectId to set it explicitly
$repo->pushForceWithLease('origin', 'main');
$repo->pushForceWithLease('origin', 'main', $repo->resolve('refs/remotes/origin/main'));
// Update several branches in one atomic request
$repo->pushAtomic('origin', ['main', 'release']);
// Make the remote mirror local refs (also deletes remote refs that no longer exist locally)
$repo->pushMirror('origin');
UploadPackClient (تفاصيل تنفيذ الجلب)
use Pitmaster\Protocol\UploadPackClient;
use Pitmaster\Protocol\SmartHttpClient;
$http = new SmartHttpClient();
$uploadPack = new UploadPackClient($http);
// Fetch with explicit wants and haves
$packData = $uploadPack->fetch($url, $wants, $haves);
ينشئ التفاوض على المطلوب والمتاح طلبًا بصيغة pkt-line:
want <hash1> <capabilities>\n
want <hash2>\n
deepen <depth>\n (only when a depth is given)
0000
have <hash3>\n
have <hash4>\n
done\n
يرد الخادم بإقرارات، ثم ببيانات الحزمة (وقد تكون مغلفة بنطاق جانبي).
ReceivePackClient (تفاصيل تنفيذ الدفع)
use Pitmaster\Protocol\ReceivePackClient;
use Pitmaster\Protocol\SmartHttpClient;
$http = new SmartHttpClient();
$receivePack = new ReceivePackClient($http);
$receivePack->push($url, [
[
'old' => $oldId, // Current remote hash (or zero hash for new branch)
'new' => $localId, // Local commit hash to push
'ref' => 'refs/heads/main',
],
], $packData);
صيغة طلب الدفع:
<old-hash> <new-hash> refs/heads/main\0<capabilities>\n
0000
PACK<pack data>
معالجة الأخطاء
use Pitmaster\Exceptions\ProtocolException;
try {
$repo->fetch();
} catch (ProtocolException $e) {
// Network unreachable, auth failed, server error, etc.
echo $e->getMessage();
}
try {
$repo->push();
} catch (\RuntimeException $e) {
// Remote not configured, not on a branch, branch not found, non-fast-forward
echo $e->getMessage();
}
يمكن أن يطرح fetch() أيضًا خطأً عاديًا من نوع RuntimeException (مستودع بعيد غير معروف، أو مقبض بلا عملية) أو PackParseException (حزمة لا يمكن فهرستها). يرث كل من ProtocolException وPackParseException من \RuntimeException، لذا فإن التقاط \RuntimeException يشملها كلها.
إعداد المستودع البعيد
تُخزّن المستودعات البعيدة في .git/config:
[remote "origin"]
url = https://github.com/user/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
الوصول عبر إعداد API:
$config = $repo->config();
$url = $config->get('remote.origin.url');
$config->set('remote.upstream.url', 'https://github.com/other/repo.git');
$config->set('remote.upstream.fetch', '+refs/heads/*:refs/remotes/upstream/*');
// set() changes the in-memory config only; write it to persist the change
$config->writeToFile($repo->commonGitDir() . '/config');
بيانات الحزمة للدفع
حاليًا، يجمع Pitmaster الكائنات التي يمكن الوصول إليها من الالتزامات الجاري دفعها، ولا يمكن الوصول إليها من المراجع الحالية للمستودع البعيد، ثم يكتبها في حزمة ككائنات أساسية (من دون ضغط دلتا). هذا صحيح وظيفيًا، لكنه ينتج حزمًا أكبر من git push. يُخطط لتحسين كتابة الحزم بإضافة ضغط دلتا.