Git
Chapters ▾ 2nd Edition

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

ریبیس‌کردن

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

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

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

Simple divergent history.
Figure 34. تاریخچهٔ دوشاخهٔ ساده

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

Merging to integrate diverged work history.
Figure 35. مرج کردن تاریخچه‌های دوشاخه شده

هرچند راه دیگری نیز وجود دارد: شما می‌توانید پچ تغییراتی که در 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

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

Rebasing the change introduced in `C4` onto `C3`.
Figure 36. ریبیس کردن تغییرات معرفی شده در C4 به روی C3

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

$ git checkout master
$ git merge experiment
Fast-forwarding the `master` branch.
Figure 37. fast-forward کردن برنچ master

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

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

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

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

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

با فرض اینکه تصمیم دارید که تغیرات سمت کلاینت را در خط اصلی پروژه ادغام کنید ولی می‌خواهید که تغییرات سمت سرور تا زمان تست شدن باقی بمانند. شما می‌توانید با استفاده از آپشن --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.
Figure 39. ریبیس کردن یک برنچ موضوعی از یک برنچ موضوعی دیگر

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

$ git checkout master
$ git merge client
Fast-forwarding your `master` branch to include the client branch changes.
Figure 40. fast-forward کردن برنچ 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.
Figure 41. ریبیس کردن برنچ سرورتان روی برنچ master

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

$ git checkout master
$ git merge server

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

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

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

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

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

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

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

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

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

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

Fetch more commits, and merge them into your work.
Figure 44. Fetch more commits, and merge them into your work

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

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

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

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

اگر در همین حین که تاریخچهٔ شما به این شکل است، یک 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.
Figure 47. به روی یک کار ریبیس و فورس پوش شده ریبیس می‌کنید

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

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

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