Chapters ▾ 2nd Edition

3.6 انشعاب‌گیری در گیت (Git Branching) - بازپایه‌گذاری (Rebasing)

بازپایه‌گذاری (Rebasing)

در گیت، دو روش اصلی برای ادغام تغییرات از یک شاخه به شاخه دیگر وجود دارد: دستور merge و دستور rebase. در این بخش خواهید آموخت که بازپایه‌گذاری چیست، چگونه انجام می‌شود، چرا این ابزار بسیار کاربردی است و در چه مواردی بهتر است از آن استفاده نکنید.

بازپایه‌گذاری پایه (The Basic Rebase)

اگر به مثالی که در بخش ادغام پایه‌ای (Basic Merging) آوردیم رجوع کنید، می‌بینید که کارتان را از هم جدا کرده‌اید و تغییراتی را روی دو شاخه متفاوت ایجاد کرده‌اید.

Simple divergent history
نمودار 35. Simple divergent history

ساده‌ترین روش برای ادغام شاخه‌ها، همان‌طور که پیش‌تر توضیح دادیم، دستور merge است. این دستور یک ادغام سه‌طرفه بین دو آخرین تصویر شاخه‌ها (C3 و C4) و آخرین جد مشترک آن‌ها (C2) انجام می‌دهد و یک تصویر (commit) جدید ایجاد می‌کند.

Merging to integrate diverged work history
نمودار 36. Merging to integrate diverged work history

اما راه دیگری نیز وجود دارد: می‌توانید تغییراتی را که در C4 ایجاد شده‌اند برداشته و مجدداً روی C3 اعمال کنید. در گیت به این کار «بازپایه‌گذاری» گفته می‌شود. با دستور rebase می‌توانید تمام تغییراتی که در یک شاخه ثبت شده‌اند را گرفته و دوباره روی شاخه‌ای دیگر اجرا کنید.

برای این مثال، ابتدا شاخه experiment را چک‌اوت می‌کنید و سپس آن را روی شاخه master بازپایه‌گذاری می‌کنید، به این صورت:

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

این عملیات با رفتن به جد مشترک دو شاخه (شاخه فعلی و شاخه‌ای که می‌خواهید بازپایه‌گذاری کنید)، گرفتن تفاوت‌هایی که هر کامیت روی شاخه فعلی ایجاد کرده، ذخیره این تفاوت‌ها در فایل‌های موقتی، ریست کردن شاخه فعلی به همان کامیتی که شاخه مقصد دارد، و در نهایت اعمال هر تغییر به ترتیب انجام می‌شود.

Rebasing the change introduced in `C4` onto `C3`
نمودار 37. Rebasing the change introduced in C4 onto C3

در این مرحله، می‌توانید دوباره به شاخه master برگردید و یک ادغام fast-forward انجام دهید.

$ git checkout master
$ git merge experiment
Fast-forwarding the `master` branch
نمودار 38. Fast-forwarding the master branch

اکنون تصویری که شاخه C4' به آن اشاره می‌کند دقیقاً همان است که در the merge example شاخه C5 به آن اشاره داشت. در نهایت محصول ادغام تفاوتی ندارد، اما بازپایه‌گذاری تاریخچه‌ای تمیزتر و منظم‌تر ایجاد می‌کند. اگر لاگ شاخه‌ای که بازپایه‌گذاری شده را بررسی کنید، تاریخچه به صورت خطی دیده می‌شود؛ یعنی انگار همه کارها به صورت سری انجام شده‌اند، حتی اگر در ابتدا به طور موازی بودند.

اغلب این کار را برای اطمینان از اینکه کامیت‌های شما به‌صورت تمیز روی شاخه ریموت اعمال می‌شوند انجام می‌دهید — مثلاً در پروژه‌ای که می‌خواهید در آن مشارکت کنید اما مسئولیت نگهداری آن را ندارید. در این حالت، کارتان را در یک شاخه انجام می‌دهید و وقتی آماده ارسال تغییرات به پروژه اصلی شدید، کارتان را روی origin/master بازپایه‌گذاری می‌کنید. به این ترتیب، مسئول نگهداری پروژه نیازی به انجام ادغام دستی ندارد — فقط یک fast-forward یا اعمال تمیز تغییرات خواهد بود.

 توجه داشته باشید که اسنپ‌شاتی که توسط آخرین کامیتی که در نهایت به آن می‌رسید اشاره می‌شود، چه آخرین کامیت‌های بازبیس شده در یک بازبیس باشند یا کامیت نهایی ادغام بعد از یک مرج، همان اسنپ‌شات است — تنها تاریخچه است که متفاوت است.
