GreatBahram
GreatBahram
خواندن ۸ دقیقه·۵ سال پیش

قانون طلایی گیت ری‌بیس

سلام!

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

مقدمه

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

در اعماق git-merge

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

به طور مثال، وقتی داریم یک ویژگی جدید به سیستم اضافه می‌کنیم آسیبی به کدی که داره درست کار می‌کنه نزنیم. و وقتی هم کارمون به نتیجه رسید می‌تونیم برنچ جدید با برنچ مد نظر به اصطلاح «ادغام» یا مرج کنیم،‌ این کار هم با کمک دستور git-merge انجام میشه.

میشه گفت ما دو روش مرج توی گیت داریم:

  • Fast-forward
  • Three way (No-Fast-forward)

قدم اول توی هر دو روش این هست که دستور git-merge دنبال یک گره/جد مشترک (common ancestor) بین این دو برنچ می‌گرده. این جد مشترک هست که مشخص می‌کنه که آیا مرج ما از نوع Fast-forward قابل انجام هست یا روش دوم.

Image from https://www.atlassian.com
Image from https://www.atlassian.com


مرج از نوع Fast-Forward

این مدل فقط در حالتی توسط گیت قابل اجرا هست که ما یک مسیر خطی از نقطه مشترک رفته باشیم: یعنی مثل پایین درحالی که برنچِ Some Feature از Master گرفته شده و تغییراتی هم به اون اضافه شده، گره مشترک (برنچ Master) نسبت به زمانی که ما ازش منشعب شدیم تغییری نکرده (کامیتی بهش اضافه نشده). گیت این مرج‌ها رو خیلی دوست داره و نیازی به تعامل با هم نداره.

توی این روش کاری که گیت می‌کنه این هست، که اشاره‌گر برنچ Master رو به نوک برنچ Some feature میزنه؛ به طوری که انگار همه‌ی این تغییرات واقعاً توی همین برنچ ما صورت گرفته.

Image from https://www.atlassian.com - git merge fast-forward
Image from https://www.atlassian.com - git merge fast-forward
  • اما چی‌میشد اگر علاوه بر برنچ Some Feature، برنچ Master هم تغییر کرده بود؟!

توی شکل پایین، برنچِ Some Feature از Master گرفته شده و دو تا کامیت‌ هم داشته، در حالی که خود master هم نسبت به زمانی که ما ازش منشعب شدیم تغییر کرده. توی این حالت دیگر Fast-forward دوای درد ما نمی‌تونه باشه.

گیت در اینجور مواقع از روش دوم استفاده می‌کنه، که بهش three-way merge گفته میشه. اسمش‌ از اینجا میاد که گیت باید سه تا اشاره‌گرِ رو بروزرسانی کنه.

Image from https://www.atlassian.com
Image from https://www.atlassian.com

خروجی کار به صورت بصری این‌طوری میشه:

Image from https://www.atlassian.com - git-merge three-way merge
Image from https://www.atlassian.com - git-merge three-way merge

در این حالت ما همیشه یک کامیت آبجکت جدید خواهیم داشت و تغییرات رو دیگه توی Master به طور مستقیم وارد نکردیم. بلکه یک اشاره‌گر به تغییرات صورت گرفته زدیم.

توی بعضی پروژه‌ها ممکن هست ببینید که سیاست مرج‌، به صورت پیش فرض، مدل بالا بشه. دلایل‌ این کار هم معمولاً:

  • برای برنچ‌هایی که اونجا تغییرات زیادی بوده،‌ maintainer نظرش این هست که تاریخچه به صورت مجزا حفظ بشه و به همین برنچ منتقل نشه.
  • این کار می‌تونه باعث سهولت هم بشه، چطور؟ مثلا اگر بخوایم برگردیم عقب به راحتی برگردیم میتونیم فقط از روی کامیت آبجکت جدید برگشت داشته باشیم، نه کل تغییرات و کامیت‌ها.
توصیه به جوانان: میشه یه قاعده سرانگشتی هم داشته باشیم برای خودمون که اگر تغییرات خیلی ساده‌اس همون fast-forward گزینه‌ای خوبی هست ولی اگر تغییرات زیادی صورت گرفته و خود کامیت‌ها و تاریخچه برای ما مهم‌ هست از مدل no-fast-forward بریم بهتر هست.

