چگونه از git استفاده کنیم–بخش دوم–branch, merge و rebase
این مطلب در ادامهی چگونه از git استفاده کنیم نوشته شده. پیشنهاد میکنم قبل از مطالعهی این مطلب به پست قبلی هم یک نگاه بندازید. در ادامهی مطلب بر خلاف مطلب قبلی برای راحت تر شدن فهم موضوع فرض شده که از گیت برای مدیریت کد منبع یک نرمافزار استفاده میشه، هرچند بدیهتاً کاربرد گیت محدود به این موضوع نیست.
شاخه یا branch
در فرایند توسعهی نرمافزار خیلی وقتها پیش میاد که لازم باشه تغییرات گستردهای در کد ایجاد شه، فیچری اضافه بشه، کدی به صورت تست به برنامه اضافه بشه و… که تا مدتی (مثل چند روز یا چند ماه) ناپایداره یا مشخص نیست که قرار هست به کد اصلی اضافه بشه یا نه. برای همین نیاز داریم که یه تعداد کامیت انجام بدیم بدون اینکه به کد پایدار برنامه خللی وارد شه و توی کار بقیه دخالتی انجام شه. اولین راهی که به ذهن میرسه اینه که یک کپی از مخزن ایجاد کنیم و کامیتها رو روی اون ببریم جلو تا وقتی که کد به حالت پایدار برسه و بشه این کامیتها رو روی کد اصلی هم اعمال کرد.
گیت برای همچین شرایطی راه حل ساده تری در اختیارمون گذاشته به اسم branch (شاخه). در این شرایط بجای کپی گرفتن از مخزن کافیه که یک شاخهی جدید درست کنیم و روی اون شاخه کار کنیم. کامیتهایی که روی شاخهی جداگانه انجام میشن ایزوله هستند و تا زمانی که نخوایم با کد اصلی ترکیب نمیشن. هر مخزن gitای که ایجاد میشه به صورت پیشفرض شاخهای به اسم master داره و کامیتهامون روی master به صورت خطی، جلو میرن. اگر تاریخچهی git رو مثل یک درخت در نظر بگیریم شاخهی master مثل تنهی درخت میمونه.
توی این تصویر شاخهای به اسم iss53 از شاخهی master در کامیت C2 جدا شده و کار روی هر دو برنچ بدون دخالت در کد همدیگه جلو رفته (منبع عکس). کامیتهای C3 و C5 از تغییراتی که در C4 اعمال شده خبردار نیستند و برعکس.
برای ساخت یک شاخهی جدید:
git branch gholi
این دستور یک شاخهی جدید به اسم قُلی از کامیتی که الان روش هستیم جدا میکنه اما وارد اون شاخه نمیشه. برای اینکه بریم روی اون شاخه و کار رو ادامه بدیم، به اصطلاح باید checkout کنیم. با این دستور:
git checkout gholi
راه میانبری هم برای ساخت شاخه و بلافاصله checkout کردن روش هست (با اندکی تفاوت):
git checkout -b gholi
برای حذف یک شاخه میتونیم از دستور زیر استفاده کنیم:
git branch -d gholi
Merge
بعد از اتمام کار و نهایی شدن کد یک شاخه، لازم میشه که کدهای اون شاخه با یک شاخهی دیگه (معمولاً master) ترکیب شه. گاهی اوقات هم میخوایم تغییراتی که در یک شاخهی سومی وجود داره رو وارد شاخهای که درحال کار روش هستیم کنیم. در این مواقع باید از دستور merge استفاده کنیم. برای merge اول باید وارد شاخهی مقصد شیم (با دستور checkout) و سپس با دستور مناسب شاخهی ثانویه رو با مقصد ترکیب کنیم.
git checkout dest
git merge source
یادتون باشه که git merge X
شاخهی X
رو با شاخهی فعلی ترکیب میکنه. برای اینکه متوجه شیم که شاخهی فعلی چی هست میتونیم از git status
کمک بگیریم.
اگر خوششانس باشیم مرج fast-forward هست. یعنی بدون هیچ کانفلیکتی انجام میشه. وگرنه باید کانفلیکتها رو برطرف کنیم و بعد از تموم شدن یک کامیت merge انجام بدیم. با استفاده از پرچم --no-ff
میتونیم کاری کنیم که در هر صورت کامیت مرج ساخته بشه. چه مرج بتونه fast-forward و بدون کامیت اضافه انجام بشه و چه کانفلیکت داشته باشه و کامیت مرج ضروری باشه.
روشهای پیشنهادی استفاده از شاخهها
workflowهای پیشنهادی برای استفاده از git و مدل استفاده از branchها خیلی متنوع هستند و هرکدوم مزایا و معایبی دارن. هرتیم با توجه به سطح افراد، نوع کار، حجم کدها، متودولوژی مورد استفاده و بقیهی عوامل مؤثر یک مدل رو انتخاب میکنه و طبق اون جلو میره یا اینکه از ترکیبی از چند مدل استفاده میکنه. من اینجا دو مدلی که ازشون تو پروژههای مختلف استفاده کردم رو معرفی میکنم. چیزی که در هردو مدل مشترک هست اینه که شاخهی master حاوی یک نسخهی پایدار از برنامهس و تحت هیچ شرایطی نباید نسخهی ناپایدار روی master بیاد. در هر لحظه که نیاز باشه باید بشه آخرین نسخهی روی master رو بدون مشکل اجرا کرد.
مدل اول: این مدل تشابه زیادی با مدل github داره (اطلاعات بیشتر). شاخهی master حفاظت شده (protected)ه و بجز مدیر فنی پروژه کسی اجازهی کامیت کردن روی master رو نداره. جز اینکه همه باید حواسشون باشه که روی master کامیت نکنند، خود git هم اجازهی این کار رو نمیده. هر عضو پروژه برای رفع باگ، اضافه کردن فیچر، آزمایش یک کد و هر تغییر دیگهای که لازم داره، از آخرین نسخهی روی master یک شاخه جدا میکنه و کارش رو انجام میده. هروقت که کارش تموم شد و آمادهی ترکیب شدن با شاخهی master بود به فردی که اجازهی دسترسی به master داره خبر میده تا کدش رو وارد master کنه. بعد از این شاخهای که کارش تموم شده حذف میشه (با دستور git branch -d gholi
). با همین روند دوباره یک شاخهی جدید برای کارهای آینده ایجاد میشه و کار تکمیل میشه و merge و الخ.
در این مدل هر فیچری به طور کاملا جدا و ایزوله از بخشهای دیگه قابل آزمایش و تکمیله. این امکان وجود داره که ادامهی کار روی یک فیچر موقتاً رها بشه و کار دیگهای که پیش اومده انجام بشه و بعداً دوباره روی اون فیچر کار شه. در این مدل رفع conflictها همه با یک نفر (کسی که کدها رو با master مرج میکنه) هست و چون فردی برای این کار انتخاب میشه که تسلط بیشتری روی کد داره، احتمال خطا در merge کمتر میشه. اما اگر دوتا شاخه لازم باشه که با هم merge شن تا یک شاخه تغییرات شاخهی دیگری رو داشته باشه کار یکم سخت میشه و رفع conflictها گیج کننده.
مدل دوم: شاخهی master همچنان حفاظت شدهست، اما تمام کارها بجای اینکه branchهای جدا داشته باشند روی یک شاخه (مثلا به اسم dev) انجام میشن. همچنان کارهایی که معلوم نیست قراره روی کد اصلی بیان یا نه یا کارهایی که مدت طولانی کد رو ناپایدار و غیرقابل استفاده برای بقیه میکنه روی شاخهی جداگانه انجام میشن با این تفاوت که این شاخهها از dev منشعب میشن نه master.
این مدل برای تیمهای کوچیک بهتره. استفاده از git براشون سربار کمتری داره و هرکسی کارهای ناقص بقیه رو میتونه داشته باشه (هم ویژگی مثبتی هست هم منفی). چون تغییرات کم کم باهم ترکیب میشن و merge نمیمونه برای آخر احتمال conflict خوردن کمتر میشه و رفعشون هم ساده تر.
توی این مدل پیشنهاد میشه که روی هر شاخهی x برای اینکه تغییرات سرور با تغییرات خودمون ترکیب بشه (اصطلاحا ترکیب x با origin/x) بجای merge از rebase استفاده شه. این کار دو مرحلهی fetch و rebase رو داره.
git fetch && git merge
# is equal to
git pull
git fetch && git rebase
# is equal to
git pull --rebase
تفاوت Rebase و Merge
وقتی که دو شاخه باهم merge میشن همهی تغییرات شاخهی ثانویه با هم جمع میشن و به صورت یک commit به آخر شاخهی مقصد اضافه میشن. در rebase، شاخهی ثانویه از ریشهش جدا میشه و به انتهای شاخهی اولیه متصل میشه.
Initial:
---A---B---C---D---E
|---X---Y
After merge:
---A---B---C---D---E--F
|---X---Y--------/
After rebase:
---A---B---C---D---E---X---Y
بعد از rebase اینطور به نظر میرسه که کامیتهای X و Y بعد از E انجام شدن. یعنی کسی که اونها رو کامیت کرده اول صبر کرده کار بقیه تموم شه و به E برسه بعد کار خودش رو شروع کرده، هرچند در واقعیت همه کارها رو به صورت موازی پیش میبرن. به این صورت شاخه به شکل خطی جلو میره و دنبال کردن تغییرات خیلی سادهتره.
موقعی که rebase انجام میدیم چون کامیتهامون دونه دونه به آخر شاخهی مقصد اعمال میشن به ازای هر کامیت احتمال یک بار conflict خوردن هست. یعنی در بدترین حالت به تعداد کامیتهای روی شاخهی ثانویه میشه کانفلیکت ایجاد شه. البته چون کامیتها دونه دونه دارن اعمال میشن حجم تغییرات در هر کامیت کم هست و احتمال کانفلیکت خیلی کم میشه، به همین دلیل هم کانفلیکتها هم معمولا جزئی و ساده هستن و اذیت خیلی خاصی ندارن.
فرض کنید شاخهی dev رو میخوایم با master مرج کنیم. اول باید وارد شاخهی مقصد (master) بشیم و بعد:
git checkout master
git merge --no-ff dev
اگر کانفلیکتی وجود داشت رفع میکنیم و یک کامیت merge انجام می دیم.
اینبار فرض کنید که میخوایم شاخهی dev رو روی origin/dev ریبیس کنیم. برخلاف merge باید اول وارد شاخهی مبدا شیم و:
git checkout dev
git rebase origin/dev
گیت شروع میکنه کامیتهای روی dev رو به آخر origin/dev اضافه کردن. اگر در این حین به کانفلیکتی برخورد کنه یک پیام خطا تولید میشه و وظیفهی ماست که کانفلیکت رو برطرف کنیم. بعد از برطرف کردن کانفلیکتها باید rebase رو ادامه بدیم
git rebase --continue
بعضی اوقات در حین rebase یک کامیت عملا هیچ تغییری در مقصد ایجاد نمیکنه. این وقتی به وجود میاد که تمام تغییراتی که توی اون کامیت وجود داشتن قبلا یه جایی به یک طریقی روی مقصد داده شده باشند. تو این مواقع هم git ارور تولید میکنه و باید از روی اون کامیت بپریم.
git rebase --skip
اگر در حین rebase متوجه شدیم که دو شاخهی اشتباه رو داریم با هم rebase میکنیم یا به هر دلیل دیگری از ادامهی rebase منصرف شدیم میتونیم از دستور زیر استفاده کنیم:
git rebase --abort
مشابه همین رو برای merge هم داریم.
git merge --abort
ادامهی مطالب
گیت ابزار خیلی گسترده و گاهی پیچیدهای هست. استفاده ازش سرعت کار رو خیلی بالا میبره و وجود تاریخچهی دقیق کد توی دیباگ کردن و مقایسهی کد و خیلی کارهای جانبی دیگه مفیده، تا حدی که بعضی کارها رو بدون یک ورژن کنترل خوب (مثل گیت) نمیشه انجام داد. طبق تجربه، با توجه سرعتی که گیت نسبت به روشهای سنتی ترکیب کد مثل استفاده از ایمیل و فلش و pastebin و… و مرج دستی کد به کار میده به علاوه کم کردن احتمال خطای انسانی و حذف و اضافه شدن اشتباه کد؛ کاملا به صرفهست که توی یک تیم (که باید به سرعت به یک محصول برسند) قبل از شروع کد زنی وقتی برای یاد گیری گیت گذاشته بشه. شخصاً منابع فارسی مناسبی برای گیت پیدا نکردم (البته یکی از دلایل اینه که کلا دنبال منابع فارسی نگشتم :D) و روی تمام جزئیات git تسلط ندارم اما سعی میکنم که در حدی که توان دارم بر اساس خواندهها و شنیدهها و تجربیات، راهنمای سریعی برای شروع کار با git بنویسم.
همچنین بخوانید: SSH Public key چیه؟ به چه دردی میخوره؟ چطوری بسازم؟