Первый раз увидеть в файле строчки
Код: Выделить всё
<<<<<<< HEADКод: Выделить всё
>>>>>>> featureВспомни три дерева из прошлых уроков и устройство merge. Когда Git сливает две ветки, он берёт их общего предка (merge base) и смотрит, что изменилось в каждой ветке относ��тельно базы. Если ты в одной ветке поправил функцию авторизации, а в другой - футер сайта, Git спокойно сольёт оба изменения сам: они не пересекаются. Это автослияние, и оно происходит в 95% случаев незаметно.
Git конфликт merge возникает ровно тогда, когда обе ветки тронули один и тот же кусок одного файла по-разному. База говорит "тут было A", ветка слева говорит "стало B", ветка справа говорит "стало C". У Git нет правила, что важнее - B или C. Любой автоматический выбор будет угадыванием, а угадывать в твоём коде он не имеет права. Поэтому он останавливается, расставляет маркеры и передаёт решение тебе - единственному, кто знает, чего хотел автор.

Анатомия маркеров: что Git вписал в твой файл
Разберём реальный merge conflict. Есть ветка main и ветка feature, обе поправили одну строку приветствия в файле app.py. Делаем слияние:
Код: Выделить всё
$ git merge feature
Auto-merging app.py
CONFLICT (content): Merge conflict in app.py
Automatic merge failed; fix conflicts and then commit the result.
Код: Выделить всё
def greet(name):
<<<<<<< HEAD
return f"Привет, {name}! Добро пожаловать."
=======
return f"Здравствуйте, {name}. Рады видеть вас."
>>>>>>> feature
Код: Выделить всё
<<<<<<< HEADКод: Выделить всё
=======Код: Выделить всё
=======Код: Выделить всё
>>>>>>> featureКод: Выделить всё
<Код: Выделить всё
=Код: Выделить всё
>Запомни мнемонику навсегда: ours (наше) - это всегда верхний блок, сторона HEAD; theirs (их) - нижний блок, входящая сторона. Эти же слова всплывут в
Код: Выделить всё
--oursКод: Выделить всё
--theirsdiff3 и zdiff3: покажи базу и перестань гадать
Стандартный стиль merge показывает только две стороны - наше и их. И это его слабость. Глядя на B и C, ты часто не понимаешь, КТО ЧТО менял. Может, левая сторона вообще не трогала строку, а правая её переписала? Или обе переписали с нуля? Без точки отсчёта ты решаешь конфликт вслепую.
Включи стиль, который показывает merge base - общего предка. Их два: diff3 (старый) и zdiff3 (умнее, "зеленый" zealous diff3, доступен с Git 2.35). На 2026 правильный выбор - zdiff3. Ставим глобально:
Код: Выделить всё
$ git config --global merge.conflictStyle zdiff3
Код: Выделить всё
def greet(name):
<<<<<<< HEAD
return f"Привет, {name}! Добро пожаловать."
||||||| merge base
return f"Hello, {name}."
=======
return f"Здравствуйте, {name}. Рады видеть вас."
>>>>>>> feature
Код: Выделить всё
|||||||Код: Выделить всё
=======Чем zdiff3 лучше обычного diff3? Он "зеленее" в смысле выноса общих строк за пределы конфликта. Если внутри конфликтного блока у наших и у них совпадают первые и последние строки, zdiff3 вынесет совпадающие части НАРУЖУ конфликта, оставив внутри только реально расходящийся кусок. Это резко сокращает размер блока, который надо читать глазами. На больших конфликтах разница огромна: вместо двадцати строк маркеров ты видишь три спорные. Для новичка diff3/zdiff3 - это переход от "что за иероглифы" к "а, понятно". Включай и не думай.
git status и git diff во время конфликта - твои глаза
Пока конфликт не разрешён, репозиторий в особом состоянии - "merging". Не паникуй и не закрывай терминал. Первым делом спроси Git, где именно больно:
Код: Выделить всё
$ git status
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: app.py
no changes added to commit (yet)
Теперь
Код: Выделить всё
git diffКод: Выделить всё
$ git diff
diff --cc app.py
index 1a2b3c4,4d5e6f7..0000000
--- a/app.py
+++ b/app.py
@@@ -1,3 -1,3 +1,7 @@@
def greet(name):
++<<<<<<< HEAD
+ return f"Привет, {name}! Добро пожаловать."
++=======
+ return f"Здравствуйте, {name}. Рады видеть вас."
++>>>>>>> feature
Код: Выделить всё
+Код: Выделить всё
-Код: Выделить всё
git diff --checkКак разрешить конфликт git руками: правка, add, commit
Цикл разрешения всегда один и тот же, заучи его как таблицу умножения.
Шаг 1. Отредактируй файл. Открой app.py и СОТРИ все маркеры
Код: Выделить всё
<<<<<<<Код: Выделить всё
|||||||Код: Выделить всё
=======Код: Выделить всё
>>>>>>>Код: Выделить всё
def greet(name):
return f"Здравствуйте, {name}. Рады видеть вас!"
Код: Выделить всё
git add app.pyКод: Выделить всё
git statusШаг 3. Заверши слияние. Когда все конфликтные файлы добавлены:
Код: Выделить всё
$ git commit
Код: Выделить всё
git mergeКод: Выделить всё
git commitКод: Выделить всё
git rebase --continueКод: Выделить всё
git cherry-pick --continue--ours, --theirs и mergetool: когда руками неудобно
Иногда редактировать вручную глупо. Классика - бинарник или сгенерированный файл (картинка, package-lock.json, скомпилированный ассет). Там нет "строк", чтобы слить - нужно целиком взять одну из версий. Для этого есть
Код: Выделить всё
git checkoutКод: Выделить всё
git restoreКод: Выделить всё
$ git restore --ours logo.png # берём версию из нашей ветки (HEAD)
$ git restore --theirs logo.png # берём версию из входящей ветки
$ git add logo.png
Код: Выделить всё
git checkout --ours logo.pngКод: Выделить всё
--oursКод: Выделить всё
--theirsКогда конфликтов много и хочется трёхпанельный визуальный разбор (наше | база | их), зови git mergetool:
Код: Выделить всё
$ git mergetool
Merging:
app.py
Normal merge conflict for 'app.py':
{local}: modified file
{remote}: modified file
Код: Выделить всё
git config --global merge.tool meldКод: Выделить всё
.origКод: Выделить всё
mergetool.keepBackup falsegit rerere: Git запоминает, как ты решал, и повторяет сам
Вот это - приём уровня профи, и он реально экономит часы. rerere расшифровывается как reuse recorded resolution - "переиспользуй записанное разрешение". Идея: когда ты разрешаешь конфликт, Git запоминает ПАРУ "вот такой конфликт -> вот такое разрешение". Если ровно тот же конфликт встретится снова - Git применит твоё решение АВТОМАТИЧЕСКИ.
Зачем это нужно? Два частых сценария. Первый - долгий rebase ветки на постоянно двигающийся main: ты перебазируешь снова и снова, и КАЖДЫЙ раз ловишь один и тот же конфликт в одном файле. Без rerere ты решаешь его вручную пять раз. С rerere - один раз, дальше Git делает сам. Второй - ты сливаешь, потом откатываешь слияние, потом сливаешь заново (так бывает при тесте интеграции): rerere помнит твоё решение.
Включается одной строкой:
Код: Выделить всё
$ git config --global rerere.enabled true
Код: Выделить всё
$ git rebase main
...
CONFLICT (content): Merge conflict in app.py
Recorded preimage for 'app.py'
Код: Выделить всё
CONFLICT (content): Merge conflict in app.py
Resolved 'app.py' using previous resolution.
Код: Выделить всё
.git/rr-cacheКод: Выделить всё
git rerere statusКод: Выделить всё
git rerere diffКод: Выделить всё
git rerere forget app.pyАварийный выход и профилактика конфликтов
Главная кнопка спасения -
Код: Выделить всё
git merge --abortКод: Выделить всё
git rebase --abortКод: Выделить всё
git cherry-pick --abortКод: Выделить всё
$ git merge --abort # передумал - вернулись как было
$ git merge --continue # то же, что commit, но проверит, что unmerged не осталось
- Сливай часто, мелкими порциями. Чем дольше ветка живёт в стороне, тем сильнее она расходится с main и тем больнее финальный merge. Регулярный (или rebase) в свою ветку держит расхождение маленьким.
Код: Выделить всё
git merge main - Не форматируй чужой код "заодно". Массовое переформатирование, смена отступов, перенос строк рождают конфликты на ровном месте, потому что трогают строки, которые логически никто не менял.
- Договоритесь о границах в команде: кто за какой модуль отвечает. Два человека, синхронно правящие один файл, - главный источник конфликтов.
- Не держи в репозитории генерируемые/собираемые артефакты, если можешь - они конфликтуют постоянно и бессмысленно (для них как раз /
Код: Выделить всё
--oursили regenerate).Код: Выделить всё
--theirs - Включи rerere и zdiff3 один раз на машину - дальше каждый конфликт решается быстрее и менее болезненно.
Проделай по шагам, это 5 минут и навсегда снимает страх.
Код: Выделить всё
# 1. Песочница
$ git init -b main conflict-lab && cd conflict-lab
$ printf 'line1\noriginal\nline3\n' > f.txt
$ git add f.txt && git commit -m "base"
# 2. Включаем инструменты профи
$ git config merge.conflictStyle zdiff3
$ git config rerere.enabled true
# 3. Ветка feature правит среднюю строку
$ git switch -c feature
$ printf 'line1\nFROM-FEATURE\nline3\n' > f.txt
$ git commit -am "feature edit"
# 4. main правит ту же строку иначе
$ git switch main
$ printf 'line1\nFROM-MAIN\nline3\n' > f.txt
$ git commit -am "main edit"
# 5. Сливаем -> конфликт
$ git merge feature
$ git status # both modified: f.txt
$ cat f.txt # увидишь <<<<<<< ||||||| ======= >>>>>>>
# 6. Разрешаем: оставь нужную строку, сотри маркеры
# (отредактируй f.txt так, чтобы осталась line1 / нужный вариант / line3)
$ git add f.txt
$ git commit # завершает merge
# 7. Проверь, что rerere записал решение
$ git rerere status
Код: Выделить всё
git reset --hard HEAD~1Код: Выделить всё
git merge featureКонтрольные вопросы
- Почему Git вообще останавливается на конфликте, а не выбирает версию сам? Какие три версии строки он сравнивает?
- Что именно лежит в блоке между и
Код: Выделить всё
|||||||при стиле zdiff3 и почему это резко упрощает разрешение?Код: Выделить всё
======= - Чем /
Код: Выделить всё
--oursотличаются от ручного слияния и для каких файлов они уместны? Меняется ли смысл ours при rebase?Код: Выделить всё
--theirs - Как rerere находит твоё прошлое решение, если ветки слились в другом порядке или с другим conflictStyle?
Конфликт слияния - это не поломка, а вопрос Git к тебе: "обе стороны тронули одно место по-разному, реши, как надо". Маркеры
Код: Выделить всё
<<<<<<< ======= >>>>>>>Код: Выделить всё
git addКод: Выделить всё
commitКод: Выделить всё
--continueКод: Выделить всё
--abortКод: Выделить всё
merge.conflictStyle=zdiff3Код: Выделить всё
rerere.enabled=trueКод: Выделить всё
--oursКод: Выделить всё
--theirs