بازبیس کردن تغییرات را به ترتیبی که معرفی شده‌اند، از یک شاخه کاری روی شاخه دیگری بازپخش می‌کند، در حالی که مرج گرفتن نقاط انتهایی شاخه‌ها را گرفته و آن‌ها را با هم ادغام می‌کند.

بازپایه‌گذاری‌های جالب‌تر (More Interesting Rebases)

شما همچنین می‌توانید بازبیس خود را روی چیزی غیر از شاخه هدف بازبیس اجرا کنید. برای مثال، یک تاریخچه مثل A history with a topic branch off another topic branch را در نظر بگیرید. شما یک شاخه موضوعی به نام server ایجاد کردید تا عملکردهای سمت سرور را به پروژه‌تان اضافه کنید و یک کامیت انجام دادید. سپس، از آن شاخه انشعاب گرفتید تا تغییرات سمت کلاینت (client) را ایجاد کنید و چندین بار کامیت کردید. در نهایت، به شاخه server برگشتید و چند کامیت دیگر انجام دادید.

A history with a topic branch off another topic branch
نمودار 39. A history with a topic branch off another topic branch

فرض کنید تصمیم گرفته‌اید تغییرات سمت کلاینت را برای انتشار به شاخه اصلی خود مرج کنید، اما می‌خواهید تغییرات سمت سرور را تا زمان آزمایش بیشتر نگه دارید. شما می‌توانید تغییرات روی شاخه client که روی server نیستند (C8 و C9) را با گزینه --onto در دستور git rebase روی شاخه master بازپخش کنید:

$ git rebase --onto master server client

این اساساً می‌گوید: «شاخه client را بگیر، پچ‌هایی را که از زمانی که از شاخه server جدا شده‌اند پیدا کن، و این پچ‌ها را در شاخه client طوری بازپخش کن که گویی مستقیماً روی شاخه master پایه‌گذاری شده‌اند.» این کمی پیچیده است، اما نتیجه بسیار جالب است.

Rebasing a topic branch off another topic branch
نمودار 40. Rebasing a topic branch off another topic branch

حالا می‌توانید شاخه master را به‌صورت fast-forward جلو ببرید (نگاه کنید به Fast-forwarding your master branch to include the client branch changes):

$ git checkout master
$ git merge client
Fast-forwarding your `master` branch to include the `client` branch changes
نمودار 41. Fast-forwarding your master branch to include the client branch changes

فرض کنید تصمیم می‌گیرید شاخه server را هم وارد کنید. می‌توانید شاخه server را بدون نیاز به چک‌اوت کردن قبلی، روی شاخه master بازبیس کنید با اجرای دستور git rebase <basebranch> <topicbranch> — که شاخه موضوع (در اینجا server) را برای شما چک‌اوت می‌کند و روی شاخه پایه (master) بازپخش می‌کند:

$ git rebase master server

این کار، تغییرات شاخه server را روی تغییرات شاخه master بازپخش می‌کند، همانطور که در Rebasing your server branch on top of your master branch نشان داده شده است.

Rebasing your `server` branch on top of your `master` branch
نمودار 42. Rebasing your server branch on top of your master branch

سپس می‌توانید شاخه پایه (master) را fast-forward کنید:

$ git checkout master
$ git merge server

می‌توانید شاخه‌های client و server را حذف کنید چون تمام کارها ادغام شده و دیگر به آن‌ها نیازی ندارید، و تاریخچه شما برای کل این فرایند مشابه Final commit history خواهد بود.

$ git branch -d client
$ git branch -d server
Final commit history
نمودار 43. Final commit history

خطرات بازپایه‌گذاری (The Perils of Rebasing)

آه، اما لذت بازبیس کردن بدون معایب نیست، که می‌توان آن را در یک جمله خلاصه کرد:

کامیت‌هایی را که خارج از مخزن شما وجود دارند و ممکن است دیگران روی آن‌ها کار کرده باشند، بازبیس نکنید.

اگر این دستورالعمل را رعایت کنید، مشکلی نخواهید داشت. اگر رعایت نکنید، دیگران از شما متنفر خواهند شد و دوستان و خانواده شما را تحقیر خواهند کرد.

هنگامی که بازبیس می‌کنید، شما کامیت‌های موجود را رها کرده و کامیت‌های جدیدی می‌سازید که شبیه آن‌ها اما متفاوت هستند. اگر کامیت‌هایی را جایی پوش کنید و دیگران آن‌ها را کلون و روی آن‌ها کار کنند، سپس شما با git rebase آن کامیت‌ها را بازنویسی کرده و دوباره پوش کنید، همکاران شما مجبور خواهند بود کارهایشان را دوباره ادغام کنند و وقتی بخواهید تغییراتشان را به کار خودتان برگردانید، اوضاع پیچیده و نامرتب خواهد شد.

