Git
Chapters ▾ 2nd Edition

3.6 شاخه‌سازی در گیت - ریبیس‌کردن

ریبیس‌کردن

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

ریبیس مقدماتی

اگر به یک مثال قدیمی‌تر از ادغام مقدماتی بازگردیم، می‌بینید که کار شما و کامیت‌های که کرده‌اید دوشاخه شده است.

Simple divergent history.
نمودار 35. تاریخچهٔ دوشاخهٔ ساده

همانطور که قبلاً بررسی کردیم، آسان‌ترین راه برای یکپارچه‌سازی برنچ‌ها دستور merge است. این دستور یک ادغام‌سازی سه طرفه بین آخرین اسنپ‌شات‌های دو برنچ ‌(C3 و C4) و آخرین والد مشترک آنها (C2) انجام می‌دهد، یک اسنپ‌شات جدید می‌سازد (و آنرا کامیت می‌کند).

Merging to integrate diverged work history.
نمودار 36. مرج کردن جهت یکپارچه‌سازی تاریخچهٔ کار دوشاخه

هرچند راه دیگری نیز وجود دارد: شما می‌توانید پچ تغییراتی که در 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. ریبیس کردن تغییرات معرفی شده در C4 به روی C3

در این نقطه می‌توانید به برنچ master بازگردید و یک مرج fast-forward کنید.

$ git checkout master
$ git merge experiment
Fast-forwarding the `master` branch.
نمودار 38. فست-فوروارد کردن برنچ master

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

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

ریبیس‌های جذاب‌تر

شما همچنین می‌توانید تغییرات را روی چیزی به غیر از برنچ هدف اعمال کنید. به طور مثال یک تاریخچه مانند تاریخچه‌ای با یک برنچ موضوعی که از برنچ موضوعی دیگر شروع می‌شود را در نظر بگیرید. یک برنچ موضوعی (server) ساختید تا چند مورد سمت سرور به پروژهٔ خود اضافه کنید و کامیتی گرفتید. سپس یک برنچ ساختید (client) تا تغییرات سمت کلاینت را اعمال کنید و چند کامیت هم آنجا گرفتید. در نهایت به برنچ سرور بازگشتید و چند کامیت دیگر گرفته‌اید.

A history with a topic branch off another topic branch.
نمودار 39. تاریخچه‌ای با یک برنچ موضوعی که از برنچ موضوعی دیگر شروع می‌شود

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

$ git rebase --onto master server client

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

Rebasing a topic branch off another topic branch.
نمودار 40. ریبیس کردن یک برنچ موضوعی از یک برنچ موضوعی دیگر

حال می‌توانید برنچ master خود را fast-forward کنید (به فست-فوروارد کردن برنچ master برای اضافه کردن تغییرات برنچ کلاینت نگاهی بیاندازید):

$ git checkout master
$ git merge client
Fast-forwarding your `master` branch to include the client branch changes.
نمودار 41. فست-فوروارد کردن برنچ master برای اضافه کردن تغییرات برنچ کلاینت

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

$ git rebase master server

همانطور که در ریبیس کردن برنچ سرورتان روی برنچ master نمایش داده شد، این دستور کارهای برنچ server را روی کارهای master شما سوار می‌کند.

Rebasing your server branch on top of your `master` branch.
نمودار 42. ریبیس کردن برنچ سرورتان روی برنچ master

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

$ git checkout master
$ git merge server

شما می‌توانید برنچ‌های client و server را حذف کنید چراکه تمام کارها تعبیه شده‌اند و شما دیگر به آنها احتیاجی ندارید، در نتیجه تمام تاریخچه شما مشابه تاریخچهٔ کامیت نهایی می‌شود:

$ git branch -d client
$ git branch -d server
Final commit history.
نمودار 43. تاریخچهٔ کامیت نهایی

خطرات ریبیس‌کردن

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

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

اگر این راهنما را به گوش بسپارید، با مشکلی مواجه نخواهید شد. اگر نسپارید، ملت از شما متنفر خواهند شد و شما توسط دوستان و خانوادهٔ خود ترد خواهید شد.

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

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

Clone a repository, and base some work on it.
نمودار 44. یک مخزن را کلون می‌کنید و کمی کار روی آن انجام می‌دهید

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

Fetch more commits, and merge them into your work.
نمودار 45. کامیت‌های بیشتری را فچ می‌کنید و آنها را با کار خود مرج می‌کنید