کامند‌لاین

  • به صورت پیش‌فرض خود گیت سعی در مرج با fast-forward داره و نیاز نیست آپشن‌ خاصی بهش داده بشه.
  • اما با کمک آپشن پایین به گیت می‌گیم حتماً یک کامیت آبجکت جدید بساز و کلا از روش fast-forward استفاده نکن.
Don't use fast-forward git-merge, please!
Don't use fast-forward git-merge, please!

آپشن Squash

یک آپشن دیگه که توی دستور merge ممکن هست به کارمون بیاد، آپشن پایین هست که باعث میشه همه‌ی تغییرات در قالب یک کامیت اضافه بشه. یکی از کاربرداش هم می‌تونه این باشه که اکثر کامیت‌ها حول یک ویژگی بوده اما به هر دلیلی مجبور شدیم وسط کار کامیت‌های متفاوتی داشته باشم. اما قصد‌مون ادغام همه‌ی کامیت در قالب یک کامیت هست.

Summon all commits into one commit
Summon all commits into one commit

معرفی revert

مهم‌ترین ویژگی که باعث میشه ما از ورژن کنترلر استفاده کنیم، قابلیت rollback کردن هست. دستور revert مشابه reset است اما با این تفاوت که revert به عنوان ورودی یک کامیت رو می‌گیره به بجای اینکه به اون وضعيت برگرده، میاد عکس تمام کارهایی که توی اون کامیت انجام شده، رو انجام میده.

کاربردش؟ فرض کنید توی یک کامیت یک dependency رو حذف کردیم و می‌خوایم دوباره اون رو داشته باشیم به جای دستی اضافه کردن‌اش، بهتره با revert این کار انجام بشه، چون در قالب یک کامیت به تاریخچه اضافه میشه و دیگران هم می تونن این رو ببینن. یا مثلاً یک کامیت باعث به وجود اومدن باگ شده، و می‌خواید به وضعیت قبلی برگردید؟ جواب کمک گرفتن از git revert هست.

What does git-revert do?
What does git-revert do?

نحوه استفاده

How to use git-revert command
How to use git-revert command

توی کامیت‌های بالا، آخرین کامیت رو اگر دقت کنید، Revert بعلاوه پیام کامیت قبلی هست.

معرفی cherry-pick

اول از همه بد نیست بدونیم cherry-pick به معنی گلچین (سوا) کردن هست و این امکان رو به ما میده که بتونیم کامیت‌های خاصی رو از یک برنچ به برنچ دیگه ببریم. و تفاوتش با git-reset اینه که دستور cherry-pick دقیقا عمل کپی رو انجام میده و باعث حذف چیزی نمیشه؛ در حالی که git-reset کاره‌اش جابجایی اشاره‌گر کامیت‌هاست.

کجاها کاربرد داره؟ فرض کنید یکی از همکاران داره توی یک برنچ یک ویژگی جدید به پروژه اضافه می‌کنه و در این حال یک باگ اساسی از سیستم رو هم طی یک یا چند کامیت حل کرده و ما فقط کد بخش حل کردن مشکل رو می‌خوایم. در این حالت اگر merge کنیم همه تغییرات رو اضافه میشه، اما با cherry-pick فقط کامیت‌ مد نظر ما رو به برنچ فعلی اضافه میشه.

توی شکل پایین، ما دو تا برنچ داریم (master, feature-branch) و بر حسب نیاز،‌لازم هست که مثلا کامیت e رو به برنچ master اضافه کنیم:

Commits in different branches
Commits in different branches

روال کار اینجوری هست که اول به برنچ مورد نظر (master) می‌ریم و بعدش هش کامیت e رو به cherry-pick می‌دیم:

How to utilize git-cherry-pick
How to utilize git-cherry-pick

معرفی git-rebase

خب، می‌رسیم به شاید یکی از سخت‌ترین یا ناشناخته‌ترین بخش‌های گیت. تا الان یاد گرفتیم اگر بخواهیم تغییرات رو از یک برنچ به برنچ دیگه ببریم تنها راه‌مون git-merge بود. اما یک راه دیگه هم وجود داره که اون git-rebase هست. اگر خاطر‌تون باشه، توی بخش git-merge گفتیم نوع اول مرج fast-forward بود. و این فقط در صورتی قابل انجام بود که گره/جد مشترک نسبت به موقعی که ما برنچ‌مون رو ساخته بودیم تغییر نکرده باشه. در غیر این صورت مرجِ three-way مورد استفاده قرار می‌گرفت.

