Конфликты слияния: анатомия и уверенное разрешение

Рейтинг: 40.9% · 8 голосов
Самый подробный курс по Git - от первого коммита до объектной базы и packfiles. Терминал и первый репозиторий, ветки и слияние, конфликты, удалённая работа и форджи (GitHub, GitLab), rebase и переписывание истории, спасение кода через reflog и fsck, stash и worktree, bisect и blame, подмодули и монорепо, хуки и подпись коммитов, внутреннее устройство и масштаб. Для новичков и профи. Актуально на 2026 (Git 2.54, дорожная карта 3.0).
Ответить
Аватара пользователя
Pavel_Git
Сообщения: 52
Зарегистрирован: 11 май 2026, 05:31

Конфликты слияния: анатомия и уверенное разрешение

Сообщение Pavel_Git »

Оглавление курса (52)
  1. Что такое контроль версий и почему победил Git
  2. Установка Git на Windows, macOS и Linux и первая настройка
  3. Терминал и как спрашивать у Git: выживание в командной строке
  4. Ментальная модель Git: три состояния, коммит-снимок и граф
  5. Первый репозиторий: init, add, commit, status
  6. Просмотр изменений: status, diff и индексация по частям
  7. История проекта: git log, диапазоны и поиск
  8. .gitignore: что не должно попасть в репозиторий
  9. .gitattributes: окончания строк, бинарники и атрибуты
  10. Ветки как указатели: branch, switch и detached HEAD
  11. Слияние веток: fast-forward против трёхстороннего merge
  12. Конфликты слияния: анатомия и уверенное разрешение (вы здесь)
  13. Практикум: твой первый день с Git от нуля до push
  14. Удалённые репозитории: remote, fetch, базовый pull и refspec
  15. git push: безопасная отправка и защита от перезаписи
  16. Клонирование: shallow, partial clone, bundle и backfill
  17. GitHub, GitLab и форки: pull request, rulesets и защита веток
  18. Рабочие процессы команды: GitFlow, GitHub Flow, trunk-based
  19. Merge queue и merge trains: безопасное вливание в trunk
  20. Культура code review и Conventional Commits
  21. git reset: три дерева и отмена индексации и коммитов
  22. git revert: безопасная отмена в общей истории
  23. Правка последнего коммита: git commit --amend
  24. git rebase: пересадка истории и золотое правило
  25. Интерактивный rebase: переписать серию коммитов
  26. git cherry-pick: перенос отдельных коммитов
  27. Stacked diffs: цепочка зависимых PR
  28. Массовое переписывание истории: git filter-repo и git replace
  29. git reflog: журнал, который помнит всё
  30. Восстановление утерянных коммитов, веток и файлов
  31. Откат опасных операций: reset --hard, сломанный rebase, плохой merge
  32. git stash: тайник для незавершённой работы
  33. git worktree: несколько рабочих деревьев одного репозитория
  34. git bisect: бинарный поиск регрессии
  35. git blame, mailmap и археология кода
  36. Теги, релизы, archive и bundle
  37. Подмодули: внешние репозитории внутри проекта
  38. Subtree, монорепозитории и sparse-checkout
  39. Git LFS и большие файлы
  40. Вспомогательные команды: clean, mv, rm, restore
  41. Псевдонимы и кастомизация .gitconfig
  42. Хуки Git: автоматизация на клиенте и сервере
  43. Подпись коммитов и тегов: SSH, GPG и Sigstore
  44. Внутреннее устройство I: объектная база Git
  45. Внутреннее устройство II: ссылки, HEAD, notes и reftable
  46. Внутреннее устройство III: индекс, packfiles, gc и обслуживание
  47. От SHA-1 к SHA-256: переход на новый хеш
  48. Огромные репозитории: partial clone, FSMonitor, Scalar
  49. Git в CI/CD: shallow, кеш, merge_group и безопасность
  50. Куда движется Git: дорожная карта 3.0 и экосистема
  51. Профессиональный setup: собираем рабочее окружение Git
  52. 30+ типичных ошибок Git и как из них выбираться