پس از این، شخصی که کار مرج‌شده را پوش کرده تصمیم می‌گیرد که باز گردد و بجای آن کار خود را ریبیس کند؛ او یک git push --force می‌کند تا تاریخچهٔ سرور را بازنویسی کند. سپس شما آن سرور را فچ می‌کنید و کامیت‌های جدید را دریافت می‌کنید:

Someone pushes rebased commits, abandoning commits you’ve based your work on.
نمودار 46. شخصی کامیت‌های ریبیس‌شده را پوش می‌کند، کامیت‌هایی که شما کار خود را روی آنها بنا گذاشته‌اید را پشت سر می‌گذارد

حالا هر دوی شما گیر افتاده‌اید. اگر شما git pull کنید، یک مرج کامیت خواهید ساخت که شامل هر دو خط از تاریخچه خواهد بود و مخزن شما به شکل زیر در خواهد آمد:

You merge in the same work again into a new merge commit.
نمودار 47. شما همان کارها را دوباره مرج می‌کنید و یک مرج کامیت جدید می‌سازید

اگر در همین حین که تاریخچهٔ شما به این شکل است، یک git log اجرا کنید خواهید دید که دو کامیت وجود دارد که دقیقاً یک نویسنده، تاریخ و پیغام دارند که می‌تواند بسیار گیج‌کننده باشد. بعلاوه اگر این تاریخچه را به سرور پوش کنید، شما تمام آن کامیت‌های ریبیس شده را دوباره به سرور مرکزی معرفی می‌کنید که به مقدار گیج‌کنندگی مسئله بیش از پیش می‌افزاید. خیلی راحت می‌توان فرض را بر این گذاشت که توسعه‌دهندگان دیگر نمی‌خواهند که C4 و C6 در تاریخچه باشند و همین باعث بوده که در وهله اول ریبیس انجام شود.

وقتی ریبیس می‌کنید ریبیس کنید

اگر شما خودتان را در چنین شرایطی می‌بینید، گیت کمی راه حل جادویی پیش پای شما گذاشته است که ممکن است به شما کمک کنند. اگر شخصی در تیمتان فورس پوشی کرده که پایهٔ کار شما را بازنویسی کرده است، چالش شما تشخیص این خواهد بود که چه کاری مال شماست و چه چیزی را آنها بازنویسی کرده‌اند.

در اینجا معلوم می‌شود که علاوه بر چک‌سام SHA-1 کامیت، گیت همچنین چک‌سامی از تغییراتی که کامیت به تاریخچه افزوده محاسبه می‌کند. این چک‌سام را «شناسهٔ پچ» (Patch-ID) می‌خوانیم.

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

به طور مثال، در شرایط قبلی، اگر هنگامی که در شخصی کامیت‌های ریبیس‌شده را پوش می‌کند، کامیت‌هایی که شما کار خود را روی آنها بنا گذاشته‌اید را پشت سر می‌گذارد هستید به جای مرج‌کردن، git rebase teamone/master را اجرا کنید، گیت:

  • خواهد فهمید که چه کاری مختص به برنچ ماست (C2، C3، C4، C6، C7)

  • خواهد فهمید کدام کامیت‌ها مرج نشده‌اند (C2، C3، C4)

  • خواهد فهمید کدام کامیت‌ها در برنچ هدف بازنویسی نشده‌اند (فقط C2 و C3، از آنجایی که C4 همان پچ C4' است)

  • آن کامیت‌ها را به روی teamone/master اعمال خواهد کرد

Rebase on top of force-pushed rebase work.
نمودار 48. به روی یک کار ریبیس و فورس پوش شده ریبیس می‌کنید

این فقط در شرایطی کار خواهد کرد که 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 و filter-branch استفاده می‌کنند تا داستان را به نحوی بسرایند که برای خوانندگان احتمالی پخته باشد.

حال برای جواب دادن به اینکه مرج بهتر است یا ریبیس: خوشبختانه ملاحظه می‌کنید که جواب دادن خیلی ساده نیست. گیت ابزار قدرتمندی است، به شما اجازهٔ انجام خیلی کار‌ها را به خصوص روی تاریخچه‌تان می‌دهد، اما هر تیم و هر پروژه متفاوت است. حال که می‌دانید چگونه این‌ها کار می‌کنند، به شما بستگی دارد که تصمیم بگیرید که کدام برای شرایط بخصوص شما بهترین است.

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