التفرع والدمج بعمق
التفرع والدمج بعمق
في الدرس السابق تعلمت كيف يخزن Git البيانات على شكل رسم بياني موجه لا دوري من الكائنات. الآن نستغل هذا البناء لنفهم ما هو الفرع حقاً، ولماذا يستغرق إنشاؤه ميلي ثانية، وما الذي يحدث داخلياً عند الدمج أو عند نشوء تعارض. في Google وMeta يدفع عشرات المهندسين إلى نفس المستودع كل دقيقة — وكلهم يعتمدون على الآليات التي يغطيها هذا الدرس.
الفروع مجرد مؤشرات خفيفة
الفرع ليس سوى ملف داخل .git/refs/heads/ يحتوي على 40 حرفاً (SHA-1 أو SHA-256 حديثاً) يشير إلى كائن commit. إنشاء الفرع لا ينسخ أي ملفات؛ بل يكتب 41 بايتاً فقط على القرص.
في كل مرة تُنشئ commit، يُحرِّك Git مؤشر الفرع الحالي إلى الـ commit الجديد. يتبعه HEAD دائماً، إلا إذا كنت في حالة detached HEAD — أي أن HEAD يشير مباشرةً إلى commit وليس إلى ملف فرع.
الدمج السريع (Fast-Forward)
عندما لم يتشعب الفرع الهدف (مثل main) عن الفرع المُدمَج — أي أن كل commit في main هو جد للفرع الآخر — يكتفي Git بتحريك المؤشر دون إنشاء commit دمج. يُسمى هذا Fast-Forward.
--no-ff حتى يظهر كل دمج لميزة كحدث مستقل في git log --graph. هذا لا يُقدَّر بثمن في تحقيقات الحوادث: "متى وصلت auth-service إلى main؟" تصبح له إجابة واضحة وقابلة للبحث.
الدمج الثلاثي وcommits الدمج
عندما يتشعب الفرعان، لا يستطيع Git تطبيق fast-forward. بدلاً من ذلك يبحث عن قاعدة الدمج — أحدث جد مشترك بين طرفي الفرعين — ثم يطبق خوارزمية الدمج الثلاثي مقارناً: القاعدة، وطرف فرعنا، وطرف الفرع الآخر. إذا عدّل الطرفان نفس الأسطر، ينشأ تعارض.
حل التعارضات
التعارض يعني أن Git لا يستطيع التوفيق تلقائياً بين مجموعتي تغييرات على نفس المنطقة في نفس الملف. يكتب Git علامات التعارض داخل الملف ويتوقف:
HEAD هو فرعك الحالي (ما لديك). القسم بعد ======= هو ما يأتي من الفرع الآخر. مهمتك إنتاج النتيجة الصحيحة — وغالباً تجمع بين التغييرين:
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.
git revert للتراجع، ولا تستخدم git push --force.
استراتيجيات الدمج على نطاق واسع
يدعم Git استراتيجيات دمج قابلة للتوصيل تُمرَّر عبر -s. الاستراتيجية الافتراضية هي ort (مُقدَّمة في Git 2.34، أسرع وأدق من recursive القديمة). للتعارضات الثنائية أو الملفات المُولَّدة، تحتفظ -s ours بنسختك بالكامل. يمرر الخيار -X خيارات للاستراتيجية: -X ours يحل التعارضات تلقائياً بتفضيل جانبك، وهو مفيد عند دمج فرع إصدار في main سريعة التطور.
تُنتج دمجات Squash تاريخاً خطياً على main دون تكلفة إعادة الأساس، على حساب فقدان تفاصيل الـ commits الفردية. تعتمد شركات كثيرة (Shopify وStripe) دمج squash افتراضياً على فرعها الرئيسي لأن كل commit على main يصبح وحدة كاملة وقابلة للنشر.