Конфликт слияния git - это не авария, а нормальная развилка

Первый раз увидеть в файле строчки

Код: Выделить всё

<<<<<<< HEAD
и

Код: Выделить всё

>>>>>>> feature
- это маленький инфаркт. Кажется, что Git что-то сломал, проект испорчен, и сейчас ты потеряешь работу. Спокойно. Конфликт слияния git - это не ошибка и не баг. Это честное признание Git в том, что он НЕ умеет читать твои мысли.

Вспомни три дерева из прошлых уроков и устройство 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.
Открываем app.py и видим конфликтный блок:

Код: Выделить всё

def greet(name):
<<<<<<< HEAD
    return f"Привет, {name}! Добро пожаловать."
=======
    return f"Здравствуйте, {name}. Рады видеть вас."
>>>>>>> feature
Читается это так. Между

Код: Выделить всё

<<<<<<< HEAD
и лежит ТВОЯ версия - то, что сейчас в текущей ветке (HEAD, то есть main). Между и

Код: Выделить всё

>>>>>>> feature
лежит версия из ВХОДЯЩЕЙ ветки feature. Семь знаков , семь и семь - это не украшение, а служебные маркеры, которые Git ищет дословно. Имена после маркеров - это метки сторон: HEAD слева, имя сливаемой ветки (или коммита) справа.

Запомни мнемонику навсегда: ours (наше) - это всегда верхний блок, сторона HEAD; theirs (их) - нижний блок, входящая сторона. Эти же слова всплывут в и

Код: Выделить всё

--theirs
чуть ниже. В обычном merge "наше" = ветка, куда сливаем; "их" = ветка, которую сливаем. (Маленькая засада: при rebase ours и theirs меняются местами, потому что rebase переставляет твои коммиты поверх чужих. Об этом - в уроке про rebase, пока просто держи в голове.)

diff3 и 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)
Ключевая строка - both modified: обе стороны меняли app.py. Git прямо подсказывает следующий шаг и аварийный выход. Файлы в разделе "Unmerged paths" - это твой список дел.

Теперь

Код: Выделить всё

git diff
без аргументов во время конфликта показывает не обычный diff, а так называемый combined 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
Две колонки / слева вместо одной - это и есть combined diff: первый столбец относится к одному родителю слияния, второй к другому. Глубоко вникать в формат не обязательно, важно понять идею - Git показывает изменение относительно ОБОИХ предков сразу. Полезный спецприём:

Код: Выделить всё

git diff --check
найдёт оставшиеся маркеры конфликта, если ты вдруг забыл их вычистить.

Как разрешить конфликт git руками: правка, add, commit

Цикл разрешения всегда один и тот же, заучи его как таблицу умножения.

Шаг 1. Отредактируй файл. Открой app.py и СОТРИ все маркеры , , , вместе с ненужными блоками. Оставь ровно тот код, который должен быть в итоге. Это может быть наша версия, их версия, или СПЛАВ обеих - часто правильный ответ "взять кусок отсюда и кусок оттуда". Никакой магии: ты просто пишешь финальный код, как будто конфликта и не было.

Код: Выделить всё

def greet(name):
    return f"Здравствуйте, {name}. Рады видеть вас!"
Шаг 2. Пометь файл как разрешённый - git add. Команда

Код: Выделить всё

git add app.py
здесь имеет особый смысл: ты говоришь Git "этот файл я починил, убери его из unmerged". Технически add записывает разрешённую версию в индекс и снимает конфликтную пометку. Проверь

Код: Выделить всё

git status
- файл должен уйти из "Unmerged paths".

Шаг 3. Заверши слияние. Когда все конфликтные файлы добавлены:

Код: Выделить всё

$ git commit
Без аргументов: Git уже подготовил осмысленное сообщение вида "Merge branch 'feature'" и список конфликтовавших файлов - можно просто сохранить. Если разрешал конфликт в ходе

Код: Выделить всё

