سلام!
توی قسمت قبلی، سعی کردیم یه دید کلی نسبت به کارهای که گیت انجام میده بدست بیاریم. راجعبه آبجکتهای اصلی گیت صحبت کردیم. اما در این قسمت میخواهیم که یکم راجعبه مباحث نسبتاً پیچیده گیت صحبت کنیم.
یکی از مهمترین چالشها در گیت، مرج کردن برنچهای مختلف هست که دو مدل اصلی هم داره. و یک سری مفاهیم هم شاید بهش توجه نشده باشه مثل git-revert که توی موقعیت خودش میتونه خیلی کمککننده باشه. و در نهایت سعی کردیم یه سری به git-rebase بزنیم هر چند یکم به صورت متنی راجعبهاش توضیح دادن سخت هست.
یکی از ویژگیهای خیلی خوب گیت، نحوه ساخت و مدیریت برنچها در اون هست. برنچها رو توی ورژن کنترلر میسازیم تا از آسیب به بخشهای دیگه جلوگیری کنیم.
به طور مثال، وقتی داریم یک ویژگی جدید به سیستم اضافه میکنیم آسیبی به کدی که داره درست کار میکنه نزنیم. و وقتی هم کارمون به نتیجه رسید میتونیم برنچ جدید با برنچ مد نظر به اصطلاح «ادغام» یا مرج کنیم، این کار هم با کمک دستور git-merge انجام میشه.
میشه گفت ما دو روش مرج توی گیت داریم:
قدم اول توی هر دو روش این هست که دستور git-merge دنبال یک گره/جد مشترک (common ancestor) بین این دو برنچ میگرده. این جد مشترک هست که مشخص میکنه که آیا مرج ما از نوع Fast-forward قابل انجام هست یا روش دوم.
این مدل فقط در حالتی توسط گیت قابل اجرا هست که ما یک مسیر خطی از نقطه مشترک رفته باشیم: یعنی مثل پایین درحالی که برنچِ Some Feature از Master گرفته شده و تغییراتی هم به اون اضافه شده، گره مشترک (برنچ Master) نسبت به زمانی که ما ازش منشعب شدیم تغییری نکرده (کامیتی بهش اضافه نشده). گیت این مرجها رو خیلی دوست داره و نیازی به تعامل با هم نداره.
توی این روش کاری که گیت میکنه این هست، که اشارهگر برنچ Master رو به نوک برنچ Some feature میزنه؛ به طوری که انگار همهی این تغییرات واقعاً توی همین برنچ ما صورت گرفته.
توی شکل پایین، برنچِ Some Feature از Master گرفته شده و دو تا کامیت هم داشته، در حالی که خود master هم نسبت به زمانی که ما ازش منشعب شدیم تغییر کرده. توی این حالت دیگر Fast-forward دوای درد ما نمیتونه باشه.
گیت در اینجور مواقع از روش دوم استفاده میکنه، که بهش three-way merge گفته میشه. اسمش از اینجا میاد که گیت باید سه تا اشارهگرِ رو بروزرسانی کنه.
خروجی کار به صورت بصری اینطوری میشه:
در این حالت ما همیشه یک کامیت آبجکت جدید خواهیم داشت و تغییرات رو دیگه توی Master به طور مستقیم وارد نکردیم. بلکه یک اشارهگر به تغییرات صورت گرفته زدیم.
توی بعضی پروژهها ممکن هست ببینید که سیاست مرج، به صورت پیش فرض، مدل بالا بشه. دلایل این کار هم معمولاً:
توصیه به جوانان: میشه یه قاعده سرانگشتی هم داشته باشیم برای خودمون که اگر تغییرات خیلی سادهاس همون fast-forward گزینهای خوبی هست ولی اگر تغییرات زیادی صورت گرفته و خود کامیتها و تاریخچه برای ما مهم هست از مدل no-fast-forward بریم بهتر هست.
یک آپشن دیگه که توی دستور merge ممکن هست به کارمون بیاد، آپشن پایین هست که باعث میشه همهی تغییرات در قالب یک کامیت اضافه بشه. یکی از کاربرداش هم میتونه این باشه که اکثر کامیتها حول یک ویژگی بوده اما به هر دلیلی مجبور شدیم وسط کار کامیتهای متفاوتی داشته باشم. اما قصدمون ادغام همهی کامیت در قالب یک کامیت هست.
مهمترین ویژگی که باعث میشه ما از ورژن کنترلر استفاده کنیم، قابلیت rollback کردن هست. دستور revert مشابه reset است اما با این تفاوت که revert به عنوان ورودی یک کامیت رو میگیره به بجای اینکه به اون وضعيت برگرده، میاد عکس تمام کارهایی که توی اون کامیت انجام شده، رو انجام میده.
کاربردش؟ فرض کنید توی یک کامیت یک dependency رو حذف کردیم و میخوایم دوباره اون رو داشته باشیم به جای دستی اضافه کردناش، بهتره با revert این کار انجام بشه، چون در قالب یک کامیت به تاریخچه اضافه میشه و دیگران هم می تونن این رو ببینن. یا مثلاً یک کامیت باعث به وجود اومدن باگ شده، و میخواید به وضعیت قبلی برگردید؟ جواب کمک گرفتن از git revert هست.
توی کامیتهای بالا، آخرین کامیت رو اگر دقت کنید، Revert بعلاوه پیام کامیت قبلی هست.
اول از همه بد نیست بدونیم cherry-pick به معنی گلچین (سوا) کردن هست و این امکان رو به ما میده که بتونیم کامیتهای خاصی رو از یک برنچ به برنچ دیگه ببریم. و تفاوتش با git-reset اینه که دستور cherry-pick دقیقا عمل کپی رو انجام میده و باعث حذف چیزی نمیشه؛ در حالی که git-reset کارهاش جابجایی اشارهگر کامیتهاست.
کجاها کاربرد داره؟ فرض کنید یکی از همکاران داره توی یک برنچ یک ویژگی جدید به پروژه اضافه میکنه و در این حال یک باگ اساسی از سیستم رو هم طی یک یا چند کامیت حل کرده و ما فقط کد بخش حل کردن مشکل رو میخوایم. در این حالت اگر merge کنیم همه تغییرات رو اضافه میشه، اما با cherry-pick فقط کامیت مد نظر ما رو به برنچ فعلی اضافه میشه.
توی شکل پایین، ما دو تا برنچ داریم (master, feature-branch) و بر حسب نیاز،لازم هست که مثلا کامیت e رو به برنچ master اضافه کنیم:
روال کار اینجوری هست که اول به برنچ مورد نظر (master) میریم و بعدش هش کامیت e رو به cherry-pick میدیم:
خب، میرسیم به شاید یکی از سختترین یا ناشناختهترین بخشهای گیت. تا الان یاد گرفتیم اگر بخواهیم تغییرات رو از یک برنچ به برنچ دیگه ببریم تنها راهمون git-merge بود. اما یک راه دیگه هم وجود داره که اون git-rebase هست. اگر خاطرتون باشه، توی بخش git-merge گفتیم نوع اول مرج fast-forward بود. و این فقط در صورتی قابل انجام بود که گره/جد مشترک نسبت به موقعی که ما برنچمون رو ساخته بودیم تغییر نکرده باشه. در غیر این صورت مرجِ three-way مورد استفاده قرار میگرفت.
خبر خوب اینه که git-rebase این امکان رو به ما میده که تغییرات رو جوری توی برنچ مورد نظر اعمال کنیم که انگار هیچ تغییری توی مبدا اتفاق نیفتاده و به صورت خطی مرج بشن و و دیگه نیازی به ساخت یک کامیت آبجکت جداگانه نباشه. در حقیقت همونطور که از اسمش پیداست داره base برنچ رو (یا بخونید گره/جد مشترک) رو به وضعیت جدید تغییر میده.
مثال پایین از مستندات گیت هست: فرض کنید مطابق شکل پایین یک برنچ master داشتیم و از اون یک برنچ دیگه به اسم experimental ساختیم و توی اون کامیت هم داشتیم. در عین حال، خود برنچِ master هم در این بازه تغییر کرده.
در نتیجه اگر بخواهید کامیت کنید، به صورت fast-forward دیگه نمیشه. و خروجی اون یک مرج سه طرفه میشه:
ولی اگر هدف ما خطی نگه داشتن تاریخچه کامیتها باشه میتونیم با کمک git-rebase این کارو انجام بدیم. این دستور میاد برنچ experimental رو بر میداره و نقطه/جد مشترکاش رو به master جدید تغییر میده که شامل کامیت C3 هم هست. خروجی git-rebase این شکلی میشه:
حالا اگر دستور 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 داره که توی شکل زیر به کار میاد.
مطابق تصویر بالا، مثلاً میخوایم برنچ client رو بدون کامیتهای مشترکاش با server ببریم rebaseاش کنیم با برنچ master.
در نهایت git-rebase یک آپشن خیلی باحال دیگه هم داره به اسم interactive که با اون میشه تاریخچه کامیتها رو بازنویسی کرد، مثلاً کارهایی مثل Squash یا تغییر پیام کامیت چند کامیت قبل رو هم میشه با این مدل تغییر داد.
چند وقت پیش من یک ارائه راجعبه گیت داشتم، و در حین ارائه حس کردم مباحثی هست که به خوبی در رابطه با گیت درک نشده، بهمین دلیل توی این دو قسمت (قسمت اول) سعی کردم تا جایی که شدنی هست این مشکلات رو پوشش بدم.
در پایان، امیداوارم که از این مطالب چیزی یاد گرفته باشید!
https://www.atlassian.com/git/tutorials/using-branches/git-merge
https://git-scm.com/book/en/v2/Git-Branching-Rebasing
https://www.daolf.com/posts/git-series-part-2/
https://thoughtbot.com/blog/git-interactive-rebase-squash-amend-rewriting-history