Git وتدفقات العمل التعاونية

التفرع والدمج بعمق

18 دقيقة الدرس 2 من 28

التفرع والدمج بعمق

في الدرس السابق تعلمت كيف يخزن Git البيانات على شكل رسم بياني موجه لا دوري من الكائنات. الآن نستغل هذا البناء لنفهم ما هو الفرع حقاً، ولماذا يستغرق إنشاؤه ميلي ثانية، وما الذي يحدث داخلياً عند الدمج أو عند نشوء تعارض. في Google وMeta يدفع عشرات المهندسين إلى نفس المستودع كل دقيقة — وكلهم يعتمدون على الآليات التي يغطيها هذا الدرس.

الفروع مجرد مؤشرات خفيفة

الفرع ليس سوى ملف داخل .git/refs/heads/ يحتوي على 40 حرفاً (SHA-1 أو SHA-256 حديثاً) يشير إلى كائن commit. إنشاء الفرع لا ينسخ أي ملفات؛ بل يكتب 41 بايتاً فقط على القرص.

# اكتشاف ما يمثله الفرع فعلياً على القرص cat .git/refs/heads/main # → e4a3f2c1d9b8a7c6e5f4d3c2b1a09876543210ab # إنشاء فرع — فوري، لا نسخ للملفات git branch feature/auth-service # HEAD يشير إلى الفرع الحالي cat .git/HEAD # → ref: refs/heads/main # التبديل (HEAD يشير الآن إلى الفرع الجديد) git switch feature/auth-service cat .git/HEAD # → ref: refs/heads/feature/auth-service

في كل مرة تُنشئ commit، يُحرِّك Git مؤشر الفرع الحالي إلى الـ commit الجديد. يتبعه HEAD دائماً، إلا إذا كنت في حالة detached HEAD — أي أن HEAD يشير مباشرةً إلى commit وليس إلى ملف فرع.

Branches as pointers to commits C1 Initial commit C2 Add router C3 Auth service C4 Fix logging feature/auth-service main HEAD
الفروع هي ملفات مؤشر بحجم 41 بايت. يشير كل من main وfeature/auth-service إلى آخر commit في كل منهما، ويشير HEAD إلى الفرع النشط.

الدمج السريع (Fast-Forward)

عندما لم يتشعب الفرع الهدف (مثل main) عن الفرع المُدمَج — أي أن كل commit في main هو جد للفرع الآخر — يكتفي Git بتحريك المؤشر دون إنشاء commit دمج. يُسمى هذا Fast-Forward.

# السيناريو: main عند C2، feature/auth-service عند C3 # C3 والده C2 — يمكن تطبيق fast-forward git switch main git merge feature/auth-service # الناتج: Fast-forward # auth.go | 42 ++++++++++ # 1 file changed, 42 insertions(+) # فرض إنشاء commit دمج حتى عند إمكانية FF: git merge --no-ff feature/auth-service -m "Merge feature/auth-service into main"
متى تستخدم --no-ff: تفرض كثير من الفرق (وأزرار الدمج الافتراضية في GitHub وGitLab) استخدام --no-ff حتى يظهر كل دمج لميزة كحدث مستقل في git log --graph. هذا لا يُقدَّر بثمن في تحقيقات الحوادث: "متى وصلت auth-service إلى main؟" تصبح له إجابة واضحة وقابلة للبحث.

الدمج الثلاثي وcommits الدمج

عندما يتشعب الفرعان، لا يستطيع Git تطبيق fast-forward. بدلاً من ذلك يبحث عن قاعدة الدمج — أحدث جد مشترك بين طرفي الفرعين — ثم يطبق خوارزمية الدمج الثلاثي مقارناً: القاعدة، وطرف فرعنا، وطرف الفرع الآخر. إذا عدّل الطرفان نفس الأسطر، ينشأ تعارض.

Three-way merge creating a merge commit C2 (base) Merge base C4 (main) Fix logging C3 (feature) Auth service C5 (merge) Two parents Three-way merge compares all three main
الدمج الثلاثي: يجد Git الجد المشترك (C2)، يقارن التغييرات من الفرعين، ثم ينشئ commit دمج جديد (C5) بوالدين اثنين.

حل التعارضات

التعارض يعني أن Git لا يستطيع التوفيق تلقائياً بين مجموعتي تغييرات على نفس المنطقة في نفس الملف. يكتب Git علامات التعارض داخل الملف ويتوقف:

<<<<<<< HEAD return authenticate(user, password, mfa_required=True) ======= return authenticate(user, password, timeout=30) >>>>>>> feature/auth-service

HEAD هو فرعك الحالي (ما لديك). القسم بعد ======= هو ما يأتي من الفرع الآخر. مهمتك إنتاج النتيجة الصحيحة — وغالباً تجمع بين التغييرين:

# 1. افتح الملف المتعارض وعدّله إلى الحالة الصحيحة، مثلاً: # return authenticate(user, password, mfa_required=True, timeout=30) # 2. أضف الملف المحلول إلى منطقة التدريج git add src/auth.py # 3. أكمل الدمج git commit # Git يملأ رسالة الـ commit مسبقاً؛ اقبلها أو أضف ملاحظات. # لإلغاء الدمج في المنتصف (يستعيد الحالة قبل الدمج): git merge --abort # استخدام أداة دمج ثلاثية اللوحات: git mergetool
اضبط أداة الدمج مرة واحدة واستخدمها دائماً. في Google، يضبط المهندسون عادةً vimdiff أو meld أو تكاملاً مع IDE حتى يفتح git mergetool عرضاً ثلاثي اللوحات (LOCAL / BASE / REMOTE → MERGED). اضبط أداتك بـ git config --global merge.tool vimdiff وgit config --global mergetool.keepBackup false.

أنماط الفشل الشائعة في الإنتاج

  • الفروع طويلة العمر تتراكم فيها التعارضات. فرع ميزة مفتوح أسبوعين مقابل main نشطة قد يجمع مئات التعارضات. الحل: الدمج أو إعادة الأساس من main يومياً وليس قبيل تقديم طلب السحب.
  • دمجات الأخطبوط المتعثرة. git merge branchA branchB branchC ينفذ دمج أخطبوط (commit واحد بآباء متعددة). يرفض Git ذلك عند وجود تعارضات؛ استخدم دمجات متتالية بدلاً من ذلك.
  • تعارضات الملفات الثنائية. لا يستطيع Git دمج صورة PNG أو ملفاً ثنائياً ثلاثياً. اعتمد Git LFS وحدد استراتيجيات الدمج (*.png merge=ours في .gitattributes) للاحتفاظ بجانب واحد دائماً.
  • نسيان حذف الفروع المدمجة. المستودعات التي تحتوي على آلاف الفروع القديمة تصبح بطيئة. فرض حذف الفرع تلقائياً: GitHub وGitLab لديهما خيار "Delete branch on merge". أضف مهمة دورية لتنظيف refs التتبع: git fetch --prune.
لا تُنفِّذ أبداً force-push على فرع مشترك بعد دمج. إذا دمجت فرع زميل في فرعك ثم نفّذ أحدهم force-push، تُعاد كتابة commit الدمج وآباؤه. الزملاء الذين سبق لهم السحب سيتباعدون. عامل أي فرع سحبه شخصان على أنه غير قابل للتعديل — استخدم git revert للتراجع، ولا تستخدم git push --force.

استراتيجيات الدمج على نطاق واسع

يدعم Git استراتيجيات دمج قابلة للتوصيل تُمرَّر عبر -s. الاستراتيجية الافتراضية هي ort (مُقدَّمة في Git 2.34، أسرع وأدق من recursive القديمة). للتعارضات الثنائية أو الملفات المُولَّدة، تحتفظ -s ours بنسختك بالكامل. يمرر الخيار -X خيارات للاستراتيجية: -X ours يحل التعارضات تلقائياً بتفضيل جانبك، وهو مفيد عند دمج فرع إصدار في main سريعة التطور.

# تفضيل الجانب "ours" عند أي تعارض (مع بقاء الدمج الحقيقي) git merge -X ours release/v2.1 # الاحتفاظ بالفرع الحالي بالكامل — يُسجَّل تاريخ الفرع الآخر # دون تطبيق أي من تغييراته: git merge -s ours hotfix/legacy # دمج كل commits الفيتشر في تغيير واحد غير مُدمَج، ثم commit يدوي: git merge --squash feature/data-pipeline git commit -m "feat: add data pipeline (squashed)"

تُنتج دمجات Squash تاريخاً خطياً على main دون تكلفة إعادة الأساس، على حساب فقدان تفاصيل الـ commits الفردية. تعتمد شركات كثيرة (Shopify وStripe) دمج squash افتراضياً على فرعها الرئيسي لأن كل commit على main يصبح وحدة كاملة وقابلة للنشر.