open atlas
↑ К треку
Git: от нуля до сеньора GIT · 03 · 04

Разрешение конфликтов слияния

Конфликт слияния возникает, когда обе ветки меняют одни и те же строки. Git вписывает в файл маркеры конфликта (HEAD против входящего), ставя слияние на паузу. Ты правишь до финала, удаляешь маркеры, git add, затем commit. git merge --abort откатывает слияние.

GIT Middle ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior

Трёхстороннее слияние работает автоматически, пока две ветки меняли разные регионы. В тот момент, когда обе ветки правят одни и те же строки по-разному, git останавливается и признаёт, что не может угадать, какую версию ты хочешь. Новички видят маркеры >>>>>>>, паникуют и иногда сносят весь репозиторий. Бояться нечего: конфликт — это просто git, ставящий слияние на паузу и отдающий решение тебе, с обеими версиями, разложенными в файле.

К концу этого урока ты сможешь читать маркеры конфликта, разрешать конфликт руками, завершать слияние через git add и git commit и чисто выходить через git merge --abort.

Цель

После этого урока ты сможешь объяснить, почему возникает конфликт слияния, читать три маркера конфликта, которые git вставляет, приводить файл к задуманному финальному содержимому, завершать слияние через git add, затем git commit (или git merge --continue) и отменять слияние через git merge --abort.

1

Конфликт возникает, когда обе ветки изменили один и тот же регион по-разному. Вспомни трёхстороннее слияние: для каждого куска git сравнивает merge base с каждой стороной. Если регион тронула только одна сторона, git берёт это изменение молча. Если обе стороны изменили одни и те же строки и изменения различаются, у git нет правила выбрать победителя — этот кусок становится конфликтом:

git merge feature
# Auto-merging app.js
# CONFLICT (content): Merge conflict in app.js
# Automatic merge failed; fix conflicts and then commit the result.

Слияние теперь на паузе, а не провалено-и-откачено. Файлы, которые git смог слить чисто, уже проиндексированы; конфликтный файл оставлен на диске с обеими версиями, помеченными, чтобы ты их примирил.

2

Git помечает конфликт в файле тремя строками. Открой конфликтный файл, и ты увидишь блок вроде этого (каждый маркер ровно семь символов):

<<<<<<< HEAD
const timeout = 30;
=======
const timeout = 60;
>>>>>>> feature

Читай это так: между <<<<<<< HEAD и ======= — версия твоей текущей ветки (ветки, в которую ты сливал); между ======= и >>>>>>> featureвходящая версия из ветки, которую ты сливаешь. Метка после >>>>>>> называет источник. Всё за пределами маркеров слилось нормально; человек нужен лишь помеченному блоку.

3

Разреши, приведя файл к финальному содержимому, которое тебе нужно, затем удалив все три маркера. Ты не обязан выбирать одну из сторон — ты пишешь любой правильный результат. Это может быть их вариант, твой, комбинация или что-то новое:

const timeout = 45;

Незыблемое правило: трёх строк-маркеров (<<<<<<<, =======, >>>>>>>) быть не должно. Оставить случайный маркер — это классический баг новичка: он отправляет сломанный синтаксис в продакшен. Быстрый git diff или поиск по дереву <<<<<<< перед коммитом ловит любой пропущенный.

4

Скажи git, что конфликт улажен, через git add, затем заверши через git commit. Индексация файла — это как ты сигналишь «этот разрешён»:

git add app.js                 # пометить файл разрешённым
git status                     # убедиться, что ничего не осталось "Unmerged"
git commit                     # завершает merge-коммит
# или, эквивалентно, после индексации:
git merge --continue

Во время конфликта git status — твоя приборная панель: он перечисляет файлы как «Unmerged paths», пока ты не сделаешь git add каждый. Как только все проиндексированы, git commit записывает merge-коммит (git заранее заполняет сообщение слияния). git merge --continue делает то же самое, когда всё проиндексировано.

Почему это работает

Почему git останавливается, а не угадывает? Неверное авторазрешение хуже, чем никакого: молча выбрав одну сторону, можно выбросить фикс безопасности или применить изменение дважды, и ты можешь не заметить, пока не сломается продакшен. Принцип дизайна git — никогда не терять и молча не менять закоммиченную работу, так что когда он действительно не может сказать, какое намерение верно, он отказывается решать и показывает обе версии. Конфликт — это git, защищающий тебя от уверенного неправильного ответа.

5

Чтобы выйти полностью, git merge --abort восстанавливает состояние до слияния. Если конфликт больше, чем ты ожидал, или ты начал слияние не с той ветки, откатись:

git merge --abort   # отбросить идущее слияние, вернуться точно к моменту до 'git merge'

Это безопасно и полно: указатель ветки, рабочее дерево и индекс возвращаются к моменту перед запуском git merge. Две вещи, которые стоит знать: git mergetool открывает настроенный визуальный трёхпанельный инструмент для разрешения конфликтов вместо правки маркеров вручную, а git rebase производит те же самые маркеры конфликта — так что всё, что ты только что выучил о разрешении, применимо и там (rebase ты завершаешь через git rebase --continue вместо коммита).

Разбор примера

Один конфликт, от начала до конца.

Обе ветки изменили одну и ту же строку конфига. На mainconst retries = 3;. На feature та же строка стала const retries = 5;. Ты сливаешь:

git switch main
git merge feature
# CONFLICT (content): Merge conflict in config.js

git status показывает config.js под «Unmerged paths». Открой его:

<<<<<<< HEAD
const retries = 3;
=======
const retries = 5;
>>>>>>> feature

Ты решаешь, что правильное значение — 5 (фича намеренно его подняла). Сократи блок до одной правильной строки и убери каждый маркер:

const retries = 5;

Проиндексируй и заверши:

git add config.js
git status            # ничего несведённого больше нет
git commit            # пишет merge-коммит

Готово. У merge-коммита два родителя, ровно как при чистом трёхстороннем слиянии — разница лишь в том, что спорную строку решил ты, а не git. Если бы ты захотел выйти, git merge --abort в любой момент до коммита вернул бы config.js и main к состоянию до слияния.

Частая ошибка

Самая разрушительная ошибка конфликта — закоммитить с маркером, всё ещё оставшимся в файле. Забытый ======= или >>>>>>> feature — валидный текст для git, но сломанный синтаксис для компилятора, и он незаметно проскакивает в merge-коммит. Выработай привычку грепать дерево по <<<<<<< (или поручи это lint/CI-проверке) перед коммитом разрешения. Вторая по частоте ошибка — «разрешить», слепо оставив HEAD и молча выбросив входящее изменение: это выбрасывает ровно ту работу, которую ты сливал.

Проверь себя
Викторина

После правки конфликтного файла до правильного финального содержимого, что заставляет git считать этот файл разрешённым?

Итог

Конфликт слияния возникает, когда обе ветки меняют один и тот же регион по-разному и git не может выбрать победителя, поэтому он ставит на паузу слияние и вписывает в файл три маркера конфликта: <<<<<<< HEAD (твоя сторона), ======= и >>>>>>> branch (входящая сторона). Ты разрешаешь, приведя блок к задуманному финальному содержимому и удалив каждый маркер, затем делаешь git add файла, чтобы пометить его разрешённым, и git commit (или git merge --continue), чтобы записать merge-коммит. git merge --abort чисто восстанавливает состояние до слияния. Те же маркеры появляются при rebase, а git mergetool предлагает визуальную альтернативу — но решение всегда твоё: git ставит паузу именно потому, что отказывается угадать неверно.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.