git rebase
إعادة تطبيق commits فوق أساس آخر
ينقل git rebase سلسلة من الـ commits أو يضمّها إلى commit أساس جديد. يُستخدم كثيرًا للحفاظ على تاريخ خطي بإعادة تطبيق commits فرع الميزة فوق أحدث main. وبخلاف merge، يعيد rebase كتابة التاريخ بإنشاء commits جديدة بـ SHA مختلف. ويتيح rebase التفاعلي (-i) ضم الـ commits أو إعادة ترتيبها أو تعديلها أو إسقاطها قبل إعادة تطبيقها.
الصيغة
git rebase [-i | --interactive] [<options>] [--exec <cmd>] [--onto <newbase> | --keep-base] [<upstream> [<branch>]]
git rebase [-i | --interactive] [<options>] [--exec <cmd>] [--onto <newbase>] --root [<branch>]
git rebase (--continue | --skip | --abort | --quit | --edit-todo | --show-current-patch)الخيارات
اختيار الخيار المناسب
مقارنة git rebase بـ git merge
متى تستخدم rebase ومتى merge لدمج التغييرات
| الأمر | الجانب | Git Rebase | Git Merge |
|---|---|---|---|
History | خطي (يعيد كتابة الـ commits) | يحافظ على التاريخ المتباعد | |
Commits | ينشئ commits جديدة بـ SHA مختلف | يحافظ على الـ commits الأصلية + commit دمج | |
Destructive | نعم - يغيّر hashes الـ commits | لا - يضيف commit دمج فقط | |
Conflicts | تُحل لكل commit أثناء إعادة التطبيق | تُحل مرة واحدة عند نقطة الدمج | |
Best for | الفروع المحلية/الخاصة قبل الدفع | الفروع العامة/المشتركة | |
Traceability | يصعب رؤية نقاط الدمج | سجل واضح لدمج الفروع |
أمثلة على سير العمل
git fetch originاحصل على أحدث التغييرات من المستودع البعيد
git checkout featureانتقل إلى فرع ميزتك
git rebase origin/mainأعد تطبيق commits الخاصة بك فوق أحدث main
git push --force-with-leaseادفع الفرع المُعاد تأسيسه (فقط إن لم يكن مشتركًا)
git log --onelineاحسب عدد الـ commits المراد ضمها
git rebase -i HEAD~5rebase تفاعلي لآخر 5 commits
(in editor) pick, squash, squash...أبقِ الأول وضمّ البقية إليه
(edit combined message)اكتب رسالة commit نظيفة
(make your fix)أصلح المشكلة في الشيفرة
git commit --fixup=<commit-hash>أنشئ commit من نوع fixup للهدف
git rebase --autosquash mainيرتّب autosquash ويضم تلقائيًا (لا حاجة إلى -i منذ Git 2.44)
git rebase -i HEAD~4rebase تفاعلي لآخر 4 commits
(in editor) reorder linesانقل أسطر الـ commits إلى مواضع جديدة
save and close editorيعيد Git تطبيقها بالترتيب الجديد
git rebase -i HEAD~3ابدأ rebase تفاعليًا
(mark commit with 'edit')توقّف عند الـ commit المراد تقسيمه
git reset HEAD^ألغِ الـ commit مع إبقاء التغييرات غير مجهّزة في مجلد العمل
git add file1 && git commit -m 'Part 1'أنشئ أول commit أصغر
git add file2 && git commit -m 'Part 2'أنشئ ثاني commit أصغر
git rebase --continueتابع مع الـ commits المتبقية
git reflogاعثر على الـ commit الذي كان قبل rebase
git reset --hard HEAD@{n}عُد إلى حالة ما قبل rebase
حل المشكلات
تعارض أثناء rebase
السبب: تغييرات الـ commit الخاص بك تتعارض مع الفرع الأساس
عدّل الملفات لحل التعارضات، ونفّذ git add عليها، ثم git rebase --continue
git add <resolved-files> && git rebase --continueCannot rebase: You have unstaged changes
السبب: في مجلد العمل تعديلات غير محفوظة
احفظها في commit، أو خبّئها، أو استخدم --autostash للتعامل معها
git rebase --autostash mainصار الـ commit فارغًا بعد rebase
السبب: تغييرات هذا الـ commit موجودة أصلًا في الفرع الهدف
يُسقطه rebase العادي تلقائيًا؛ ويتوقف التفاعلي - شغّل --skip لإسقاطه، أو 'git commit --allow-empty' لإبقائه
git rebase --skipينتج عن rebase تعارضات كثيرة
السبب: تباعد الفرع كثيرًا عن الأساس
فكّر في استخدام merge بدلًا منه، أو ألغِ ونفّذ rebase على خطوات أصغر
git rebase --abortضياع commits بعد rebase
السبب: الـ commits الأصلية يتيمة (لم تُحذف، بل لم يعد شيء يشير إليها)
استخدم الـ reflog لإيجاد الـ commits الأصلية واستعادتها
git reflog && git branch recovery HEAD@{n}رُفض الدفع (non-fast-forward) بعد rebase
السبب: أعاد rebase كتابة الـ commits، فلم يعد فرعك يحتوي تاريخ النسخة البعيدة
استخدم --force-with-lease لدفع بالإجبار أكثر أمانًا (يرفض إن دفع شخص آخر في الأثناء)
git push --force-with-leaseيقول git rebase «Current branch is up to date»
السبب: لا commits لإعادة تطبيقها - الفرع مؤسَّس على الهدف أصلًا
استخدم -f لفرض rebase رغم ذلك إن احتجت إلى hashes جديدة
git rebase -f mainقائمة مهام rebase التفاعلي فارغة
السبب: لا commits في النطاق المحدد
تحقّق من النطاق - استخدم HEAD~n أو حدد الـ upstream الصحيح
git log --oneline main..HEADنصائح وأخطاء شائعة
أفضل الممارسات
- ✓لا تنفّذ rebase أبدًا على commits دُفعت إلى مستودع مشترك - فهذا يجبر الجميع على إصلاح فروعهم يدويًا
- ✓استخدم دائمًا 'git rebase --abort' إن صارت التعارضات معقّدة جدًا - يمكنك تجربة نهج آخر
- ✓استخدم rebase التفاعلي (-i) لتنظيف الـ commits قبل الدفع أو إنشاء pull request
- ✓فكّر في استخدام '--autostash' للتعامل تلقائيًا مع التغييرات غير المحفوظة أثناء rebase
- ✓استخدم 'git commit --fixup=<hash>' مع '--autosquash' لسير عمل إصلاح أنظف
- ✓شغّل الاختبارات بعد rebase للتأكد أن الـ commits المُعاد تأسيسها ما زالت تعمل
- ✓استخدم '-x npm test' لتشغيل الاختبارات تلقائيًا بعد كل commit يُعاد تطبيقه
- ✓أبقِ عمليات rebase صغيرة - نفّذ rebase كثيرًا بدل ترك الفروع تتباعد كثيرًا
- ✓استخدم 'git reflog' للتعافي إن ساءت عملية rebase - لا تُحذف الـ commits الأصلية فورًا أبدًا
- ✓فضّل merge على rebase في الفروع العامة/المشتركة للحفاظ على التاريخ
- ✓استخدم '--force-with-lease' بدل '--force' عند دفع فروع أُعيد تأسيسها، من باب الأمان
- ✓عند الضم، اكتب رسالة commit جديدة وصفية تلخّص كل الـ commits المضمومة
أخطاء متكررة
- ✗إعادة تأسيس commits دُفعت أصلًا إلى مستودع مشترك. يعيد هذا كتابة التاريخ ويجبر الآخرين على إصلاح فروعهم يدويًا.
- ✗نسيان تجهيز التعارضات المحلولة قبل تشغيل --continue. شغّل دائمًا 'git add' على الملفات المحلولة أولًا.
- ✗استخدام --skip دون تأنٍّ، فيُسقط تغييرات الـ commit من الفرع المُعاد تأسيسه. لا تستخدمه إلا حين تريد عمدًا إزالة commit.
- ✗عدم فهم أن rebase ينشئ commits جديدة بـ hashes مختلفة. تصبح الـ commits الأصلية يتيمة.
- ✗تنفيذ rebase مع تغييرات غير محفوظة في مجلد العمل. استخدم --autostash أو خبّئها يدويًا أولًا.
الأسئلة الشائعة
يحافظ merge على التاريخ بإنشاء commit دمج، فيبقى تاريخ الفرعين سليمًا. أما rebase فيعيد كتابة التاريخ بإعادة تطبيق commits الخاصة بك فوق فرع آخر، فينشأ تاريخ خطي. استخدم merge للفروع المشتركة، وrebase للتنظيف المحلي.
يغيّر rebase hashes الـ commits. إن بنى آخرون عملهم على commits الأصلية، فسيتباعد تاريخهم عن تاريخك. وسيحتاجون إلى إصلاح فروعهم يدويًا، وهذا مرهق وعرضة للأخطاء.
استخدم 'git reflog' لإيجاد الـ commit الذي كان قبل rebase (ابحث عن الرأس القديم للفرع)، ثم 'git reset --hard HEAD@{n}' لاستعادته. افتراضيًا تبقى الـ commits الأصلية قابلة للوصول من الـ reflog لمدة 30 يومًا. وبعد rebase مباشرة يعمل أيضًا 'git reset --hard ORIG_HEAD'.
في rebase التفاعلي: 'pick' يُبقي الـ commit كما هو، و'squash' يضمّه إلى الـ commit السابق (مع إبقاء الرسالتين)، و'fixup' يضمّه لكنه يتخلّص من رسالته، و'reword' يُبقي الـ commit لكنه يتيح لك تعديل رسالته.
شغّل 'git rebase -i HEAD~n' حيث n عدد الـ commits. في المحرر أبقِ 'pick' على أول commit وغيّر البقية إلى 'squash' أو 'fixup'. احفظ وعدّل رسالة الـ commit المدموج.
يتيح --onto نقل الـ commits إلى أساس مختلف. مثلًا، «git rebase --onto main feature~3 feature» ينقل آخر 3 commits فقط من feature فوق main، متخطّيًا ما قبلها.
عدّل الملفات المتعارضة لحلّها، وشغّل 'git add' على الملفات المحلولة، ثم 'git rebase --continue'. وإن صار الأمر فوضويًا جدًا، فإن 'git rebase --abort' يعيدك إلى حالة ما قبل rebase.
أُزيل --preserve-merges في Git 2.34 (يفشل git الآن برسالة «--preserve-merges was replaced by --rebase-merges»). استخدم --rebase-merges (-r) بدلًا منه للحفاظ على commits الدمج وبنية الفروع أثناء rebase. يعيد إنشاء طوبولوجيا الدمج فوق الأساس الجديد.
نعم، استخدم 'git rebase -i' وعلّم الـ commits التي تريد إبقاءها بـ 'pick' والتي تريد إزالتها بـ 'drop'. أو استخدم --onto لتحديد نطاق الـ commits المراد نقله بدقة.
يُحسب hash الـ commit من محتواه ورسالته ومؤلفه وتاريخه والـ commit الأب. وبما أن rebase يغيّر الأب، تحصل كل الـ commits اللاحقة على hashes جديدة حتى لو كان محتواها متطابقًا.