مرج در Git: چگونه تغییرات شاخهها را درست ادغام کنیم؟
مرج در گیت (Git Merge) چیست و چگونه بدون از دست دادن کد، شاخهها را ادغام کنیم؟ راهنمای گامبهگام از چهار نوع مرج و استراتژیهای ادغام تا مرج در پولریکوئست و پیشگیری از فاجعه.
مرج (Merge) در گیت، آن دستوری است که بار اول که میزنید، حس میکنید چیزی جادویی اتفاق افتاده. من هم همان حس را داشتم؛ وقتی برای اولین بار دو شاخهٔ جدا را با git merge ادغام کردم و دیدم که هر دو مجموعه تغییر با هم ترکیب شدند، فکر کردم کار تمام است. اما بعد در پروژهٔ تیمی، شاخهای را ادغام کردم که ده commit قدیمیتر از main بود؛ نتیجه یک merge commit با پنج فایل تعارضدار و بیست دقیقه کار برای برگرداندن وضعیت. تجربهام میگوید مرج در گیت، نه یک دستور ساده است و نه یک عملیات خطرناک؛ یک تصمیم معماری است که باید با آگاهی گرفته شود — همانقدر که نوشتن کد اهمیت دارد، شکلِ ادغام کردن کد هم اهمیت دارد.
مرج در گیت چیست و چرا به آن نیاز داریم؟
گیت بهعنوان یک سیستم کنترل نسخهٔ توزیعشده، امکان کار همزمان روی چند شاخهٔ (Branch) مستقل را فراهم میکند. هر شاخه، یک خطِ توسعهٔ جداگانه است که تغییراتش در آن ذخیره میشود، بدون آنکه روی شاخههای دیگر اثر بگذارد. اما در نهایت، این خطوطِ موازی باید در یک نقطه به هم برسند؛ اینجاست که مفهوم مرج وارد میشود. مرج یعنی «ادغام کردن تاریخچهٔ یک شاخه در شاخهٔ دیگر»، بهطوریکه گیت بتواند تغییرات هر دو را در یک نسخهٔ واحد ترکیب کند.
چرا این کار لازم است؟ تصور کنید در یک تیم سهنفره، یک نفر روی صفحهٔ محصول کار میکند، یک نفر روی سیستم پرداخت، و یک نفر روی بهبود سرعت. اگر همه مستقیم روی main کار کنند، هر commit با commit دیگری قاطی میشود و هیچکس نمیداند کدام تغییر از کدام کار میآید. با شاخهبندی جدا و مرج، هر تغییر در جای خود میماند، امکان بازبینی دارد و در لحظهٔ مناسب به اصلی اضافه میشود. اگر با مفاهیم پایهٔ شاخهبندی آشنا نیستید، پیش از ادامه مرجع برنچ در گیت را بخوانید؛ در آنجا ساختار شاخه و منطق ایجاد آنها توضیح داده شده است.
نکتهای که در پروژههای تیمی زیاد دیدهام: مرج، فقط یک عملیات فنی نیست؛ یک «عملیات سازمانی» است. وقتی شاخهای را ادغام میکنید، در واقع دارید میگویید «این تغییرات بهاندازهٔ کافی بالغ شده که به خط اصلی پروژه اضافه شوند.» این تصمیم، اگر آگاهانه گرفته شود، پروژه را نجات میدهد؛ اگر شتابزده باشد، پروژه را به سمت آشفتگی میبرد. تعریف دقیقتر مفاهیم اولیه در آموزش گیت از صفر آمده و اگر میخواهید بهسرعت با دستورات پرکاربرد آشنا شوید، دستورات پرکاربرد گیت راهنمای کاربردی است.
مرج یک دستور نیست؛ یک اعلام است. وقتی شاخهای را مرج میکنید، دارید میگویید این تغییرات بالغ شدهاند و آمادهاند وارد خط اصلی شوند. این جمله را با احتیاط بگویید.
آناتومی یک مرج: از دستور تا تاریخچه
دستور پایهای برای مرج ساده است:
git checkout main
git merge feature/new-pricing
این دو خط، شاخهٔ فعلی را روی main تنظیم میکند و بعد شاخهٔ feature/new-pricing را در آن ادغام میکند. اما در همان لحظه، یکی از چهار حالت زیر ممکن است رخ دهد:
- Fast-forward merge: اگر شاخهٔ
mainاز زمان جدا شدن شاخهٔ مقصد، هیچ تغییر جدیدی نداشته، گیت فوراً اشارهگر main را به نوک شاخهٔ مقصد منتقل میکند. تاریخچه، خطی و تمیز میماند. این حالت مطلوب و بیدردسرترین نوع مرج است. - Three-way merge: اگر main تغییر کرده و شاخهٔ مقصد هم تغییرات مستقلی دارد، گیت از یک نقطهٔ مشترک (Common Ancestor) استفاده میکند و یک merge commit میسازد که دو تاریخچه را به هم میدوزد. این حالت رایجترین است.
- Conflict: اگر در هر دو شاخه، همان خطوط یک فایل تغییر کرده باشند، گیت نمیتواند تصمیم بگیرد و یک تعارض (Conflict) اعلام میکند. مسیر حل این تعارض در حل تعارض در گیت گامبهگام آمده است.
- Already up to date: اگر شاخهٔ مقصد هیچ تغییر جدیدی نسبت به شاخهٔ فعلی نداشته باشد، گیت میگوید «چیزی برای ادغام نیست». این حالت اغلب وقتی رخ میدهد که خودتان قبل از مرج، آخرین نسخه را کشیده باشید.
نکتهٔ ظریف دربارهٔ merge commit: وقتی گیت یک merge commit میسازد، این commit دو parent دارد — یعنی به دو خط تاریخچه اشاره میکند. همین ویژگی است که merge commit را از commit معمولی جدا میکند. در نمای گرافیکی با دستور git log --graph --oneline، این ساختار بهشکل یک دوراهی و ادغام مجدد دیده میشود. بار اول که این تصویر را دیدم، فهمیدم چه چیزی در تاریخچهٔ گیت اتفاق میافتد؛ توصیه میکنم یک بار روی یک مخزن آزمایشی این دستور را امتحان کنید.
چهار نوع مرج که هر توسعهدهنده باید بشناسد
علاوه بر مرج معمولی، گیت چند نوع مرج تخصصی هم دارد که در موقعیتهای خاص به کار میآیند:
۱. Fast-forward Merge
سادهترین و تمیزترین نوع مرج. زمانی رخ میدهد که شاخهٔ فعلی، از نقطهٔ انشعاب شاخهٔ مقصد تغییری نکرده باشد. نتیجهاش یک تاریخچهٔ خطی است که گویی شاخهٔ فرعی هرگز وجود نداشته. برای پروژههایی که به تاریخچهٔ خطی اهمیت میدهند، انتخاب مطلوب است. برای اجبار به این حالت، از git merge --ff-only استفاده کنید؛ اگر شرایط فراهم نباشد، گیت مرج را رد میکند.
۲. Three-way Merge
حالت معمول مرج در پروژههای تیمی. گیت سه نقطه را کنار هم میگذارد: Base (نقطهٔ مشترک)، Ours (شاخهٔ فعلی) و Theirs (شاخهٔ مقصد)، و از تفاوت این سه، یک نسخهٔ ترکیبی میسازد. نتیجهٔ این نوع مرج، یک merge commit است که دو تاریخچه را در خود نگه میدارد.
۳. Squash Merge
در این حالت، تمام commit های شاخهٔ مقصد در یک commit واحد فشرده میشوند و بعد روی شاخهٔ فعلی اعمال میگردند. مزیت: تاریخچهٔ main تمیز میماند و هر merge، یک commit واحد است. عیب: تاریخچهٔ کامل commit های شاخهٔ فرعی از بین میرود و امکان بازگشت دقیق به یک commit میانی از دست میرود. Squash معمولاً در گیتهاب، گیتلب و ابزارهای مشابه بهعنوان گزینهٔ مرج در Pull Request استفاده میشود. دستور خط فرمان:
git merge --squash feature/new-pricing
git commit -m "feat: add new pricing logic"
توجه: در این حالت، گیت خودش commit نمیسازد؛ باید بعد از squash، خودتان commit بزنید.
۴. Octopus Merge
نوعی مرج که چند شاخه را همزمان ادغام میکند. شاید اسمش عجیب باشد (Octopus یعنی هشتپا)، ولی در عمل برای ادغام چند شاخهٔ کوچک مستقل استفاده میشود:
git merge feature/a feature/b feature/c
این دستور، سه شاخه را در یک عملیات ادغام میکند. کاربرد اصلیاش در پروژههای بزرگ است که چند قابلیت مستقل باید با هم عرضه شوند. البته اگر بین این سه شاخه تعارضی باشد، octopus merge رد میشود و باید یکییکی ادغام کنید.
| نوع مرج | سناریو | نتیجهٔ تاریخچه | دستور |
|---|---|---|---|
| Fast-forward | شاخهٔ فعلی تغییری نداشته | خطی | git merge --ff-only |
| Three-way | هر دو شاخه تغییر کردهاند | شامل merge commit | git merge branch |
| Squash | میخواهید همه در یک commit فشرده شود | خطی، یک commit | git merge --squash |
| Octopus | چند شاخهٔ مستقل همزمان | merge commit چند پدری | git merge a b c |
استراتژیهای مرج: انتخاب بین سادگی و نظم تاریخچه
گیت در پشت صحنه، از چند استراتژی برای ادغام استفاده میکند. دانستن این استراتژیها در پروژههای بزرگ که تاریخچهٔ پیچیده دارند، کمککننده است:
- ort (پیشفرض امروزی): استراتژیای که در نسخههای جدید گیت جایگزین recursive شده. سریعتر، دقیقتر و معمولاً بیمشکل. در نود و نه درصد موارد، همین را استفاده میکنید.
- recursive: استراتژی قدیمیتر، هنوز در برخی شرایط خاص کاربرد دارد.
- ours: کل شاخهٔ مقصد را نادیده میگیرد و فقط شاخهٔ فعلی را حفظ میکند. کاربردش وقتی است که میخواهید مرج را از نظر تاریخی ثبت کنید ولی تغییرات طرف مقابل را نمیخواهید.
- subtree: برای مواقعی که پروژهای را بهعنوان زیرپروژه (Subproject) دارید و میخواهید آن را در مخزن اصلی ادغام کنید.
برای فعالکردن یک استراتژی خاص، از گزینهٔ -s یا --strategy استفاده میشود:
git merge -s recursive feature/branch
در تجربهٔ پروژهای، این انتخابها فقط وقتی معنا پیدا میکنند که واقعاً با ساختار پیچیدهٔ مخزن مواجه باشید. برای اکثر پروژهها، استراتژی پیشفرض (ort) کافی است. توصیهام این است که بهجای دستکاری استراتژی مرج، بیشتر روی نظم شاخهبندی و اندازهٔ commit ها وقت بگذارید؛ این دو متغیر، اثرشان روی سلامت مرج از هر تنظیم استراتژی بیشتر است.
مرج یا ریبیس؟ دوگانهای که هیچوقت کهنه نمیشود
این سؤال، یکی از پرتکرارترین سؤالات در تیمهای توسعه است. تفاوت اصلی این است که در مرج، تاریخچه هر دو شاخه حفظ میشود و یک merge commit ساخته میشود؛ در ریبیس، commit های شاخهٔ شما بازنویسی میشوند و روی نوک شاخهٔ مقصد قرار میگیرند، بهطوریکه تاریخچه خطی میشود. نتیجهٔ مرج، تاریخچهٔ دقیقِ هر چه اتفاق افتاده است؛ نتیجهٔ ریبیس، تاریخچهای تمیز ولی دستکاریشده.
سه نکتهای که در پروژهها به کارم آمده:
- برای شاخههای شخصی، ریبیس: اگر روی شاخهای کار میکنید که فقط شما دارید، ریبیس قبل از مرج باعث میشود تاریخچهٔ main تمیز بماند. این کار در پروژههایی که commit های فرعی میخواهند خطی بهنظر برسند، مفید است.
- برای شاخههای مشترک، مرج: اگر شاخه را با دیگران به اشتراک گذاشتهاید، ریبیس نکنید. ریبیس، commit های موجود را بازنویسی میکند و روی شاخههای دیگران اثر مخرب دارد.
- برای پول ریکوئست، بسته به تیم: بعضی تیمها روی merge commit های مرتب اصرار دارند، بعضی ترجیح میدهند تاریخچه خطی باشد و squash-merge استفاده میکنند. در هر دو حالت، قواعد را در مستندات تیم روشن کنید و از آنها پیروی کنید.
در پروژههای وردپرس که با آنها کار کردهام، رویکرد متعادل غالباً این است: روی شاخههای قابلیت شخصی، ریبیس کنید تا تاریخچه خطی بماند؛ در ادغام به main، از مرج معمولی استفاده کنید تا تاریخچهٔ مدیریتی دقیق حفظ شود. ترکیب این دو، همزمان نظم و صحت را تضمین میکند. اگر با مفهوم ریبیس و دستورات آن آشنا نیستید، مرجع دستورات پرکاربرد گیت توضیح میدهد.
مرج، حقیقتِ تاریخچه را ثبت میکند؛ ریبیس، داستانِ مرتبی از تاریخچه میسازد. انتخاب بین این دو، انتخاب بین دقیق بودن و خوانا بودن است.
فرآیند گامبهگام یک مرج سالم
ترتیبی که در پروژههای خودم به کار میبرم — با تمام گامهای احتیاطی:
- بهروزرسانی شاخهٔ main: پیش از هر کاری، مطمئن شوید که main بهروز است:
git checkout main git pull origin main - بهروزرسانی شاخهٔ قابلیت: شاخهٔ خودتان را هم بهروز کنید تا از main عقب نمانده باشد:
اگر در این مرحله تعارض دیدید، بهتر است همانجا حلش کنید؛ چون در حین کار روی شاخهٔ قابلیت، ذهن شما با کد آشناست.git checkout feature/new-pricing git merge main - اجرای تست روی شاخهٔ قابلیت: پیش از مرج به main، مطمئن شوید که همهٔ تستها روی شاخهٔ قابلیت پاس میشوند. اگر Continuous Integration (بهاختصار CI) دارید، این گام خودکار است. مسیر راهاندازی آن در GitHub Actions آمده است.
- مرج به main با یک پیام معنادار:
گزینهٔgit checkout main git merge --no-ff feature/new-pricing--no-ff(مخفف no fast-forward) باعث میشود حتی اگر شرایط fast-forward فراهم باشد، گیت یک merge commit بسازد. این کار برای مستندسازی تاریخی اهمیت دارد؛ چون در غیر این صورت، مرج شاخه بهطور کامل در تاریخچه گم میشود. - تست پس از مرج: بعد از مرج، دوباره تستها را اجرا کنید. اگر مشکل جدیدی دیدید، پیش از push کردن، در همان main محلی حلش کنید.
- push به مخزن دور:
git push origin main - پاکسازی شاخه: اگر شاخهٔ قابلیت تمام شده، آن را حذف کنید تا مخزن تمیز بماند:
git branch -d feature/new-pricing
یک نکتهٔ تجربی: در پروژههای بزرگ، پس از هر مرج یک «پنجرهٔ تحریم» کوچک تعریف کنید — مثلاً نیم ساعت — که در آن کسی commit جدید به main نزند. در آن پنجره، تست کامل بگیرید و مطمئن شوید که مرج خودش مشکلی نساخته. این عادت، از دردسرِ پس از مرج که در پروژههای چندنفره شایع است، جلوگیری میکند.
مرج در Pull Request: تفاوتهای میدانی
در گیتهاب و گیتلب، وقتی یک Pull Request (به اختصار PR) باز میکنید و میخواهید مرج کنید، سه گزینه در پیش دارید:
- Merge commit: همان مرج معمولی که در بالا توضیح دادم. تاریخچهٔ هر دو شاخه حفظ میشود. مناسب پروژههایی که میخواهند تاریخچهٔ دقیق داشته باشند.
- Squash and merge: تمام commit های PR در یک commit واحد فشرده میشوند. مناسب پروژههایی که میخواهند تاریخچهٔ main تمیز بماند.
- Rebase and merge: commit ها روی نوک main بازنویسی میشوند و بدون merge commit اضافه میشوند. مناسب پروژههایی که میخواهند تاریخچه خطی داشته باشند.
انتخاب بین این سه، به سیاست تیم بستگی دارد. توصیهٔ من این است که در یک تیم، یک سیاست واحد انتخاب کنید و همهٔ اعضا همان را رعایت کنند. اگر بین رویکردها نوسان داشته باشید، تاریخچهٔ پروژه به آشفتگی میرسد. تفصیل کار با PR در Pull Request در گیتهاب و آموزش گیتهاب آمده است. اگر پروژه روی GitLab میزبانی میشود، این گزینهها هم مشابهاند و روند کار همان است.
یک نکتهٔ ظریف که در تیمهای بالغ زیاد دیدهام: پیش از مرج PR، از ابزار «Required Reviews» استفاده کنید تا حداقل یک نفر از تیم کد را تأیید کند. این کار از ادغام کد تستنشده جلوگیری میکند. در تیمهای کوچک که فقط یک توسعهدهنده دارد، این گزینه استفاده نمیشود؛ ولی بهعنوان عادت سالم، حتی در حالت تکنفره هم میتوانید خودتان در همان PR یک یادداشت بنویسید که چرا این تغییر را درست میدانید — این کار در بازگشت به پروژه در ماههای بعد، نجاتبخش است.
اشتباهات پرهزینهای که در مرج زیاد دیدهام
سه اشتباه که هر کدامشان میتواند یک پروژه را ساعتها عقب بیندازد:
- مرج کردن بدون بهروزرسانی شاخهٔ main: اگر شاخهٔ قابلیت شما از main عقب باشد و مستقیم مرج کنید، حجم تعارضها بیشتر میشود. همیشه اول main را pull کنید و بعد از آن مرج.
- مرج کردن شاخهای که نصفهکاره است: برخی توسعهدهندگان، برای «تست در پروداکشن» یک شاخهٔ نیمهکاره را مرج میکنند. اگر بعد از مرج، قابلیت بهخاطر مسئلهای غیرفعال شد، تاریخچه آلوده میشود. اگر میخواهید قابلیتی را در پروداکشن تست کنید، آن را پشت یک «Feature Flag» یا متغیر محیطی قرار دهید، نه اینکه کد نصفه را مرج کنید.
- force push بعد از مرج: اگر پس از مرج، متوجه اشتباهی شدید و با force push سعی کنید آن را برگردانید، ممکن است تاریخچهٔ همکاران بههم بریزد. راه درست، commit اصلاحی جدید است، نه force push روی شاخهٔ مشترک.
و یک اشتباه ظاهراً کوچک ولی پرهزینه: مرج کردن در آخر وقت کاری و رفتن به خانه. اگر مرجی ساعت ۶ عصر انجام شود و کسی یک ساعت بعد متوجه مشکلی شود، در لحظهٔ بحرانی، هیچکس نیست که وضعیت را برگرداند. عادت سالم این است که مرجهای مهم در نیمهٔ اول روز یا ابتدای هفته انجام شود، نه پایان هفته.
در لایهٔ معماری: مرج بهعنوان تصمیم طراحی
برای مهندسانی که در پروژههای بزرگ کار میکنند، مرج را نه فقط بهعنوان یک عملیات گیت، بلکه بهعنوان یک تصمیم معماری ببینید. سه اصل که در تیمهای فنی بالغ دیدهام:
- Merge cadence (آهنگ مرج): در پروژههای موفق، مرجها بهصورت منظم انجام میشوند — روزانه یا حداکثر چند روز یکبار — نه آنکه در یک روز فاجعهبار تمام شاخهها ادغام شوند. آهنگ منظم، حجم هر تعارض را کوچک نگه میدارد. اگر شاخهای مدتها بیمرج میماند، در لحظهٔ ادغام به یک بحران تبدیل میشود. مشخص کنید که هر شاخه بیش از چه مدتی (مثلاً یک هفته) نباید بیمرج بماند.
- Trunk-based development یا GitFlow؟ انتخاب بین استراتژیهای شاخهبندی، مستقیماً روی فرآیند مرج اثر میگذارد. در Trunk-based، توسعهدهندگان روزانه به main مرج میکنند و شاخههای طولانی ندارند؛ در GitFlow، شاخههای main، develop، release و feature وجود دارند و مرجها پیچیدهترند ولی نظم تاریخی بالاتری دارند. هیچکدام مطلقاً بهتر نیستند؛ به اندازهٔ تیم و آهنگ انتشار بستگی دارد. اگر تیم کوچک است (کمتر از پنج نفر)، Trunk-based معمولاً سبکتر و کمدردسرتر است.
- CI/CD و تأثیرش روی مرج: در پروژههای CI/CD (Continuous Integration / Continuous Delivery)، هر مرج بهطور خودکار تست و در صورت موفقیت، مستقر میشود. این یعنی در لحظهٔ مرج، یک «دروازه» وجود دارد که مانع ادغام کد ناسالم میشود. اگر CI ندارید، مرج بسته به بازبینی دستی، خطاهای بیشتری را عبور میدهد. راهاندازی CI برای پروژههای وردپرس در GitHub Actions توضیح داده شده است.
یک نکتهٔ ظریفتر که در پروژههای چندساله اهمیت زیادی دارد: مرج، تاریخچهٔ تصمیمهای تیم شماست. اگر یک سال بعد بخواهید بفهمید چرا یک خط کد تغییر کرده، تنها سرنخی که دارید، merge commit و پیامهای commit داخل آن است. بنابراین، کیفیت مرج نه فقط برای امروز، برای آیندهٔ پروژه هم اهمیت دارد. تیمی که به تاریخچهٔ مرج اهمیت میدهد، در طولانیمدت پروژهای قابل نگهداریتر خواهد داشت. این پیوند میان انضباط گیت و کیفیت پروژه، بخشی از همان اصولی است که در ساختاربندی پروژه توسعه وردپرس و اصول کدنویسی تمیز در پروژههای وردپرس شرح دادهام. اگر در پروژههای چندمحیطی (لوکال، استجینگ، پروداکشن) کار میکنید، ترتیب مرج بین این محیطها را هم جزو همین انضباط بدانید؛ رویکرد کلی در توسعه وردپرس با محیط لوکال آمده است. و اگر پس از مرج با خطای گیت روبهرو شدید — مثلاً detached HEAD یا رد شدن push — مرجع رفع خطاهای رایج گیت راهنمای دقیقی است.
سخن آخر
مرج در گیت، در نگاه اول یک دستور بهنظر میرسد؛ ولی وقتی عمیقتر نگاه میکنید، یک عملیات سازمانی است که بر سرعت تیم، نظم تاریخچه و قابلیت نگهداری پروژه اثر میگذارد. با انتخاب درست بین fast-forward، three-way، squash و octopus، با سیاست روشن دربارهٔ مرج در برابر ریبیس، و با رعایت گامهای فرآیند سالم مرج، میتوانید پروژهای بسازید که سالها بعد هم قابل فهم و قابل نگهداری باشد. اگر در پروژههای خودتان به سناریوی جالبی از مرج برخورد کردهاید — مثلاً مرجی که بعد از چند ماه ناگهان معلوم شد مسئلهای پنهان داشته — آن تجربه را در دیدگاه بنویسید. این نکتهها، برای توسعهدهندهٔ بعدی که در همان مسیر قدم میگذارد، از هر راهنمای عمومی ارزشمندترند. 🌿