چگونه تعارض گیت را بدون از دست دادن کدها حل کنیم؟
تعارض گیت (Git Conflict) چیست، چرا هنگام مرج و پول ریکوئست رخ میدهد و چگونه آن را بدون از دست دادن کد حل کنیم؟ راهنمای گامبهگام با دستورات واقعی و ابزارهای حرفهای.
اولین باری که با تعارض گیت روبهرو شدم، بهجای خواندن پیامها، پوشهٔ پروژه را از فایلسیستم حذف کردم و از یک بکاپ قدیمی برگرداندم — یعنی تمام کار دو روزِ خودم را سوزاندم تا از دیدنِ علامتهای عجیبی که Git در فایلها گذاشته بود، فرار کنم. سالها بعد، در پروژههایی با تیمهای چندنفره، یاد گرفتم که تعارض گیت نه دشمن است و نه نشانهٔ بیکفایتی. تعارض، فقط یک پیام است: دو نفر روی یک خط از یک فایل، دو تصمیمِ متفاوت گرفتهاند و Git منتظر است تصمیم نهایی را به او بگویید. تجربهام میگوید پشت هر وحشت از تعارض، یک نبود دانشِ کوچک است؛ و برطرفکردن همان نبود، تفاوت بین ساعتی سرگردانی و پنج دقیقه کار تمیز است.
تعارض گیت دقیقاً چیست؟
گیت (Git) یک سیستم کنترل نسخهٔ توزیعشده است که تغییرات کد شما را در طول زمان نگه میدارد. هر بار که تغییرات را با git commit ثبت میکنید، گیت یک «عکس فوری» از وضعیت پروژه میسازد. وقتی دو شاخه (Branch) مختلف از یک نقطهٔ مشترک رشد میکنند و بعد تصمیم میگیرید آنها را با هم ادغام کنید (مرج)، گیت باید دو مجموعه تغییر را روی هم بگذارد.
در نود درصد موارد این ادغام، بیصدا و بیمشکل انجام میشود. اما اگر هر دو شاخه، روی همان خطوط از یک فایل، تغییر داده باشند، گیت نمیتواند تصمیم بگیرد کدام نسخه درست است. آنوقت میگوید: «من نمیتوانم این را خودم تصمیم بگیرم، لطفاً شما انتخاب کنید.» این نقطه، همان چیزی است که تعارض (Conflict) نامیده میشود.
نکتهای که در پروژههای تیمی خیلی کمک میکند: تعارض فقط زمانی رخ میدهد که دو تغییر، در یک بلاک مجاور یا همپوشان باشند. اگر شما در خط بیست فایل و همکارتان در خط دویست فایل تغییر داده باشید، گیت هر دو را در کنار هم قرار میدهد و تعارضی پیش نمیآید. اگر با مفاهیم پایهٔ گیت آشنا نیستید، پیش از ادامه توصیه میکنم مرجع آموزش گیت از صفر را بخوانید؛ در آن، ساختار مخزن، commit و شاخهها با مثالهای ساده توضیح داده شده است.
تعارض گیت، نشانهٔ خرابی نیست؛ نشانهٔ برخورد دو واقعیتِ متفاوت است که هر دو معتبرند و حالا شما باید تصمیم نهایی را بگیرید.
چرا تعارض رخ میدهد؟ سه سناریوی رایج
در تجربهام، تقریباً تمام تعارضها در یکی از این سه سناریو رخ میدهند:
سناریوی اول: مرج دو شاخه
شما و همکارتان از شاخهٔ main جدا شدهاید و هر کدام روی یک شاخهٔ جداگانه کار کردهاید. حالا میخواهید شاخهٔ همکار را با شاخهٔ خودتان ادغام کنید. اگر هر دو روی همان فایل کار کرده باشید، بهاحتمال زیاد تعارض رخ میدهد. این شایعترین سناریو است و بیشتر اوقات در پروژههای چندنفره دیده میشود. تفصیل کار با شاخهها در برنچ در گیت آمده است.
سناریوی دوم: کشیدن آخرین تغییرات از مخزن دور
شما مدتی روی نسخهٔ محلی کار کردهاید بدون آنکه از مخزن اصلی (Remote) تغییرات را بکشید. حالا با git pull سعی میکنید آخرین نسخه را بگیرید. اگر کسی از تیم در این فاصله روی همان فایلها کار کرده باشد، گیت در لحظهٔ merge (که پیشفرض بخشی از pull است) تعارض میسازد.
سناریوی سوم: پول ریکوئست یا مرج در گیتهاب
شما یک Pull Request (PR) باز کردهاید و منتظر تأییدید. تا زمانی که تأیید بیاید، شاخهٔ اصلی جلو رفته و یک نفر دیگر روی همان فایل تغییر داده. حالا گیتهاب میگوید «این PR قابل مرج خودکار نیست؛ تعارض دارد.» روش حل این تعارض در Pull Request در گیتهاب گامبهگام توضیح داده شده است.
یک نکتهٔ ظریف که در پروژهها زیاد دیدهام: تعارضهای کوچک روزمره، نشانهٔ اشتباه در فرآیند نیستند؛ ولی اگر مرتب تکرار میشوند، الگوی کار تیمی جای بازبینی دارد. یادداشتبرداری از علت تعارضها در طول چند ماه، بهترین داده برای اصلاح این الگو است.
آناتومی یک تعارض: از نشانگر تا حلقهٔ حیاتی
وقتی تعارض رخ میدهد، گیت در فایلهای درگیر، سه نشانگر (Marker) کار میگذارد که ساختار مشخصی دارند:
<<<<<<< HEAD
$price = 1000;
=======
$price = 1200;
>>>>>>> feature/new-pricing
سه بخش این ساختار را بشناسید:
- بلوک اول (بین
<<<<<<< HEADو=======): نسخهای که در شاخهٔ فعلی (HEAD) شما وجود دارد. در مثال بالا، قیمت ۱۰۰۰. - خط جداکنندهٔ
=======: مرز بین دو نسخه. - بلوک دوم (بین
=======و>>>>>>> branch-name): نسخهای که در شاخهٔ مقابل وجود دارد. در مثال بالا، قیمت ۱۲۰۰.
حالا نگاه مهندسی به این ساختار: Git سه نسخهٔ فایل را نگه میدارد — نسخهٔ «Base» (آخرین نقطهٔ مشترک)، نسخهٔ «Ours» (شاخهٔ فعلی) و «Theirs» (شاخهٔ دیگر). به همین دلیل گاهی در حل تعارضهای پیچیده، میتوانید با git checkout --ours <file> یا git checkout --theirs <file> کل فایل را به یکی از دو نسخه برگردانید — بدون اینکه خط به خط دست بزنید. این قابلیت، در پروژههایی که یک فایل کاملاً بازنویسی شده، صرفهجویی زمانی زیادی میکند.
انواع تعارض و تفاوتهای مهمشان
تعارضها را میتوان در چهار دسته دید که هر کدام استراتژی حل متفاوتی دارند:
| نوع تعارض | محل وقوع | استراتژی حل |
|---|---|---|
| Content Conflict | یک یا چند خط از یک فایل | انتخاب دستی بین دو نسخه |
| File-level Conflict | یک فایل حذف، دیگری ویرایش شده | تصمیم به حذف یا حفظ فایل |
| Add/Add Conflict | هر دو شاخه یک فایل جدید ساختهاند | ادغام محتوا یا انتخاب یکی |
| Delete/Modify Conflict | یک شاخه فایل را حذف، دیگری ویرایشش کرده | تصمیم درباره بقای فایل |
در تجربهام، تعارضهای File-level ناخوشایندترین نوعاند؛ چون نه با مرج خطبهخط قابل حلند و نه با ابزارهای بصری. در این موارد، تصمیم درباره بقای فایل، بستگی به نیت تجاری پروژه دارد، نه به فنیِ صرف. اگر شک دارید، با کسی که آن فایل را ساخته، پیش از حذف، همفکری کنید.
آمادهسازی پیش از حل: سه گام طلایی
پیش از آنکه دست به حل تعارض ببرید، سه کار انجام دهید. این سه کار، از دهها دردسر جلوگیری میکند:
گام اول: بکاپ از وضعیت فعلی
پیش از حل، یک بکاپ از شاخهٔ فعلی بگیرید. یک راه تمیز، ساختن یک شاخهٔ موقت است:
git branch backup-before-merge
اگر حل تعارض خراب شد، میتوانید به این شاخه برگردید. این عادت، یکی از ارزشمندترین درسهایی است که در پروژههای تیمی یاد گرفتهام و ریشهٔ آن همان انضباطی است که در گیت در وردپرس درباره آن صحبت کردهام.
گام دوم: شناسایی فایلهای درگیر
با این دستور، فهرست فایلهایی که تعارض دارند را ببینید:
git status
گیت فایلهای درگیر را در بخش «Unmerged paths» فهرست میکند. این نقطهٔ شروع شماست. تعداد فایلها، تخمینِ خوبی از حجم کاری میدهد.
گام سوم: فهم منبع تغییرات
پیش از حل، بدانید هر طرف چه چیزی تغییر داده و چرا. دستور مفید:
git log --oneline HEAD...feature/new-pricing -- <file-path>
این دستور، فهرست commit هایی که در هر طرف روی یک فایل خاص اعمال شده را نشان میدهد. اگر commit ها نامگذاری معناداری داشته باشند، خودِ commit message نصف داستان را میگوید. همینجاست که اهمیت پیامهای commit خوب روشن میشود؛ روش صحیح نوشتن آن در دستورات پرکاربرد گیت آمده است.
راهحل گامبهگام: از git status تا commit نهایی
حالا برویم سر اصل مطلب. ترتیبی که در پروژهها اجرا میکنم:
قدم اول: باز کردن فایل در ادیتور
هر فایل درگیر را در ادیتور باز کنید. ادیتورهای مدرن مثل VS Code، نشانگرها را با رنگهای مختلف هایلایت میکنند. ادیتورهای تخصصی مثل «Sublime Merge» یا «Fork» یا «GitKraken» یک نمای سهستونه ارائه میدهند که در آن نسخهٔ Base، Ours و Theirs را کنار هم میبینید و انتخاب آسان میشود.
قدم دوم: تصمیمگیری خطبهخط
برای هر بلاک تعارض، سه انتخاب دارید:
- نگه داشتن نسخهٔ خودتان (Ours): اگر تغییر خودتان جدیدتر و درستتر است.
- نگه داشتن نسخهٔ مقابل (Theirs): اگر تغییر همکار بهتر است.
- ادغام هر دو: در بسیاری از موارد، بهترین نسخه ترکیبی از هر دو است — نه یکی بهجای دیگری. کد را طوری بنویسید که هر دو منطق در آن لحاظ شده باشد.
نکتهٔ کلیدی: هدف، رسیدن به کدی است که منطق هر دو تغییر را حفظ کند — نه حذف کردن یکی. اگر فقط یکی را نگه میدارید، دلیلش را در commit message بنویسید تا بار بعد که کسی به این نگاه میکند، بداند چرا. این انضباط، بخشی از همان اصولی است که در اصول کدنویسی تمیز در پروژههای وردپرس شرح داده شده است.
قدم سوم: حذف نشانگرها و ذخیره
پس از تصمیم، هر سه نشانگر (<<<<<<<، =======، >>>>>>>) را حذف کنید و فایل را ذخیره کنید. یک اشتباه رایج: فراموش کردن حتی یکی از نشانگرها در فایلی که غول است. پس از ذخیره، حتماً یک بار در فایل جستوجوی <<<<<<< بزنید تا مطمئن شوید نشانگری جا نمانده.
قدم چهارم: افزودن فایل به staging
حالا هر فایل حلشده را به staging اضافه کنید:
git add <file-path>
اگر همه فایلها حل شده، میتوانید از git add -A استفاده کنید. اما در پروژههای بزرگ، توصیه میکنم فایلبهفایل اضافه کنید تا اگر اشتباهی رخ داد، بتوانید دقیقاً بفهمید کجاست.
قدم پنجم: commit نهایی
پس از staging همه فایلها، commit را ثبت کنید:
git commit -m "merge: resolve conflicts in pricing logic"
اگر در وسط یک مرج هستید، گیت خودش پیام merge از پیش نوشته شده را ارائه میدهد. میتوانید همان را ویرایش کنید. توصیه میکنم در commit message ذکر کنید که این commit یک merge حلشده است، نه یک commit معمولی؛ این کار در تاریخچهٔ پروژه کمک زیادی میکند.
تعارض را نباید با «انتخاب سریع یکی از دو نسخه» حل کرد؛ باید با «ساختن نسخهای که هر دو منطق را در خود دارد» حل کرد. اولی سریع است، دومی درست.
ابزارها: از خط فرمان تا ادیتورهای بصری
گیت در خط فرمان، دقیق و قابلاتکاست؛ ولی برای تعارضهای پیچیده، ابزارهای بصری سرعت را چند برابر میکنند. سه دستهٔ اصلی:
ادیتورهای کد با قابلیت Git
VS Code، PHPStorm و Sublime Text، همه نمای رنگی برای نشانگرها دارند. VS Code از طریق افزونهٔ «GitLens» نمای inline میدهد که میگوید هر خط در چه commit و توسط چه کسی تغییر کرده. این قابلیت در تعارضهای پیچیده، نجاتدهنده است.
ابزارهای اختصاصی Git
ابزارهایی مثل GitKraken، Fork، Sourcetree و Sublime Merge، نمای سهستونه میدهند. در یک بلاک تعارض، هر سه نسخه را همزمان میبینید و با یک کلیک انتخاب میکنید. در پروژههایی که روزانه چند تعارض رخ میدهد، این ابزارها سرمایهگذاری سوددهیاند.
Merge Tool پیشفرض گیت
اگر از یک ابزار خط فرمان مثل vimdiff خوشتان میآید، گیت با دستور git mergetool آن را باز میکند. برای توسعهدهندگانی که در ترمینال زندگی میکنند، این سریعترین مسیر است.
انتخاب ابزار، به سبک کار شما بستگی دارد؛ ولی یک قاعدهٔ عمومی: در تعارضهای با بیش از پنج فایل یا بیش از سی بلاک، رفتن به سمت ابزار بصری همیشه تصمیم درستی است. خطای انسانی در حل تعارضهای بزرگ با خط فرمان، بالقوه بالاست. مطالعهٔ بیشتر روی این که هر ابزار چه میکند در دستورات پرکاربرد گیت آمده است.
تعارض در ریبیس چه تفاوتی با مرج دارد؟
ریبیس (Rebase) یکی از قدرتمندترین و در عین حال دردسرسازترین ابزارهای گیت است. تفاوت اساسیاش با مرج این است: در ریبیس، commit های شاخهٔ شما بازنویسی میشوند و بهجای merge commit، روی نوک شاخهٔ مقصد قرار میگیرند. نتیجهاش یک تاریخچهٔ خطی و تمیز است، اما هزینهاش این است که تعارضها ممکن است در هر commit بهصورت جداگانه ظاهر شوند.
سه تفاوت مهم که در پروژهها زیاد دیدهام:
- تکرار تعارضها: در مرج، هر فایل فقط یک بار تعارض میسازد. در ریبیس، اگر یک commit میانی با یک فایل تعارض داشت، همان تعارض ممکن است چند بار در طول ریبیس رخ دهد. ابزار
git rerere(مخفف REuse REcorded REsolution) میتواند این تکرار را کم کند؛ چون انتخابهای قبلی شما را بهخاطر میسپارد. - پیام commit متفاوت: در مرج، commit نهایی یک پیام «merge» دارد؛ در ریبیس، commit های شما با محتوای اصلی خودشان بازنویسی میشوند — که میتواند هم مزیت باشد و هم معایب.
- پاکسازی نهایی: در ریبیس، اگر یک commit میانی پس از حل تعارض ناقص بماند، تمام commit های بعدی هم آن نقص را حمل میکنند. به همین دلیل، ریبیس نیاز به دقت بیشتری در تست هر مرحله دارد.
قاعدهٔ کلی که در پروژهها رعایت میکنم: اگر شاخه شخصی است و merge commit های اضافی نمیخواهم، از ریبیس استفاده میکنم. اگر روی شاخهٔ مشترک با چند نفر کار میکنم، مرج انتخاب امنتری است چون تاریخچه را دستنخورده نگه میدارد. اگر درگیر تعارضهای پیچیده شدید، مسیر تشخیصی دقیقتری در رفع خطاهای رایج گیت آمده است. روش گامبهگام ریبیس هم در برنچ در گیت توضیح داده شده است.
پیشگیری: استراتژیهای کاهش تعارض
بهترین راهِ حل تعارض، رخ ندادن آن است. سه اصل که در تیمهایی که با آنها کار کردهام، رعایتشان تفاوت واضحی ساخته:
- commit های کوچک و منظم: هر تغییر منطقی را در یک commit جدا ثبت کنید. اگر commit ها کوچک باشند، تعارضها هم کوچکاند و حلشان سریع. اگر یک commit هشتساعت کار را نگه دارد، هر تعارضی روی آن، به یک بنبست تبدیل میشود. این اصل، از جنس همان انضباطی است که در اصول کدنویسی تمیز درباره اندازهٔ واحد کد گفتهام.
- pull مرتب از مخزن: اگر روزانه چند بار از مخزن اصلی تغییرات را بکشید، دایرهٔ واگرایی کوچک میماند و تعارضها کم. برعکس، اگر یک هفته بدون pull کار کنید، روز بازگشت به مخزن، باید با کوهی از تعارض دستوپنجه نرم کنید.
- تقسیم کار بر اساس فایل، نه بر اساس قابلیت: در تیمهای کوچک، اگر هر نفر روی فایلهای متفاوت کار کند، تعارضها بهطور چشمگیری کاهش مییابند. اگر دو نفر باید روی یک فایل کار کنند، قبل از شروع، مرزهای کاری را شفاف کنند.
یک راهحل مهندسی تکمیلی که در پروژههای بزرگ ارزش زیادی دارد: قواعد .gitattributes را تنظیم کنید تا فایلهای تولیدشده (مثل package-lock.json) که هر بار تغییر میکنند، بهطور خودکار مرج شوند — نه دستی. این کار جلوی بسیاری از تعارضهای روتین و بیمقدار را میگیرد.
اشتباهات رایج و خطرناک
سه اشتباه که در پروژههای واقعی دیدهام و هر کدام میتواند یک مشکل کوچک را به بحران تبدیل کند:
- حل تعارض بدون فهمیدن علت: اگر فقط نشانگرها را پاک کنید و یک نسخه را تصادفی نگه دارید، مشکل ظاهراً حل شده؛ ولی در واقع، منطق کد ناقص مانده و بعدها در قالب باگ ظاهر میشود. راه درست: پیش از هر انتخاب، commit message هر طرف را بخوانید.
- استفاده از
git checkout --theirs .یاgit checkout --ours .بهصورت سراسری: این دستور تمام فایلهای درگیر را به یکی از دو طرف برمیگرداند و تغییرات طرف دیگر را کاملاً از بین میبرد. اگر با عجله این کار را بکنید، ممکن است کار دو روز همکارتان را پاک کنید. فقط در شرایطی که مطمئنید یک طرف کاملاً نادرست است، از این دستور استفاده کنید — و پیش از آن، از شاخهٔ مقابل بکاپ بگیرید. - کامیت کردن نشانگرها: اگر پس از commit، در فایلها هنوز نشانگر
<<<<<<<وجود داشته باشد، کد پروژه در محیط تولید خطای نحوی میدهد. پیش از commit، حتماً یک بار جستوجوی سراسری روی این نشانگرها بزنید. ابزارهای خط فرمان گیت این را در پیام merge نشان میدهند، ولی چشم خودتان هم لازم است.
یک اشتباه چهارم که در پروژههای تازهکار رایجتر است: حل تعارض روی فایل اصلی main، بدون استفاده از شاخهٔ جدا. همیشه تعارض را در یک شاخهٔ جدا حل کنید و بعد در main ادغام کنید. این لایهٔ اضافی، در لحظهٔ فاجعه، همهچیز را نجات میدهد.
نگاه عمیق: چرا در پروژههای وردپرس تعارض بیشتر میشود؟
برای مهندسانی که در پروژههای تیمی وردپرس کار میکنند، این واقعیت را زیاد دیدهام: تعارضها در پروژههای وردپرس بهطور میانگین بیشتر از پروژههای وب عمومی است. سه دلیل برای این پدیده وجود دارد:
- files like
functions.phpوstyle.css، نقطهٔ ثقل تغییراتاند: در پروژههای وردپرس، توسعهدهندگان معمولاً ترجیح میدهند همهچیز را در یکfunctions.phpواحد بریزند. نتیجه: هر تغییر کوچکی در همان فایل بهروز میشود و هر مرج، احتمال تعارض روی همان فایل را دارد. راهحل: تقسیم منطقی کد به چند فایل باrequire_onceو نگهداشتن فایل اصلی بهعنوان فقط یک router. این الگو در ساختار فایلهای یک قالب استاندارد وردپرس توضیح داده شده است. - ادیتورهای بصری و ذخیرهسازیهای نواری (On-save) : بعضی افزونهها یا ادیتورها، در هر ذخیره، تمام فایل را دوباره مینویسند (مثلاً تغییر last-modified یا فرمتدهی خودکار). نتیجه: حتی وقتی تغییری در منطق کد ندادید، فایل بهنظر گیت «عوض شده» و در مرج، تعارض میسازد. حل: تنظیم دقیق فرمترهای خودکار یا استفاده از
.gitattributesبرای نرمالسازی نوع خطوط. - پروژههای چندزبانه و چندپوستهای: وقتی قالب شما روی چند زبان یا چند نسخه از یک محصول کار میکند، شاخههای بیشتری برای هر نسخه ایجاد میشود و ادغامشان پیچیدهتر است. در این پروژهها، استراتژی «feature branch» بهجای «version branch» انتخاب بهتری است؛ هر قابلیت در یک شاخه، و همه با یک استراتژی مرج واحد.
یک نکتهٔ ظریفتر از لایهٔ معماری: در پروژههایی که چند محیط (لوکال، استجینگ، پروداکشن) دارند، ترتیب مرج اهمیت دارد. اگر از لوکال به استجینگ، و از استجینگ به پروداکشن مرج کنید، دایرهٔ تعارض محدود میماند. اگر مستقیم از لوکال به پروداکشن مرج کنید، احتمال برخورد با تغییرات همکارانِ استجینگ، بالاست. این انضباط، از جنس همان رویکردی است که در توسعه وردپرس با محیط لوکال توضیح دادهام. اگر در تیم کار میکنید، رعایت ترتیب مرج را بهعنوان یک قاعدهٔ رسمی در مستندات تیم ثبت کنید — نتیجهاش در طول چند ماه، کاهش قابل توجه تعارضها و افزایش سرعت توسعه است.
یک توصیهٔ پایانی برای تیمها: اگر میخواهید بفهمید کدام فایلها بیشترین تعارض را میسازند، یک اسکریپت کوچک بنویسید که لاگ گیت را تحلیل کند. در بسیاری از تیمهایی که با آنها کار کردهام، دو یا سه فایل مسئول شصت درصد تعارضها بودهاند. وقتی آن فایلها را شناسایی کردید، میتوانید استراتژی مشخصی برایشان تعریف کنید — مثل مالک مشخص، شاخهٔ جدا، یا قواعد .gitattributes اختصاصی. این نگاه دادهمحور، از هر شعار دربارهٔ «کار تیمی بهتر» کاربردیتر است. ابزارهای سنجش این نوع داده در دستورات پرکاربرد گیت معرفی شدهاند و مسیر ساختاردهی این اطلاعات در چگونه یک پروژه توسعه وردپرس را ساختاربندی کنیم آمده است.
نقطهٔ پایانی این پرونده
تعارض گیت از آن دسته مشکلاتی است که در نگاه اول ترسناک به نظر میرسد، ولی پس از یک بار حل کردن درست، دیگر هرگز آن اضطراب اولیه را بازنمیگرداند. کلید اصلی، درست فهمیدن آناتومی نشانگرها و انتخاب آگاهانه بین نسخههاست — نه حذف کورکورانه. با commit های کوچک، pull های منظم، و ابزارهای بصری، هم تعارضها کمتر رخ میدهند و هم وقتی رخ میدهند، در چند دقیقه حل میشوند. اگر تجربهٔ جالبی از یک تعارض پیچیده دارید — مثلاً تعارضی که در فایلهای سریالایز شده یا lock file ها رخ داده و راهحل خلاقانهای برایش پیدا کردهاید — آن را در دیدگاه بنویسید. این تجربهها، برای توسعهدهندهٔ بعدی که در همان باتلاق افتاده، از هر راهنمای عمومی ارزشمندتر است. 🧩