أمامك فرع feature-login فيه commitان، وmain تقدّم بـ commit جديد منذ أن تفرّعت منه. تريد جمعهما. Git يعطيك طريقتين: merge وrebase. الكود الناتج في النهاية واحد، والفرق في ثلاثة أشياء: شكل التاريخ، وهل تتغيّر الـ commits الموجودة، وكيف تواجه التعارضات.
كل المخرجات هنا من مستودع حقيقي على Git 2.43.
نقطة البداية
main: أول commit ← إصلاح على main
feature-login: أول commit ← صفحة الدخول ← تحقق من المدخلات
merge: commit جديد يربط الفرعين
git switch main
git merge feature-login
* 7e7d476 Merge branch 'feature-login'
|\
| * ece0990 تحقق من المدخلات
| * a2a4945 صفحة الدخول
* | 4161258 إصلاح على main
|/
* 2bb2a39 أول commit
كل commit بقي بنفس الـ hash، وأضاف Git commit دمج (7e7d476) له أبوان. التاريخ يحكي ما حدث فعلًا: عملان جريا بالتوازي ثم اجتمعا. تفاصيل الأنواع (fast-forward والدمج الثلاثي) في درس الدمج.
rebase: إعادة إنشاء commits فرعك فوق main
git switch feature-login
git rebase main
قبل:
ece0990 تحقق من المدخلات
a2a4945 صفحة الدخول
بعد:
* 41d92f5 تحقق من المدخلات
* ec85645 صفحة الدخول
* 4161258 إصلاح على main
* 2bb2a39 أول commit
لاحظ أمرين:
- التاريخ خطي: كأنك بدأت العمل بعد
إصلاح على mainمباشرة. - الـ hashes تغيّرت:
a2a4945صارec85645. هذه ليست نفس الـ commits منقولة، بل commits جديدة بنفس التغييرات والرسائل. الـ hash يُحسب من محتوى الـ commit ومن أبيه، وأبوها تغيّر.
بعد rebase، دمج الفرع في main يصبح fast-forward بلا commit دمج:
git switch main
git merge feature-login # Fast-forward
التعارضات: لا أحد منهما يمنعها
إن عدّل فرعك وmain نفس السطر، فسيتعارضان في الطريقتين. الفرق في التجربة:
- merge يوقفك مرة واحدة بكل التعارضات، تحلّها، ثم commit دمج واحد.
- rebase يطبّق commits فرعك واحدًا تلو الآخر، ويتوقف عند كل commit يتعارض:
Rebasing (1/1)Auto-merging config.txt
CONFLICT (content): Merge conflict in config.txt
error: could not apply 8acda71... f1
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
تحلّ التعارض، ثم git add ثم git rebase --continue لينتقل إلى الـ commit التالي. فرع فيه 10 commits تعدّل نفس المنطقة قد يوقفك عدة مرات. وإن تهت في المنتصف، فـ git rebase --abort يعيدك إلى ما قبل البدء تمامًا. قراءة علامات التعارض وحلها بالتفصيل في حل تعارضات الدمج.
فخ: ours وtheirs ينقلبان أثناء rebase
أثناء merge إلى main، كلمة ours تعني main (الفرع الذي أنت عليه) وtheirs تعني الفرع القادم. أثناء rebase ينعكس المعنى: ours هي القاعدة التي تعيد البناء فوقها (main)، وtheirs هي commits فرعك أنت. فإن كتبت git checkout --ours config.txt أثناء rebase وأنت تقصد "نسختي"، ستحصل على نسخة main. اقرأ الملف بعد الحل دائمًا قبل --continue.
بعد rebase: الدفع يُرفض
إن كان فرعك مرفوعًا مسبقًا، فأول git push بعد rebase:
! [rejected] feature -> feature (non-fast-forward)
error: failed to push some refs to '...'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart.
الـ commits على الخادم لم تعد في تاريخ فرعك؛ rebase استبدلها. لا تتبع التلميح وتعمل git pull، فهذا سيدمج النسخ القديمة مع الجديدة وتحصل على كل commit مرتين. إن كان الفرع لك وحدك:
git push --force-with-lease
+ ec8ac4f...bf7bc3a feature -> feature (forced update)
--force-with-lease أأمن من --force: ترفض الدفع إن كان أحد غيرك قد أضاف commits إلى الفرع على الخادم منذ آخر مرة جلبت فيها، بدل أن تمسحها بصمت. باقي أسباب رفض الدفع في حل git push rejected.
وهنا تأتي القاعدة الذهبية: إن كان زميلك يعمل على نفس الفرع، فدفعك القسري يستبدل التاريخ الذي بنى عليه. لهذا: لا rebase لفرع يسحبه غيرك.
أخطأت؟ rebase قابل للتراجع
الـ commits القديمة لا تُحذف فورًا. git reflog يسجّل كل مكان كان فيه HEAD:
bf7bc3a HEAD@{0}: rebase (finish): returning to refs/heads/feature
bf7bc3a HEAD@{1}: rebase (pick): f2
f875053 HEAD@{2}: rebase (continue): f1
1240274 HEAD@{3}: rebase (start): checkout main
ec8ac4f HEAD@{4}: checkout: moving from main to feature
ec8ac4f هو آخر commit في فرعك قبل rebase. للرجوع إليه:
git reset --hard ec8ac4f
# أو مباشرة بعد rebase: git reset --hard ORIG_HEAD
ORIG_HEAD يحفظه Git تلقائيًا قبل العمليات الكبيرة مثل rebase وmerge. التفاصيل في درس reflog.
إذن، أيهما تستخدم؟
الاختيار يتحدد بسؤالين: هل الفرع مشترك؟ وماذا تريد أن يحكي التاريخ؟
| الموقف | الأنسب | السبب |
|---|---|---|
| فرعك تأخّر عن main وتريد آخر التحديثات قبل فتح PR | git rebase main | تحل التعارضات الآن في فرعك، والـ PR يُدمج نظيفًا |
git pull على فرعك وزميلك دفع إليه commits | git pull --rebase | تتجنّب commit دمج بلا معنى مثل Merge branch 'feature' of ... |
| commits صغيرة كثيرة ("إصلاح"، "إصلاح ثانٍ") قبل المراجعة | git rebase -i | تجمعها في commits واضحة (rebase التفاعلي) |
| دمج فرع مكتمل في main | merge (أو زر الدمج في PR) | يبقى تاريخ واضح لمتى دخلت الميزة ومن راجعها |
| فرع طويل يعمل عليه عدة أشخاص | merge فقط | rebase يكسر تاريخ كل من سحب الفرع |
| أنت غير متأكد | merge | لا يغيّر شيئًا موجودًا، ولا يحتاج دفعًا قسريًا |
لاحظ أن الفريق نفسه غالبًا يستخدم الاثنين: rebase لتحديث الفروع الشخصية، وmerge لإدخالها إلى main. وكثير من الفرق تضبط زر الدمج في GitHub على Squash and merge، فيتحوّل الـ PR كله إلى commit واحد في main، وعندها يصبح شكل تاريخ فرعك قبل الدمج أقل أهمية.
لتطبيق عملي خطوة بخطوة مع فرع حقيقي، راجع درس Rebase في مسار Git بالعربي.