بیایید مثالی از مشکلات بازبیس کردن تغییراتی که عمومی کرده‌اید را بررسی کنیم. فرض کنید از یک سرور مرکزی کلون کرده‌اید و سپس روی آن کارهایی انجام داده‌اید. تاریخچه کامیت‌های شما به این شکل است:

Clone a repository, and base some work on it
نمودار 44. Clone a repository, and base some work on it

حالا، شخص دیگری کار بیشتری انجام می‌دهد که شامل یک ادغام (merge) است و آن کار را به سرور مرکزی ارسال می‌کند. شما آن را دریافت می‌کنید و شاخه‌ی جدید راه دور را با کار خود ادغام می‌کنید، به طوری که تاریخچه‌ی شما چیزی شبیه به این می‌شود:

Fetch more commits, and merge them into your work
نمودار 45. Fetch more commits, and merge them into your work

بعداً، همان شخصی که کار ادغام‌شده را ارسال کرده است تصمیم می‌گیرد به عقب برگردد و کار خود را مجدداً بازبیس (rebase) کند؛ او با دستور git push --force تاریخچه‌ی سرور را بازنویسی می‌کند. سپس شما از آن سرور دریافت می‌کنید و کمیت‌های جدید را می‌آورید.

Someone pushes rebased commits, abandoning commits you’ve based your work on
نمودار 46. Someone pushes rebased commits, abandoning commits you’ve based your work on

حالا هر دوی شما در وضعیتی دشوار قرار دارید. اگر دستور git pull اجرا کنید، یک کمیت ادغام (merge commit) ایجاد می‌شود که هر دو خط تاریخچه را شامل می‌شود و مخزن شما به این شکل در می‌آید:

You merge in the same work again into a new merge commit
نمودار 47. You merge in the same work again into a new merge commit

اگر وقتی تاریخچه‌تان به این شکل است دستور git log بزنید، دو کمیت با همان نویسنده، تاریخ و پیام خواهید دید که باعث سردرگمی می‌شود. علاوه بر این، اگر این تاریخچه را دوباره به سرور بفرستید، تمام کمیت‌های بازبیس‌شده را دوباره به سرور مرکزی وارد می‌کنید که ممکن است باعث سردرگمی بیشتر شود. به طور منطقی می‌توان فرض کرد توسعه‌دهنده‌ی دیگر نمی‌خواهد کمیت‌های C4 و C6 در تاریخچه باشند؛ به همین دلیل است که ابتدا کارش را بازبیس کرده بود.

بازپایه‌گذاری هنگام بازپایه‌گذاری (Rebase When You Rebase)

اگر در چنین وضعیتی قرار گرفتید، گیت جادوهای دیگری هم دارد که شاید به شما کمک کند. اگر کسی در تیم شما تغییراتی را با force push ارسال کند که کاری را که شما بر اساس آن کار کرده‌اید بازنویسی کند، چالش شما این است که بفهمید چه چیزی مال شماست و چه چیزی توسط آنها بازنویسی شده است.

گیت علاوه بر محاسبه‌ی شناسه SHA-1 کمیت، یک شناسه‌ی دیگری هم بر اساس تغییرات وارد شده در کمیت محاسبه می‌کند که به آن “patch-id” می‌گویند.

اگر شما کاری را که بازنویسی شده دریافت کنید و آن را روی کمیت‌های جدید هم‌تیمی‌تان بازبیس کنید، گیت اغلب می‌تواند تشخیص دهد که چه تغییراتی مختص شماست و آنها را دوباره روی شاخه جدید اعمال کند.