git merge
, завершает

Код: Выделить всё

git commit
. Если это был rebase или cherry-pick - там вместо commit пишут

Код: Выделить всё

git rebase --continue
/

Код: Выделить всё

git cherry-pick --continue
: continue сам сделает нужный коммит и поедет дальше по списку.

--ours, --theirs и mergetool: когда руками неудобно

Иногда редактировать вручную глупо. Классика - бинарник или сгенерированный файл (картинка, package-lock.json, скомпилированный ассет). Там нет "строк", чтобы слить - нужно целиком взять одну из версий. Для этого есть

Код: Выделить всё

git checkout
и более новый

Код: Выделить всё

git restore
(стабилен с 2.51) с флагами выбора стороны:

Код: Выделить всё

$ git restore --ours  logo.png   # берём версию из нашей ветки (HEAD)
$ git restore --theirs logo.png   # берём версию из входящей ветки
$ git add logo.png

Код: Выделить всё

git checkout --ours logo.png
делает то же самое - в сети полно примеров именно с checkout, так что узнавай обе формы. Важно: /

Код: Выделить всё

--theirs
забирают сторону ЦЕЛИКОМ, никакого слияния содержимого. Для бинарника это ровно то, что надо; для исходника - обычно нет, там почти всегда хочется сплав.

Когда конфликтов много и хочется трёхпанельный визуальный разбор (наше | база | их), зови git mergetool:

Код: Выделить всё

$ git mergetool
Merging:
app.py

Normal merge conflict for 'app.py':
  {local}: modified file
  {remote}: modified file
git mergetool запустит настроенную внешнюю утилиту (meld, kdiff3, vimdiff, vscode, Beyond Compare) с тремя версиями на экране. Ты кликами собираешь результат, сохраняешь, выходишь - tool сам делает add. Настраивается через

Код: Выделить всё

git config --global merge.tool meld
. После работы git mergetool раньше оставлял -файлы с копией конфликта - проверь, не нужно ли их подчистить (или поставь

Код: Выделить всё

mergetool.keepBackup false
). git mergetool - не обязаловка: на простом конфликте быстрее руками, на запутанном трёхстороннем - визуальный инструмент экономит нервы.

git 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'
"Recorded preimage" - Git сфотографировал конфликт ДО твоей правки. Ты решаешь, делаешь add - Git дописывает "Recorded resolution for 'app.py'". При следующей встрече того же конфликта:

Код: Выделить всё

CONFLICT (content): Merge conflict in app.py
Resolved 'app.py' using previous resolution.
Файл уже разрешён за тебя - тебе остаётся проверить результат глазами и сделать add. Маленькая, но важная деталь: rerere НОРМАЛИЗУЕТ конфликт перед поиском в своей базе - срезает метки веток и убирает блок базы из diff3/zdiff3. Поэтому записанное решение находится, даже если ветки слились в другом порядке или у тебя другой conflictStyle. База лежит в

Код: Выделить всё

.git/rr-cache
. Полезные команды:

Код: Выделить всё

git rerere status
(что записано сейчас),

Код: Выделить всё

git rerere diff
(как идёт разрешение),

Код: Выделить всё

git rerere forget app.py
(стереть запись, если решил неправильно и хочешь переиграть). Один нюанс честно: rerere применяет твоё прошлое решение МЕХАНИЧЕСКИ. Если контекст изменился, оно может оказаться неверным - поэтому после "Resolved using previous resolution" всё равно глянь, что получилось.

Аварийный выход и профилактика конфликтов

Главная кнопка спасения -

Код: Выделить всё

git merge --abort
. Она откатывает репозиторий в состояние ДО merge, как будто ты и не начинал. Маркеры исчезают, рабочая директория чистая. Для rebase аналог -

Код: Выделить всё

git rebase --abort
, для cherry-pick -

Код: Выделить всё

git cherry-pick --abort
. Запомни: пока ты не сделал финальный commit/continue, отступить можно всегда и бесплатно. Это снимает 90% паники.

