git merge vs rebase: هل غادر الكوميت جهازك؟
الشجرة نفسها وتاريخان — تشغيل الاختبار الذي عاد فيه 5d48940 على هيئة 6b2be0a، وتحذير الفرع المشترك، ومخرج reflog حين يفوز الخيار الخطأ.
■ space · نثر
سؤال واحد يحسم الأمر: هل غادر الكوميت جهازك؟ عمل لم يُدفع بعد → rebase. أي شيء ربما سحبه الآخرون → merge.
الـ merge يسجّل ما حدث — كوميت واحد، أبوان، ولا يتغير أي SHA. الـ rebase يعيد الكتابة: في الاختبار أدناه عاد الكوميت 5d48940 على هيئة 6b2be0a.
هل خرج الـ rebase عن المسار؟ git reflog + git reset --hard HEAD@{1} يعيدان الفرع إلى مكانه. لا شيء ضاع.
التاريخان
كلا الأمرين يوصل الكود نفسه؛ لكنهما يختلدان حول ما ينبغي للسجل أن يقوله بعد ذلك. شغّلت كليهما في منصة اختبار استعمال واحد على git 2.47.3، وبالكوميتات الخمسة نفسها في كل مرة — تشغيل الـ merge حفظ كل SHA أصلي وأضاف عقدة واحدة؛ تشغيل الـ rebase غيّر هوية كوميتات الفرع. الشجرة نفسها، وتاريخ مختلف:
fig — النسخ المعلّمة هي التغييرات نفسها بمعرّفات كوميت جديدة: دخل c3 بصفة 5d48940 وخرج بصفة 6b2be0a
تحقق: git log --oneline --graph → العُقدة "|\\\\" تعني merge، والخط المستقيم يعني أن الـ rebase هو من فاز
حين يتحول الـ merge إلى fast-forward
إذا لم يتحرك main منذ أن أنشأت فرعك، فلا شيء لدمجه — git مجرد أن يزيح العلامة إلى الأمام. هذا هو الـ fast-forward، وهو سبب حصول أنصار الـ rebase على خطهم النظيف دون إجبار أحد: اعمل rebase لفرعك فوق main، فيصبح الـ merge fast-forward، ويبقى الرسم خطًا مستقيمًا واحدًا. من المنصة — خمسة كوميتات، صفر عقد:
* 67c0442 c4 feature 2 # rebased, then merged — fast-forward
* 6b2be0a c3 feature 1 # new SHAs — 5d48940 before the rebase
* 1398836 c5 main moved on
* facd5a4 c2 main work
* f45b000 c1 base
تحقق: git merge --ff-only feature → كلمة "Fast-forward" في المخرجات دون إنشاء كوميت دمج
الثمن: تلك الـ SHA المعلّمة لم تعد موجودة في أي مكان سوى جهازك. ادفعهما وسيظل نسخة زميلك تحتفظ بالأصل — نسختان للكوميت "نفسه".
أي الكوميتات ما زالت لك وحدك؟
الجواب أمر واحد، وليس إحساسًا. git log origin/main..HEAD يعرض الكوميتات التي لم يرَها المستقبل البعيد أبدًا — وهذه وحدها قابلة لإعادة التأسيس. إذا ضمّت القائمة كوميتًا قد يمتلكه نسخة أخرى فعلًا — دفعة، أو طلب دمج مفتوح، أو فرع دفعته ثم أعدت دفعه بالقوة من قبل — فهو لم يبق لك وحدك مهما ادّعى رسمك المحلي. العمل غير المدفوع هو بالضبط ما صُنع من أجله الـ rebase: أعده فوق القاعدة المحدّثة فيقرأ طلب الدمج الخاص بك تسلسلًا نظيفًا واحدًا. ولهذا السبب أيضًا يعد git pull --rebase افتراضًا رشيدًا للفروع الخاصة — فهو يعيد تشغيل كوميتاتك المحلية فوق ما ورد، بدل نسج عقدة دمج في كل مزامنة. اعتمد على الفرع لا على الحدس: git fetch origin أولًا، ثم git status -sb لثنائي التقدم/التأخر.
ما الذي ينكسر على فرع مشترك
كل نسخة تحتفظ بالكوميتات القديمة أصبحت الآن تخالف نسختك حول الواقع. الـ rebase يمنح كوميتاتك رموز SHA جديدة؛ فرعك المُدفع وفرعك المُعاد كتابته لم يعودا يشتركان في نسب، فالدفع المقبل يحتاج --force — وكل من سحب النسخة القديمة سيضطر إلى دمج نسخته من كوميتات "لم تعد موجودة". وتظهر نُسَخهم المكررة في كل دمج لاحق، وفكّ ذلك يكلف عصرًا كاملًا. وتنفصل تعليقات المراجعين المرتبطة برموز SHA بعينها في الحركة نفسها — أعد تأسيس طلب دمج مفتوح ويتبخر سياق المراجعة مع معرّفات الكوميت القديمة. القاعدة جملة واحدة، والكتاب الرسمي يحملها بوصفها تحذيرًا لا اقتراحًا:
لا تعمل rebase لكوميتات موجودة خارج مستودعك تكون الناس قد بنوا أعمالهم عليها.— Pro Git، الطبعة الثانية، "Git Branching — Rebasing"
التراجع عن rebase
الكوميتات القديمة تنجو من الـ rebase — reflog هو مخبأها. الـ rebase يحرك مؤشرات الفروع؛ إنه لا يحذف الكائنات. استرجاع المنصة، بعد رمى reset --hard كوميتين بعيدًا:
$ git reflog | head -4
1398836 HEAD@{0}: reset: moving to HEAD~2
67c0442 HEAD@{1}: merge feature: Fast-forward
1398836 HEAD@{2}: checkout: moving from feature to main
67c0442 HEAD@{3}: rebase (finish): returning to refs/heads/feature
$ git reset --hard HEAD@{1}
HEAD is now at 67c0442 c4 feature 2
سطر واحد، والفرع مستعاد، ومعه العمل المدرَّج وغير المدرَّج. تنتهي صلاحية مدخلات الـ reflog بعد نحو 90 يومًا افتراضيًا، فللإنقاذ ساعة تدق.
تحقق: git reflog | head -3 → قمة ما قبل rebase تجلس في HEAD@{1}، على بعد reset واحد
الحالات الست
| الحالة | الأمر | لماذا يفوز |
|---|---|---|
| فرع الميزة الخاص بك، لم يُدفع قط | git rebase main | تاريخ خطي، تسلسل نظيف واحد للمراجعة |
| فرع يسحب منه الآخرون | git merge | لا إعادة كتابة للـ SHA — لا كوميتات مكررة في نسخة أحد |
| main محلي متباعد، ولا شيء مُدفع | git pull --rebase | يزامن دون عقدة؛ تبقى كوميتاتك في الأعلى |
| نسخة fork تلحق بالمصدر الأعلى | git merge upstream/main | يظل طلب الدمج ملتصقًا بكوميتاته |
| إصلاح عاجل لا بد أن يهبط الآن | git merge --no-ff | علامة مرئية لمكان دخول الإصلاح، حتى على فرع يقبل الـ fast-forward |
| خرج الـ rebase عن مساره | git reset --hard HEAD@{1} | لا يزال الـ reflog يحتفظ بقمة ما قبل rebase (~90 يومًا) |
أين يلعب --squash دوره؟ يأخذ تغييرات فرع ويدرجها ككتلة واحدة غير مُوَلَّدة — بلا كوميت دمج، وبلا ربط بتاريخ الفرع. الضغط هو الجواب الثالث، لإلقاء الفرع بعد هبوط صافي تغيّره؛ فهو لا يعيد كتابة شيء، إنما يرفض فقط تسجيل الخطوات الداخلية للفرع. وهل يمكن لفريق أن يختار واحدًا فقط؟ كثيرون يفعلون: rebase للعمل المحلي، وmerge نحو المشترك هي السياسة الأكثر شيوعًا، كما يصلح "rebase للكل" على مستوى المستودع — شريطة ألا يُعاد كتابة أي فرع مشترك قط، وهي الجملة الوحيدة التي تنجو من كل جدال حول سير العمل. النسخة المختصرة في عشر ثوان، مرة أخيرة: هل غادر الكوميت جهازك؟ لا → rebase. نعم → merge. وإذا فاز الخطأ رغم ذلك، فإن لدى reflog مخرجًا.
نهاية التاريخين · لا شيء سُحب · كل شيء محلي