برای مثال، در سناریوی قبلی، اگر به جای ادغام در مرحله‌ی Someone pushes rebased commits, abandoning commits you’ve based your work on دستور git rebase teamone/master را اجرا کنیم، گیت این کارها را انجام می‌دهد: * تشخیص می‌دهد چه کارهایی منحصر به شاخه‌ی ماست (C2، C3، C4، C6، C7) * تشخیص می‌دهد کدام کمیت‌ها ادغام نیستند (C2، C3، C4) * تشخیص می‌دهد کدام‌ها هنوز در شاخه‌ی هدف بازنویسی نشده‌اند (فقط C2 و C3، چون C4 همان تغییر C4' است) * آن کمیت‌ها را روی رأس teamone/master اعمال می‌کند

پس به جای نتیجه‌ای که در You merge in the same work again into a new merge commit دیدیم، چیزی شبیه به Rebase on top of force-pushed rebase work خواهیم داشت.

Rebase on top of force-pushed rebase work
نمودار 48. Rebase on top of force-pushed rebase work

این فقط زمانی جواب می‌دهد که C4 و C4' که هم‌تیمی شما ساخته تقریباً همان اصلاحات یکسان باشند. در غیر این صورت، بازبیس نمی‌تواند تشخیص دهد که این یک نسخه‌ی تکراری است و یک تغییر شبیه C4 دیگر اضافه خواهد کرد (که احتمالاً به درستی اعمال نمی‌شود، چون تغییرات قبلاً تا حدی وجود دارند).

شما همچنین می‌توانید این کار را با اجرای دستور git pull --rebase به جای git pull معمولی ساده‌تر کنید. یا می‌توانید به صورت دستی ابتدا git fetch بگیرید و سپس دستور git rebase teamone/master را اجرا کنید.

اگر می‌خواهید git pull به طور پیش‌فرض با گزینه --rebase اجرا شود، می‌توانید مقدار pull.rebase را با دستور git config --global pull.rebase true تنظیم کنید.

اگر کمیت‌هایی را بازبیس می‌کنید که هرگز از کامپیوتر خودتان خارج نشده‌اند، هیچ مشکلی نخواهید داشت. اگر کمیت‌هایی را بازبیس می‌کنید که قبلاً ارسال شده‌اند، اما هیچ کس دیگری بر اساس آن‌ها کار نکرده است، باز هم مشکلی پیش نمی‌آید. اما اگر کمیت‌هایی را بازبیس کنید که قبلاً به صورت عمومی ارسال شده‌اند و دیگران بر اساس آن‌ها کار کرده‌اند، ممکن است با مشکلات ناامیدکننده‌ای روبرو شوید و همکارانتان از شما ناراضی شوند.

اگر شما یا هم‌تیمی‌تان در جایی مجبور شدید این کار را انجام دهید، مطمئن شوید همه می‌دانند که باید از دستور git pull --rebase استفاده کنند تا دردسرهای بعدی کمی کمتر شود.

بازپایه‌گذاری در مقابل ادغام (Rebase vs. Merge)

حالا که بازبیس و ادغام را در عمل دیده‌اید، شاید بپرسید کدام بهتر است؟ قبل از پاسخ دادن، کمی عقب‌تر می‌رویم و درباره‌ی مفهوم تاریخچه صحبت می‌کنیم.

یک دیدگاه این است که تاریخچه‌ی کمیت‌های مخزن شما، ثبت آنچه واقعاً اتفاق افتاده است. این یک سند تاریخی است که ارزش خاص خود را دارد و نباید دستکاری شود. از این زاویه، تغییر تاریخچه‌ی کمیت‌ها تقریباً یک عمل ناپسند است؛ انگار درباره‌ی آنچه واقعاً رخ داده دروغ می‌گویید. پس اگر یک سری کمیت ادغام نامرتب وجود داشته باشد، این همانا همان چیزی است که اتفاق افتاده و مخزن باید این را برای آیندگان حفظ کند.

دیدگاه مخالف این است که تاریخچه کامیت‌ها، داستان چگونگی ساخته شدن پروژه شما است. شما اولین پیش‌نویس یک کتاب را منتشر نمی‌کنید، پس چرا کار درهم‌وبرهم خود را نشان دهید؟ وقتی روی پروژه‌ای کار می‌کنید، ممکن است به رکورد تمام اشتباهات و مسیرهای بن‌بست خود نیاز داشته باشید، اما وقتی زمان نمایش کار به جهان می‌رسد، شاید بخواهید داستان منسجم‌تری درباره چگونگی رسیدن از نقطه A به B تعریف کنید. افراد این دسته از ابزارهایی مانند rebase و filter-branch برای بازنویسی کامیت‌هایشان قبل از ادغام در شاخه اصلی استفاده می‌کنند. آن‌ها از این ابزارها برای بیان داستان به شکلی که برای خوانندگان آینده بهتر باشد بهره می‌برند.

حالا درباره این سؤال که ادغام (merge) بهتر است یا بازپایه‌گذاری (rebase): امیدوارم متوجه شده باشید که پاسخ ساده‌ای ندارد. گیت ابزاری قدرتمند است و به شما امکان انجام کارهای زیادی روی تاریخچه می‌دهد، اما هر تیم و هر پروژه‌ای متفاوت است. حالا که می‌دانید هر دو روش چگونه کار می‌کنند، انتخاب بهترین گزینه برای وضعیت خاص خودتان بر عهده شماست.

می‌توانید بهترین‌های هر دو را داشته باشید: تغییرات محلی را قبل از ارسال با rebase مرتب کنید تا کارتان تمیز شود، اما هرگز چیزی را که جایی ارسال کرده‌اید دوباره بازپایه‌گذاری نکنید.

scroll-to-top