خبر خوب اینه که git-rebase این امکان رو به ما میده که تغییرات رو جوری توی برنچ مورد نظر‌ اعمال کنیم که انگار هیچ تغییری توی مبدا اتفاق نیفتاده و به صورت خطی مرج بشن و و دیگه نیازی به ساخت یک کامیت آبجکت جداگانه نباشه. در حقیقت همون‌طور که از اسمش پیداست داره base برنچ رو (یا بخونید گره/جد مشترک) رو به وضعیت جدید تغییر میده.

  • هدف اینکار بازم تمیز نگه داشتن تاریخچه کامیت‌هاست.

مثال پایین از مستندات گیت هست: فرض کنید مطابق شکل پایین یک برنچ master داشتیم و از اون یک برنچ دیگه به اسم experimental ساختیم و توی اون کامیت هم داشتیم. در عین حال، خود برنچِ master هم در این بازه تغییر کرده.

Simple divergent history
Simple divergent history

در نتیجه اگر بخواهید کامیت کنید، به صورت fast-forward دیگه نمیشه. و خروجی اون یک مرج سه طرفه میشه:

Merging to integrate diverged work history
Merging to integrate diverged work history

ولی اگر هدف ما خطی نگه داشتن تاریخچه کامیت‌ها باشه می‌تونیم با کمک git-rebase این کارو انجام بدیم. این دستور میاد برنچ experimental رو بر میداره و نقطه/جد مشترک‌اش رو به master جدید تغییر میده که شامل کامیت C3 هم هست. خروجی git-rebase این شکلی میشه:

Rebasing the change introduced in C4 onto C3
Rebasing the change introduced in C4 onto C3

حالا اگر دستور git-merge رو بزنیم، گیت به راحتی یه مرجِ از نوع fast-forward انجام میده. ممکن بعضی مواقع conflict هم رخ بده. توی انجور مواقع conflict رو برطرف می‌کنیم و بعدش به rebase با این دستور می‌گید به کارش ادامه بده:

git rebase --continue

یادتون باشه هرجای rebase می‌تونید از کارتون صرف نظر کنید، با دستور پایین کار شروع شده رو کنسل کنید:

git rebase --abort

اما دستور اصلی که base برنچ experimental رو به master تغییر داد،‌ این هست:

git rebase master experimental

git rebase <basebranch> <topicbranch>

میگن یه قانون طلایی توی git-rebase هست که اگر اون رو رعایت کنید، به رستگاری نزدیک‌تر می‌شید. اونم اینکه هیچ‌وقت روی برنچی که بین شما و همکارات shared هست و اونا دارن باهاش کار می‌کنن rebase نکنید. اینکار اگر آشنا نباشید باعث میشه که کل ایده تاریخچه منظم رو بهم بزنه.

همین دستور git-rebase یه آپشن خیلی باحال هم به اسم onto داره که توی شکل زیر به کار میاد.

A history with a topic branch off another topic branch
A history with a topic branch off another topic branch

مطابق تصویر بالا، مثلاً می‌خوایم برنچ client رو بدون کامیت‌های مشترک‌اش با server ببریم rebaseاش کنیم با برنچ master.

در نهایت git-rebase یک آپشن خیلی باحال دیگه هم داره به اسم interactive که با اون میشه‌ تاریخچه کامیت‌ها رو بازنویسی کرد، مثلاً کارهایی مثل Squash یا تغییر پیام کامیت‌ چند کامیت قبل رو هم میشه با این مدل تغییر داد.

جمع‌بندی

چند وقت پیش من یک ارائه راجعبه گیت داشتم، و در حین ارائه حس کردم مباحثی هست که به خوبی در رابطه با گیت درک نشده، بهمین دلیل توی این دو قسمت (قسمت اول) سعی کردم تا جایی که شدنی هست این مشکلات رو پوشش بدم.

در پایان، امیداوارم که از این مطالب چیزی یاد گرفته باشید!

از اینجا کجا بریم

  • اگر بیشتر می‌خواید راجعبه مرج بدونید

https://www.atlassian.com/git/tutorials/using-branches/git-merge

  • در رابطه با git-rebase

https://git-scm.com/book/en/v2/Git-Branching-Rebasing

https://www.daolf.com/posts/git-series-part-2/

  • در رابطه با git-rebase interactive

https://thoughtbot.com/blog/git-interactive-rebase-squash-amend-rewriting-history

gitbranchingmergerebase
Pythonista, Free Software Enthusiast. GNU/Linux Master. Network Security Researcher. Son. Brother.
شاید از این پست‌ها خوشتان بیاید