Код: Выделить всё

$ git merge --abort     # передумал - вернулись как было
$ git merge --continue  # то же, что commit, но проверит, что unmerged не осталось
Профилактика дешевле лечения. Конфликты не победить полностью, но сократить - легко.
  • Сливай часто, мелкими порциями. Чем дольше ветка живёт в стороне, тем сильнее она расходится с main и тем больнее финальный merge. Регулярный

    Код: Выделить всё

    git merge main
    (или rebase) в свою ветку держит расхождение маленьким.
  • Не форматируй чужой код "заодно". Массовое переформатирование, смена отступов, перенос строк рождают конфликты на ровном месте, потому что трогают строки, которые логически никто не менял.
  • Договоритесь о границах в команде: кто за какой модуль отвечает. Два человека, синхронно правящие один файл, - главный источник конфликтов.
  • Не держи в репозитории генерируемые/собираемые артефакты, если можешь - они конфликтуют постоянно и бессмысленно (для них как раз /

    Код: Выделить всё

    --theirs
    или regenerate).
  • Включи 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
Повтори merge с откатом (

Код: Выделить всё

git reset --hard HEAD~1
, потом снова

Код: Выделить всё

git merge feature
) - увидишь "Resolved using previous resolution". Это и есть магия rerere вживую.

Контрольные вопросы
  • Почему Git вообще останавливается на конфликте, а не выбирает версию сам? Какие три версии строки он сравнивает?
  • Что именно лежит в блоке между и при стиле zdiff3 и почему это резко упрощает разрешение?
  • Чем /

    Код: Выделить всё

    --theirs
    отличаются от ручного слияния и для каких файлов они уместны? Меняется ли смысл ours при rebase?
  • Как rerere находит твоё прошлое решение, если ветки слились в другом порядке или с другим conflictStyle?
Итог

Конфликт слияния - это не поломка, а вопрос Git к тебе: "обе стороны тронули одно место по-разному, реши, как надо". Маркеры

Код: Выделить всё

<<<<<<< ======= >>>>>>>
- просто разметка двух (а с diff3/zdiff3 - трёх) версий. Алгоритм неизменен: правишь файл -> -> /

Код: Выделить всё

--continue
; передумал - . Включи

Код: Выделить всё

merge.conflictStyle=zdiff3
, чтобы видеть базу, и

Код: Выделить всё

rerere.enabled=true
, чтобы Git помнил твои решения. Бинарники бери целиком через /

Код: Выделить всё

--theirs
, запутанное разбирай в git mergetool. И сливайся почаще - тогда конфликты будут редкими и мелкими.
👍6 ❤️1 🔥 😄 🤔1
Аватара пользователя
ZfsUser
Сообщения: 1
Зарегистрирован: 18 май 2026, 01:17

Re: Конфликты слияния: анатомия и уверенное разрешение

Сообщение ZfsUser »

Сидел и боялся этих стрелочек годами, а оказывается ours это просто верхний блок. zdiff3 включил сразу, с базой и правда в разы понятнее кто что менял.
👍1 ❤️ 🔥 😄 🤔
Аватара пользователя
elixir11
Сообщения: 1
Зарегистрирован: 24 май 2026, 00:58

Re: Конфликты слияния: анатомия и уверенное разрешение

Сообщение elixir11 »

Про rerere вообще не знал. У меня rebase ветки на main каждый раз один и тот же конфликт ловил, руками решал по три раза. Включил rerere.enabled - теперь Git сам подставляет. Спасибо, реально сэкономило.
👍 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Слияние веток: fast-forward против трёхстороннего merge
Следующая глава →
Практикум: твой первый день с Git от нуля до push

Все главы курса «Git профессионально: от первого коммита до внутреннего устройства»

Поделиться темой: ✈ Telegram VK
Похожие запросы: Разрешение конфликтов слияния в Gitgit merge: слияние ветокЧастые ошибки Git и как их исправить

Вернуться в «Git профессионально: от первого коммита до внутреннего устройства»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость