رفتن به محتوا
LoopX

حل conflict

توی این درس یاد می‌گیری وقتی دو نفر یک جای فایل را عوض کرده‌اند و Git نمی‌داند کدام درست است (یک conflict)، آرام و قدم‌به‌قدم حلش کنی. علامت‌های <<<<<<< و ======= و >>>>>>> را می‌خوانی، فایل را درست می‌کنی، با git add و git commit ادغام را تمام می‌کنی، با git checkout --ours/--theirs یکی از دو طرف را برمی‌داری، و اگر پشیمان شدی با git merge --abort برمی‌گردی. تمرین: یک conflict عمدی بساز و حلش کن.

مسئله: Git هم نمی‌تواند حدس بزند

Section titled “مسئله: Git هم نمی‌تواند حدس بزند”

در درس قبل دیدی Git دو شاخه را خودکار ادغام می‌کند، به شرط اینکه جاهای مختلفی از فایل را عوض کرده باشند. ولی اگر هر دو یک خط را عوض کنند، یکی می‌گوید «قیمت ۶» و دیگری «قیمت ۷»؛ Git دلیلی ندارد یکی را ترجیح دهد. در این حالت متوقف می‌شود و می‌گوید «تو تصمیم بگیر». اسم این وضعیت conflict (تضاد) است؛ ترسناک نیست و نشانه‌ی خرابی هم نیست: فقط یعنی یک تصمیم انسانی لازم است.

تشبیه: دو ویراستار، یک جمله

Section titled “تشبیه: دو ویراستار، یک جمله”

دو ویراستار یک جمله‌ی کتاب را جداگانه بازنویسی کرده‌اند. سردبیر هر دو را کنار هم می‌بیند و باید تصمیم بگیرد: نسخه‌ی اول، نسخه‌ی دوم، یا ترکیبی. Git همان سردبیر است که فقط هر دو نسخه را نشان می‌دهد؛ انتخاب با توست. بخش‌هایی از متن که هر دو ویراستار دست نزده‌اند یا فقط یکی عوض کرده، بدون سؤال ادغام می‌شوند.

مراحل حل conflict: ادغام را شروع کن، Git در فایل‌های تضاددار علامت می‌گذارد، تو فایل را درست می‌کنی، add می‌کنی و commit می‌زنی. در هر لحظه می‌توانی با git merge --abort به قبل از ادغام برگردی.

فایل ساده‌ی منوی یک کافه با سه خط. از یک commit مشترک دو شاخه جدا می‌کنم و هر دو را تغییر می‌دهم:

مثال ۱: ساختن conflict و دیدن آن

Section titled “مثال ۱: ساختن conflict و دیدن آن”
Terminal window
mkdir -p ~/gitlab/cafe && cd ~/gitlab/cafe && git init -q
printf 'Name: Cafe\nCoffee: 5\nOpen: 9-17\n' > menu.txt
git add menu.txt && git commit -qm "Add menu"
git switch -qc feature/price
sed -i 's/^Coffee: 5/Coffee: 6/; s/^Open: 9-17/Open: 8-18/' menu.txt
git commit -qam "Raise coffee price and open earlier"
git switch -q main
sed -i 's/^Name: Cafe/Name: Cafe Loop/; s/^Coffee: 5/Coffee: 7/' menu.txt
git commit -qam "Rename cafe and set coffee price to 7"
echo "=== ادغام:"
git merge feature/price
echo "=== وضعیت:"
git status
خروجی
=== ادغام:
Auto-merging menu.txt
CONFLICT (content): Merge conflict in menu.txt
Automatic merge failed; fix conflicts and then commit the result.
=== وضعیت:
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: menu.txt
no changes added to commit (use "git add" and/or "git commit -a")

دو چیز را دقیق ببین: Git خودش همه‌ی ادغام‌های بی‌تضاد را انجام داد (Auto-merging: نام کافه از main و ساعت کار از feature آمد)، و فقط روی یک خط (Coffee) متوقف شد. git status حالا فایل را زیر Unmerged paths و با both modified نشان می‌دهد؛ یعنی «هر دو طرف عوضش کرده‌اند». و ادغام نیمه‌کاره است: نه تمام شده و نه لغو.

مثال ۲: خواندن علامت‌ها

Section titled “مثال ۲: خواندن علامت‌ها”

Git داخل خود فایل علامت گذاشته. فایل را ببین:

Terminal window
cd ~/gitlab/cafe
cat menu.txt
خروجی
<<<<<<< HEAD
Name: Cafe Loop
Coffee: 7
Open: 9-17
=======
Name: Cafe
Coffee: 6
Open: 8-18
>>>>>>> feature/price

علامت‌ها این‌طورند:

بخش معنی
<<<<<<< HEAD شروع قسمت تضاددار؛ از این‌جا تا =======، نسخه‌ی شاخه‌ی فعلی (HEAD، همان main)
======= جداکننده‌ی دو نسخه
>>>>>>> feature/price از ======= تا این‌جا، نسخه‌ی شاخه‌ی ادغام‌شده (feature/price)

بخش‌های بدون علامت (Name و Open) همان‌هایی‌اند که Git خودکار ادغام کرد. تو باید این سه بخش (دو نسخه و علامت‌ها) را با یک نسخه‌ی نهایی عوض کنی. دستور git diff هم در این وضعیت یک شکل ویژه نشان می‌دهد (diff ترکیبی با دو ستون علامت، یکی برای هر والد):

Terminal window
cd ~/gitlab/cafe
git diff
خروجی
diff --cc menu.txt
index 9ae8221,827f826..0000000
--- a/menu.txt
+++ b/menu.txt
@@@ -1,3 -1,3 +1,9 @@@
++<<<<<<< HEAD
+Name: Cafe Loop
+Coffee: 7
+Open: 9-17
++=======
+ Name: Cafe
+ Coffee: 6
+ Open: 8-18
++>>>>>>> feature/price

مثال ۳: حل دستی، edit ← add ← commit

Section titled “مثال ۳: حل دستی، edit ← add ← commit”

حالا تصمیم می‌گیرم: قیمت را ۶٫۵ می‌گذارم (بین دو پیشنهاد) و ساعت کار و نام کافه همان ادغام‌شده. در واقعیت فایل را با ویرایشگر باز می‌کنی و علامت‌ها را پاک می‌کنی؛ این‌جا برای اینکه قابل‌اجرا باشد، محتوای نهایی فایل را می‌نویسم:

Terminal window
cd ~/gitlab/cafe
cat > menu.txt <<'EOF'
Name: Cafe Loop
Coffee: 6.5
Open: 8-18
EOF
echo "--- بعد از حل، فایل:"
cat menu.txt
git add menu.txt
echo "--- وضعیت بعد از add (حل شد، منتظر commit):"
git status | head -6
git commit --no-edit
echo "--- گراف:"
git log --oneline --graph
خروجی
--- بعد از حل، فایل:
Name: Cafe Loop
Coffee: 6.5
Open: 8-18
--- وضعیت بعد از add (حل شد، منتظر commit):
On branch main
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: menu.txt
[main dc4ad5a] Merge branch 'feature/price'
--- گراف:
* dc4ad5a Merge branch 'feature/price'
|\
| * 4f25ba3 Raise coffee price and open earlier
* | 488713f Rename cafe and set coffee price to 7
|/
* 9476ac5 Add menu

سه قدم: ۱) فایل را درست کن (فقط متن نهایی؛ هیچ علامتی نماند)، ۲) git add فایل یعنی «این فایل حل شد»، ۳) git commit ادغام را تمام می‌کند (--no-edit پیام پیش‌فرض Merge branch... را نگه می‌دارد؛ می‌توانی git merge --continue هم بزنی). حالا commit ادغام با دو والد ساخته شد.

نتیجه‌ی حل conflict: commit ادغام M دو والد دارد و حاوی نسخه‌ی نهایی‌ای است که تو انتخاب کردی.

مثال ۴: پشیمان شدی؟ git merge --abort

Section titled “مثال ۴: پشیمان شدی؟ git merge --abort”

وسط حل، اگر گیج شدی یا فهمیدی ادغام را در لحظه‌ی بد شروع کرده‌ای، یک دستور همه‌چیز را به قبل از merge برمی‌گرداند:

Terminal window
cd ~/gitlab/cafe
git switch -qc feature/menu2
sed -i 's/^Coffee: 6.5/Coffee: 8/' menu.txt
git commit -qam "Coffee price 8 on feature"
git switch -q main
sed -i 's/^Coffee: 6.5/Coffee: 9/' menu.txt
git commit -qam "Coffee price 9 on main"
echo "=== ادغام با conflict:"
git merge feature/menu2 2>&1 | tail -2
git status -s
echo "=== لغو:"
git merge --abort
git status -s
echo "(خالی: همه‌چیز مثل قبل از merge)"
cat menu.txt
خروجی
=== ادغام با conflict:
CONFLICT (content): Merge conflict in menu.txt
Automatic merge failed; fix conflicts and then commit the result.
UU menu.txt
=== لغو:
(خالی: همه‌چیز مثل قبل از merge)
Name: Cafe Loop
Coffee: 9
Open: 8-18

--abort فقط تا قبل از commit کردن ادغام کار می‌کند و دقیقاً همان وضعیت قبل از git merge را برمی‌گرداند؛ هیچ ردی نمی‌ماند. بی‌خطرترین راه برای «بیا دوباره شروع کنیم» است.

مثال ۵: یک طرف را کامل برداشتن، --ours و --theirs

Section titled “مثال ۵: یک طرف را کامل برداشتن، --ours و --theirs”

گاهی نیازی به ترکیب نیست؛ فقط می‌خواهی نسخه‌ی یکی از دو طرف را بپذیری. در ادغام، ours شاخه‌ی فعلی (جایی که merge را زدی) و theirs شاخه‌ی ادغام‌شده است:

Terminal window
cd ~/gitlab/cafe
git merge feature/menu2 2>&1 | tail -1
echo "--- نسخه‌ی خودم (ours = main):"
git checkout --ours menu.txt
grep Coffee menu.txt
echo "--- نسخه‌ی آن‌ها (theirs = feature/menu2):"
git checkout --theirs menu.txt
grep Coffee menu.txt
git add menu.txt
git commit -q --no-edit
git log --oneline -1
خروجی
Automatic merge failed; fix conflicts and then commit the result.
--- نسخه‌ی خودم (ours = main):
Updated 1 path from the index
Coffee: 9
--- نسخه‌ی آن‌ها (theirs = feature/menu2):
Updated 1 path from the index
Coffee: 8
c88049e Merge branch 'feature/menu2'

git checkout --ours فایل / --theirs فایل فایل را کاملاً با نسخه‌ی آن طرف عوض می‌کند (دستور هم‌ارز جدید: git restore --ours/--theirs فایل). بعدش مثل همیشه add و commit. هشدار: این همه‌ی فایل را از یک طرف می‌گیرد، نه فقط بخش تضاددار (تغییرهای بی‌تضاد آن طرف دیگر هم از دست می‌رود). برای انتخاب فقط در بخش‌های تضاددار، گزینه‌ی استراتژی git merge -X ours یا -X theirs هست؛ ولی بی‌صدا تصمیم می‌گیرد و خطرناک است، پس فقط وقتی ازش مطمئنی.

مثال ۶: انواع conflict، وقتی یک طرف فایل را حذف کرده

Section titled “مثال ۶: انواع conflict، وقتی یک طرف فایل را حذف کرده”

همه‌ی conflict ها «دو نفر یک خط» نیستند. اگر یکی فایل را ویرایش کند و دیگری پاک، Git نمی‌داند فایل باید بماند یا برود:

Terminal window
mkdir -p ~/gitlab/del && cd ~/gitlab/del && git init -q
echo "v1" > notes.txt && git add notes.txt && git commit -qm "Add notes"
git switch -qc edit
echo "v2 edited" > notes.txt && git commit -qam "Edit notes"
git switch -q main
git rm -q notes.txt && git commit -qm "Delete notes"
echo "=== ادغام:"
git merge edit 2>&1 | tail -3
echo "=== وضعیت (DU = من حذف کرده‌ام، آن‌ها ویرایش):"
git status -s
echo "=== تصمیم: فایل را نگه می‌دارم (نسخه‌ی ویرایش‌شده):"
git add notes.txt
git commit -q --no-edit
ls; cat notes.txt
خروجی
=== ادغام:
CONFLICT (modify/delete): notes.txt deleted in HEAD and modified in edit. Version edit of notes.txt left in tree.
Automatic merge failed; fix conflicts and then commit the result.
=== وضعیت (DU = من حذف کرده‌ام، آن‌ها ویرایش):
DU notes.txt
=== تصمیم: فایل را نگه می‌دارم (نسخه‌ی ویرایش‌شده):
notes.txt
v2 edited

حروف DU را از چپ بخوان: ستون اول وضعیت ours (D = حذف کرده‌ایم)، ستون دوم theirs (U = updated: آن‌ها ویرایش کرده‌اند). تصمیم تو: اگر می‌خواهی فایل برود git rm notes.txt، اگر می‌خواهی بماند git add notes.txt. حروف دیگر: UU هر دو ویرایش، AA هر دو فایلی با همین نام ساخته‌اند، UD برعکس DU.

مثال ۷: دیدن «نسخه‌ی اصلی» با diff3 و zdiff3

Section titled “مثال ۷: دیدن «نسخه‌ی اصلی» با diff3 و zdiff3”

علامت‌های پیش‌فرض فقط دو نسخه را نشان می‌دهند. گاهی برای تصمیم‌گیری باید بدانی «اول چه بود». با merge.conflictStyle یک بخش سوم هم می‌آید: نسخه‌ی جدّ مشترک (base):

Terminal window
cd ~/gitlab/cafe
git config merge.conflictStyle zdiff3
git switch -qc feature/zd
sed -i 's/^Coffee: 8/Coffee: 12/' menu.txt
git commit -qam "Coffee 12 on feature"
git switch -q main
sed -i 's/^Coffee: 8/Coffee: 10/' menu.txt
git commit -qam "Coffee 10 on main"
git merge feature/zd 2>&1 | tail -1
cat menu.txt
git merge --abort
خروجی
Automatic merge failed; fix conflicts and then commit the result.
Name: Cafe Loop
<<<<<<< HEAD
Coffee: 10
||||||| c88049e
Coffee: 8
=======
Coffee: 12
>>>>>>> feature/zd
Open: 8-18

حالا بین دو علامت اصلی یک خط ||||||| با نسخه‌ی قبل از هر دو تغییر (Coffee: 8) هم هست. با دیدن اینکه از چه عددی شروع شد، معلوم می‌شود هر نفر چه کرده (یکی ۸ را به ۱۰ برده و دیگری به ۱۲). برای همیشه روشنش کن: git config --global merge.conflictStyle zdiff3 (از Git ۲.۳۵ به بعد؛ diff3 قدیمی‌تر است ولی همان کار را می‌کند). بسیاری از حرفه‌ای‌ها همین را پیش‌فرض می‌کنند.

ویرایش دستی علامت‌ها همیشه لازم نیست. دو ابزار رایج:

  • git mergetool: یک ابزار گرافیکی ادغام (meld، kdiff3، vimdiff، VS Code…) باز می‌کند که سه ستون (ours، base، theirs) و یک نتیجه نشان می‌دهد. فهرست ابزارهایی که روی این ماشین هستند:
Terminal window
git mergetool --tool-help 2>&1 | head -12
خروجی
'git mergetool --tool=<tool>' may be set to one of the following:
vimdiff Use Vim with a custom layout (see `git help mergetool`'s `BACKEND SPECIFIC HINTS` section)
vimdiff1 Use Vim with a 2 panes layout (LOCAL and REMOTE)
vimdiff2 Use Vim with a 3 panes layout (LOCAL, MERGED and REMOTE)
vimdiff3 Use Vim where only the MERGED file is shown
The following tools are valid, but not currently available:
araxis Use Araxis Merge (requires a graphical session)
bc Use Beyond Compare (requires a graphical session)
bc3 Use Beyond Compare (requires a graphical session)
bc4 Use Beyond Compare (requires a graphical session)
codecompare Use Code Compare (requires a graphical session)
  • ویرایشگرهای مدرن (مثل VS Code و JetBrains) دکمه‌های «Accept Current / Accept Incoming / Accept Both» بالای هر بخش تضاددار دارند و ویرایشگر ادغام سه‌ستونه. این را من اجرا نکرده‌ام (رابط گرافیکی دارد)؛ ولی کار همان است که دستی کردیم: نسخه‌ی نهایی را بنویس، git add و git commit.

مثال ۹: یادآور حل‌ها، rerere

Section titled “مثال ۹: یادآور حل‌ها، rerere”

وقتی یک conflict را حل می‌کنی و بعداً همان را دوباره می‌بینی (مثلاً ادغام را لغو می‌کنی و دوباره شروع می‌کنی، یا در rebase)، دوباره حلش کردن خسته‌کننده است. rerere (reuse recorded resolution) راه‌حلت را به‌خاطر می‌سپارد:

Terminal window
mkdir -p ~/gitlab/rr && cd ~/gitlab/rr && git init -q
git config rerere.enabled true
echo "price: 5" > p.txt && git add p.txt && git commit -qm "Base"
git switch -qc feature && echo "price: 6" > p.txt && git commit -qam "Feature price"
git switch -q main && echo "price: 7" > p.txt && git commit -qam "Main price"
echo "=== بار اول: conflict، و حل دستی:"
git merge feature 2>&1 | tail -1
echo "price: 6.5" > p.txt && git add p.txt && git commit -q --no-edit
echo "=== ادغام را برمی‌گردانم (فقط برای آزمایش) و دوباره می‌زنم:"
git reset -q --hard HEAD~1
git merge feature 2>&1 | tail -3
echo "--- فایل (Git خودش حل را دوباره اعمال کرد، ولی هنوز staged نیست):"
cat p.txt
git status -s
خروجی
=== بار اول: conflict، و حل دستی:
Automatic merge failed; fix conflicts and then commit the result.
Recorded resolution for 'p.txt'.
=== ادغام را برمی‌گردانم (فقط برای آزمایش) و دوباره می‌زنم:
CONFLICT (content): Merge conflict in p.txt
Resolved 'p.txt' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.
--- فایل (Git خودش حل را دوباره اعمال کرد، ولی هنوز staged نیست):
price: 6.5
UU p.txt

Resolved 'p.txt' using previous resolution: Git حل قبلی‌ات را اعمال کرد (باید فقط git add و commit بزنی). برای فعال‌کردن دائمی: git config --global rerere.enabled true. در تیم‌هایی که زیاد rebase می‌کنند خیلی کمک می‌کند.

پشت پرده: Git conflict را چطور تشخیص می‌دهد؟

Section titled “پشت پرده: Git conflict را چطور تشخیص می‌دهد؟”

ادغام سه‌طرفه سه نسخه از هر فایل را مقایسه می‌کند: base (جدّ مشترک)، ours و theirs. اگر فقط یک طرف نسبت به base عوض کرده، آن را می‌پذیرد؛ اگر هر دو یک جای متن را عوض کرده باشند (و متفاوت)، conflict. در حالت تضاد، Git سه نسخه‌ی فایل را در ناحیه‌ی staging نگه می‌دارد (slot های ۱، ۲ و ۳) تا بتوانی هر کدام را بخوانی. ببین:

Terminal window
mkdir -p ~/gitlab/stages && cd ~/gitlab/stages && git init -q
echo "x: 1" > f.txt && git add f.txt && git commit -qm "Base"
git switch -qc b && echo "x: 2" > f.txt && git commit -qam "B"
git switch -q main && echo "x: 3" > f.txt && git commit -qam "A"
git merge b > /dev/null 2>&1
echo "--- ناحیه‌ی staging در حال conflict (۱=base، ۲=ours، ۳=theirs):"
git ls-files -u
echo "--- محتوای هر سه:"
for n in 1 2 3; do printf "stage $n: "; git show :$n:f.txt; done
خروجی
--- ناحیه‌ی staging در حال conflict (۱=base، ۲=ours، ۳=theirs):
100644 d508cf756344aaf9123c531122a9d2d410c4ecfa 1 f.txt
100644 e5cfddc4f7c407e08d3b6ba2bfdeec91b849c780 2 f.txt
100644 ad932ccf517a09d23b7b892d6ee994bebdb7e8da 3 f.txt
--- محتوای هر سه:
stage 1: x: 1
stage 2: x: 3
stage 3: x: 2

عدد بعد از حالت (۱ و ۲ و ۳) شماره‌ی slot است: ۱ = base، ۲ = ours، ۳ = theirs؛ و git show :۲:فایل هر کدام را چاپ می‌کند. بعد از git add، این سه slot با یک نسخه‌ی حل‌شده (slot ۰) جایگزین می‌شود و Git می‌داند حل شد. به همین دلیل git add همان «علامت حل‌شدن» است.

دستور کار
git merge شاخه شروع ادغام (ممکن است conflict بدهد)
git status / git status -s فایل‌های تضاددار (UU، DU، UD، AA)
git diff diff ترکیبی در حال conflict
git add فایل فایل حل شد
git commit / git merge --continue پایان ادغام
git merge --abort لغو و برگشت به قبل از merge
git checkout --ours فایل / --theirs نسخه‌ی یک طرف
git config merge.conflictStyle zdiff3 نمایش base هم
git config rerere.enabled true یادآور حل‌ها
git diff --check یافتن علامت‌های conflict جامانده
git mergetool ابزار گرافیکی
وضعیت معنی
UU هر دو ویرایش کرده‌اند (تضاد محتوا)
AA هر دو فایلی با همین نام ساخته‌اند
DU ما حذف کرده‌ایم، آن‌ها ویرایش
UD ما ویرایش کرده‌ایم، آن‌ها حذف
Terminal window
mkdir -p ~/gitlab/early && cd ~/gitlab/early && git init -q
echo "n: 1" > f.txt && git add f.txt && git commit -qm "Base"
git switch -qc b && echo "n: 2" > f.txt && git commit -qam "B"
git switch -q main && echo "n: 3" > f.txt && git commit -qam "A"
git merge b > /dev/null 2>&1
git commit -m "try to commit" 2>&1 | head -4
خروجی
error: Committing is not possible because you have unmerged files.
hint: Fix them up in the work tree, and then use 'git add/rm <file>'
hint: as appropriate to mark resolution and make a commit.
fatal: Exiting because of an unresolved conflict.

Committing is not possible because you have unmerged files: Git جلوی commit را می‌گیرد تا conflict ناتمام ثبت نشود. راه‌حل: فایل را حل و git add کن.

۲) علامت‌ها در فایل جا می‌مانند

Section titled “۲) علامت‌ها در فایل جا می‌مانند”

بدترین حالت: علامت‌ها را پاک نمی‌کنی، git add می‌زنی (که فقط «حل شد» را علامت می‌زند) و commit می‌کنی. Git هیچ‌چیز نمی‌گوید و فایلی با <<<<<<< وارد تاریخچه می‌شود (و کد یا تنظیمات می‌شکند). راه گرفتنش git diff --check است:

Terminal window
cd ~/gitlab/early
git add f.txt
echo "--- علامت‌ها هنوز در فایل‌اند؛ git diff --check چه می‌گوید؟"
git diff --cached --check; echo "کد خروج: $?"
echo "--- و جستجوی دستی:"
grep -n '^[<=>]\{7\}' f.txt
git merge --abort
خروجی
--- علامت‌ها هنوز در فایل‌اند؛ git diff --check چه می‌گوید؟
f.txt:1: leftover conflict marker
f.txt:3: leftover conflict marker
f.txt:5: leftover conflict marker
کد خروج: 2
--- و جستجوی دستی:
1:<<<<<<< HEAD
3:=======
5:>>>>>>> b

git diff --check (برای staging: --cached) خط‌هایی با علامت conflict و فاصله‌ی اضافه را گزارش می‌دهد و کد خروج غیرصفر برمی‌گرداند. قبل از commit آن را بزن (یا در یک pre-commit hook بگذار). و بعد از هر حل، پروژه را اجرا و تست کن؛ فایلی که علامت ندارد ولی منطقش اشتباه ادغام شده هم ممکن است.

۳) انتخاب کورکورانه‌ی یک طرف

Section titled “۳) انتخاب کورکورانه‌ی یک طرف”

--ours یا -X theirs را بدون خواندن می‌زنی و کار همکارت بی‌صدا از بین می‌رود. راه‌حل: هر conflict را با git diff بخوان؛ اگر نمی‌دانی کدام درست است، از همکارت بپرس.

۴) حل conflict روی شاخه‌ی اشتباه

Section titled “۴) حل conflict روی شاخه‌ی اشتباه”

مثل هر merge، قبل از شروع ببین روی کدام شاخه‌ای (git branch). ادغام در جهت اشتباه، ours و theirs را عوض می‌کند و ممکن است نسخه‌ی اشتباه برنده شود.

اگر وسط حل گیر کردی، git merge --abort بدون هیچ ضرری برمی‌گرداند. بعد با خیال راحت دوباره شروع کن.

✎ تمرینآسان

یک conflict بساز و بلافاصله با git merge --abort آن را لغو کن؛ ثابت کن مخزن به حالت قبل برگشت.

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex1 && cd ~/gitlab/ex1 && git init -q
echo "a" > f && git add f && git commit -qm "Base"
git switch -qc x && echo "b" > f && git commit -qam "X"
git switch -q main && echo "c" > f && git commit -qam "Y"
git merge x 2>&1 | tail -1
git status -s
git merge --abort
git status -s; echo "(خالی)"
cat f
خروجی
Automatic merge failed; fix conflicts and then commit the result.
UU f
(خالی)
c
✎ تمرینمتوسط

تمرین اصلی: یک conflict عمدی بساز و حلش کن. فایل config.ini با خط timeout=30 را در دو شاخه به دو مقدار متفاوت عوض کن و در main ادغام کن؛ مقدار نهایی را (timeout=45) خودت انتخاب کن و ادغام را تمام کن. ثابت کن در فایل هیچ علامتی نمانده.

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex2 && cd ~/gitlab/ex2 && git init -q
printf 'name=app\ntimeout=30\n' > config.ini && git add config.ini && git commit -qm "Add config"
git switch -qc feature/fast && sed -i 's/timeout=30/timeout=10/' config.ini && git commit -qam "Shorter timeout"
git switch -q main && sed -i 's/timeout=30/timeout=60/' config.ini && git commit -qam "Longer timeout"
git merge feature/fast 2>&1 | tail -2
echo "--- حل: مقدار نهایی را من می‌نویسم"
printf 'name=app\ntimeout=45\n' > config.ini
git add config.ini && git commit -q --no-edit
echo "--- بررسی علامت‌ها:"
grep -c '^[<=>]\{7\}' config.ini || echo "هیچ علامتی نمانده"
cat config.ini
git log --oneline --graph
خروجی
CONFLICT (content): Merge conflict in config.ini
Automatic merge failed; fix conflicts and then commit the result.
--- حل: مقدار نهایی را من می‌نویسم
--- بررسی علامت‌ها:
0
هیچ علامتی نمانده
name=app
timeout=45
* 1406b70 Merge branch 'feature/fast'
|\
| * 2558f81 Shorter timeout
* | cba8e25 Longer timeout
|/
* 7c9a0c0 Add config
✎ تمرینسخت

یک conflict از نوع حذف و ویرایش بساز (یک شاخه فایل report.txt را ویرایش می‌کند، دیگری پاکش می‌کند) و آن را طوری حل کن که فایل پاک شود (تصمیم: دیگر لازم نیست). با git status -s نوع conflict را نشان بده و در پایان ثابت کن فایل در پروژه نیست.

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex3 && cd ~/gitlab/ex3 && git init -q
echo "draft" > report.txt && echo keep > keep.txt && git add . && git commit -qm "Add files"
git switch -qc edit && echo "draft v2" > report.txt && git commit -qam "Edit report"
git switch -q main && git rm -q report.txt && git commit -qm "Remove report"
git merge edit 2>&1 | tail -2
echo "--- نوع conflict:"
git status -s
echo "--- تصمیم: حذف (git rm)"
git rm -q report.txt
git commit -q --no-edit
ls
git log --oneline --graph
خروجی
CONFLICT (modify/delete): report.txt deleted in HEAD and modified in edit. Version edit of report.txt left in tree.
Automatic merge failed; fix conflicts and then commit the result.
--- نوع conflict:
DU report.txt
--- تصمیم: حذف (git rm)
keep.txt
* cdc55ef Merge branch 'edit'
|\
| * df6ab19 Edit report
* | 782d90b Remove report
|/
* 3fccb08 Add files
⚡ بررسی سریع

در علامت‌های conflict، بخش بین «<<<<<<< HEAD» و «=======» مال کدام طرف است؟

؟ آزمونک
  1. Git چه زمانی conflict می‌دهد؟

  2. بعد از ویرایش فایل و پاک‌کردن علامت‌ها، چه باید کرد؟

  3. git merge --abort چه می‌کند؟

  4. در ادغام، «ours» و «theirs» چیست؟

  5. علامت‌های conflict را جا گذاشته‌ای و add و commit زده‌ای. چطور قبل از این اتفاق می‌گیری‌اش؟

  • conflict یعنی دو شاخه یک بخش از یک فایل را متفاوت عوض کرده‌اند؛ Git بقیه را خودکار ادغام می‌کند و فقط همین بخش را به تو می‌سپارد.
  • فایل علامت دارد: <<<<<<< HEAD (شاخه‌ی فعلی) ======= (جداکننده) >>>>>>> نام (شاخه‌ی ادغام‌شده). با merge.conflictStyle=zdiff3 نسخه‌ی base هم می‌آید.
  • حل: فایل را درست کن (هیچ علامتی نماند) ← git add فایل ← git commit (یا git merge --continue).
  • هر لحظه می‌توانی با git merge --abort برگردی. یک طرف را کامل برداشتن: git checkout --ours/--theirs فایل (در ادغام: ours = شاخه‌ی فعلی).
  • conflict ها فقط «دو نفر یک خط» نیستند: UU، AA، DU، UD (حذف و ویرایش).
  • git diff --check علامت‌های جامانده را می‌گیرد؛ بعد از حل، تست کن. rerere حل‌های تکراری را به‌خاطر می‌سپارد.
  • برای کم‌کردن conflict: ادغام‌های کوچک و مکرر، شاخه‌های کوتاه‌عمر، و هماهنگی با همکار.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
git merge شاخهشروع ادغام
git statusفایل‌های unmerged را ببین
git diffdiff ترکیبی conflict
(ویرایش فایل و حذف علامت‌ها)نسخه‌ی نهایی
git add فایلعلامت: حل شد
git commit / git merge --continueپایان ادغام
git merge --abortلغو ادغام
git checkout --ours فایل / --theirsنسخه‌ی یک طرف
git config merge.conflictStyle zdiff3نمایش base
git diff --checkعلامت‌های جامانده
git config rerere.enabled trueیادآور حل‌ها