$ sudo teach IT
Модуль 4 · Ветки · Урок 4.3

Конфликты слияния

Вы спокойно решите конфликт слияния: прочитаете маркеры, выберете нужную версию и закончите слияние.

Теория + практика~25 минутНовичокCONFLICTgit merge --abort

Откуда берётся конфликт

Конфликт слияния в Git (merge conflict) — это ситуация, когда две ветки изменили одну и ту же строку, и Git не может сам решить, какая версия правильная: он останавливает слияние и просит выбрать вас. Вы нарочно устроите конфликт на визитке, построчно разберёте маркеры <<<<<<<, ======= и >>>>>>> и решите конфликт в терминале и в VS Code.

Когда вы сливали подвал, Git сам соединил правки двух веток: они были в разных местах. Теперь посмотрим, что будет, если места совпадут.

Простыми словами

Два редактора правили один абзац книги, каждый по-своему. Корректор не гадает, кто прав: кладёт обе версии рядом и спрашивает автора. Git поступает так же.

Конфликт — это не поломка

Самый частый страх новичка — «я всё сломал». Нет: обе версии строки целы в своих коммитах. Git поставил слияние на паузу и ждёт вашего решения, а передумать можно одной командой. В команде конфликты решают каждую неделю.

Устроим конфликт

Создайте ветку feature/subtitle и поменяйте в index.html подзаголовок — строку <p class="lead">:

Терминал
$ git switch -c feature/subtitle
Switched to a new branch 'feature/subtitle'
$ git diff
...
-  <p class="lead">Начинающий веб-разработчик</p>
+  <p class="lead">Начинающий веб-разработчик: HTML, CSS и Git</p>
...
$ git add index.html
$ git commit -m "Уточнить подзаголовок"
[feature/subtitle 5162731] Уточнить подзаголовок
 1 file changed, 1 insertion(+), 1 deletion(-)

Теперь представьте, что вы забыли про ветку и поправили ту же строку прямо в main, но по-другому:

Терминал
$ git switch main
Switched to branch 'main'
$ git diff
...
-  <p class="lead">Начинающий веб-разработчик</p>
+  <p class="lead">Начинающий фронтенд-разработчик</p>
...
$ git add index.html
$ git commit -m "Изменить подзаголовок"
[main a8b0439] Изменить подзаголовок
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline --graph --all
* a8b0439 (HEAD -> main) Изменить подзаголовок
| * 5162731 (feature/subtitle) Уточнить подзаголовок
|/
*   d3c03bd Merge branch 'feature/footer'
...

На графе развилка: обе ветки начались от d3c03bd и поменяли одну строку, каждая по-своему.

Сообщение о конфликте

Сливаем, как обычно, стоя на main:

Терминал
$ git merge feature/subtitle
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

Разбор по строкам

  • Auto-merging index.html — Git начал соединять правки в index.html.
  • CONFLICT (content): Merge conflict in index.html — конфликт по содержимому (content) в файле index.html.
  • Automatic merge failed; fix conflicts and then commit the result. — «автоматическое слияние не удалось; исправьте конфликты и закоммитьте результат». Это план действий, а не приговор.

Слияние сейчас на паузе. Спросим git status, что происходит:

Терминал
$ 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:   index.html

no changes added to commit (use "git add" and/or "git commit -a")

Разбор по строкам

  • You have unmerged paths. — «есть неслитые файлы». Ниже два пути: вперёд — исправить и закоммитить (git commit), назад — отменить слияние (git merge --abort).
  • both modified: index.html — «изменён в обеих ветках»: этот файл ждёт решения. Остальные файлы Git слил сам.
  • (use "git add <file>..." to mark resolution) — исправленный файл отмечают решённым через git add.

В Git Bash в приглашении терминала рядом с именем ветки появится MERGING — напоминание, что слияние ещё не закончено.

Маркеры конфликта

Откройте index.html в VS Code. В месте конфликта Git вписал прямо в файл обе версии, разделив их специальными строками — маркерами конфликта:

HTML · index.html (фрагмент с конфликтом)
  <h1>Алекс Иванов</h1>
<<<<<<< HEAD
  <p class="lead">Начинающий фронтенд-разработчик</p>
=======
  <p class="lead">Начинающий веб-разработчик: HTML, CSS и Git</p>
>>>>>>> feature/subtitle
  <section id="about">

Разбор по строкам

  • <h1>Алекс Иванов</h1> и <section id="about"> — обычные строки. Всё, что вне маркеров, Git уже слил сам.
  • <<<<<<< HEAD — начало конфликта; ниже, до разделителя, — версия HEAD, то есть текущей ветки main, в которую вы сливаете.
  • <p class="lead">Начинающий фронтенд-разработчик</p> — «наша» версия, из коммита a8b0439 в main.
  • ======= — разделитель между двумя версиями.
  • <p class="lead">Начинающий веб-разработчик: HTML, CSS и Git</p> — «их» версия, пришедшая из сливаемой ветки.
  • >>>>>>> feature/subtitle — конец конфликта и имя ветки, откуда пришла нижняя версия.

Маркеры — не HTML: Git вписал их обычным текстом, и в браузере сейчас <<<<<<< HEAD видно прямо на странице. Поэтому все три маркера обязательно удаляют. Конфликтов в файле может быть несколько — ищите их поиском по <<<<<<< (Ctrl+F, на macOS — Cmd+F).

Решаем: правка, add, commit

Решить конфликт — значит оставить в файле ту версию, которая нужна, и убрать маркеры. Что оставить, решаете вы:

Что оставитьЧто будет в файле
нашу версию (HEAD)Начинающий фронтенд-разработчик
их версию (feature/subtitle)Начинающий веб-разработчик: HTML, CSS и Git
обе подряддве строки <p class="lead">: для подзаголовка не годится, а для списка бывает в самый раз
свою, объединив обеНачинающий фронтенд-разработчик: HTML, CSS и Git

Возьмём лучшее из обеих: «фронтенд-разработчик» из main и «HTML, CSS и Git» из ветки. Удалите пять строк от <<<<<<< HEAD до >>>>>>> feature/subtitle включительно и напишите вместо них одну:

HTML · index.html (конфликт решён)
  <h1>Алекс Иванов</h1>
  <p class="lead">Начинающий фронтенд-разработчик: HTML, CSS и Git</p>
  <section id="about">

Сохраните файл. Проверьте, что маркеров не осталось, отметьте конфликт решённым и посмотрите статус. Команда grep -n ищет строку в файле и печатает номера найденных строк; пустой ответ — маркеров нет:

Терминал
$ grep -n "<<<<<<<" index.html
$ git add index.html
$ git status
On branch main
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:
	modified:   index.html

Разбор по строкам

  • grep -n "<<<<<<<" index.html — пусто, маркеров нет. Иначе вывел бы, например, 10:<<<<<<< HEAD.
  • git add index.html — здесь add значит «конфликт в этом файле решён».
  • All conflicts fixed but you are still merging. — «все конфликты решены, но слияние ещё идёт».
  • (use "git commit" to conclude merge) — остался последний шаг: коммит.

Выполните git commit без -m. Откроется редактор с готовым сообщением:

Вывод · редактор
Merge branch 'feature/subtitle'

# Conflicts:
#	index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please run
#	git update-ref -d MERGE_HEAD
# and try again.
...

Разбор по строкам

  • Merge branch 'feature/subtitle' — сообщение merge-коммита, оставьте его.
  • # Conflicts: — напоминание, в каких файлах был конфликт; # It looks like you may be committing a merge. — Git подтверждает, что это коммит слияния. Строки с # в коммит не попадут.
  • Сохраните и закройте редактор, как при обычном merge-коммите.
Терминал
$ git commit
[main 9a283db] Merge branch 'feature/subtitle'
$ git log --oneline --graph --all
*   9a283db (HEAD -> main) Merge branch 'feature/subtitle'
|\
| * 5162731 (feature/subtitle) Уточнить подзаголовок
* | a8b0439 Изменить подзаголовок
|/
*   d3c03bd Merge branch 'feature/footer'
...
$ git branch -d feature/subtitle
Deleted branch feature/subtitle (was 5162731).

Слияние закончено: merge-коммит 9a283db соединил обе линии, а на главной теперь подзаголовок «Начинающий фронтенд-разработчик: HTML, CSS и Git». Ветку feature/subtitle удаляем, как любую слитую.

Страховка в Git 2.56: git add --resolved

В Git 2.56 появился флаг git add --resolved index.html: он добавляет в индекс только файлы с конфликтом и откажется, если в них остались маркеры. Обычный git add маркеры не проверяет.

То же самое в VS Code

VS Code сам подсвечивает конфликт цветом, а над ним показывает строку действий:

  • Accept Current Change — «принять текущее»: оставить версию HEAD (из main);
  • Accept Incoming Change — «принять входящее»: оставить версию из сливаемой ветки;
  • Accept Both Changes — «принять оба»: оставить обе версии подряд;
  • Compare Changes — открыть две версии рядом для сравнения.

Нажатие на действие само удаляет маркеры. Для нашего подзаголовка нажмите Accept Current Change и допишите «: HTML, CSS и Git» руками. В панели Source Control (Ctrl+Shift+G) файлы с конфликтом собраны в разделе Merge Changes; кнопка + (Stage Changes) у файла — то же, что git add.

Слияние и тут завершает коммит: git commit или кнопка Commit в Source Control. Подробно, включая трёхпанельный редактор слияния, — в документации VS Code.

Проверяйте результат глазами

Кнопки не понимают смысла: Accept Both Changes здесь оставит два подзаголовка подряд. После любой кнопки перечитайте фрагмент и откройте страницу в браузере.

Передумали: git merge --abort

Не уверены, какую версию оставить, или запутались в правках? Слияние можно отменить целиком, пока вы его не закоммитили:

Терминал
$ git merge --abort
$ git status
On branch main
nothing to commit, working tree clean
$ git log --oneline -1
a8b0439 Изменить подзаголовок

Разбор по строкам

  • git merge --abort ничего не печатает и возвращает файлы и индекс в состояние до git merge.
  • a8b0439 Изменить подзаголовок — main стоит на прежнем коммите, merge-коммита не было. Ветка feature/subtitle тоже цела: сливайте снова, когда будете готовы.

Начинайте слияние с чистым git status

Если перед git merge в папке были незакоммиченные правки, git merge --abort может не суметь их восстановить. Поэтому перед слиянием закоммитьте правки или временно уберите их (как — в следующем уроке).

Подробно о том, как Git показывает конфликты, — в документации git merge, раздел HOW CONFLICTS ARE PRESENTED.

Практика у себя

Пройдите путь сами: сначала отменим слияние, потом решим конфликт по-настоящему.

Шаг 1. Сделайте два коммита из раздела «Устроим конфликт»: «Уточнить подзаголовок» в ветке feature/subtitle и «Изменить подзаголовок» в main.

Шаг 2. На main: git merge feature/subtitle — CONFLICT (content): Merge conflict in index.html, в git status — both modified: index.html. Найдите маркеры в файле.

Шаг 3. Отмените: git merge --abort. git status снова чистый.

Шаг 4. Слейте ещё раз, оставьте одну строку <p class="lead">Начинающий фронтенд-разработчик: HTML, CSS и Git</p> без маркеров, сохраните.

Шаг 5. grep -n "<<<<<<<" index.html (пусто), git add index.html, git commit с сообщением из редактора, затем git branch -d feature/subtitle.

Проверьте себя

  • git log --oneline --graph начинается с Merge branch 'feature/subtitle', под ним две линии: «Изменить подзаголовок» и «Уточнить подзаголовок».
  • В index.html нет ни одной строки с <<<<<<<, ======= или >>>>>>>.
  • В браузере под именем один подзаголовок: «Начинающий фронтенд-разработчик: HTML, CSS и Git».
  • git status: nothing to commit, working tree clean; git branch показывает только main.

Частые ошибки

Закоммитили маркеры. Обычный git add маркеры не проверяет, и коммит с ними пройдёт — на странице появится <<<<<<< HEAD. Уберите маркеры, выполните git add index.html и новый коммит «Убрать маркеры конфликта». Чтобы не повторилось, перед git add ищите <<<<<<< (grep -n или поиск в VS Code).

Сделали git add до правки. Git поверил вам на слово и пишет All conflicts fixed, хотя маркеры на месте. Пока нет коммита, ничего страшного: поправьте файл, снова git add index.html и только потом git commit.

Коммит без git add. Если сразу выполнить git commit, Git откажет:

Терминал
$ git commit
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.
U	index.html

Разбор по строкам

  • «Коммит невозможен: есть нерешённые файлы»; подсказка hint: — исправьте их и отметьте через git add. U index.html — U (unmerged): файл с нерешённым конфликтом.

Не дают переключить ветку. Во время конфликта git switch ответит fatal: cannot switch branch while merging. Сначала закончите слияние коммитом или отмените его git merge --abort.

Не удаляйте папку проекта

В панике хочется удалить portfolio и начать заново. Не надо: вместе с папкой пропадёт .git со всей историей. git status подскажет, что делать, а git merge --abort вернёт всё как было до слияния.

Что важно запомнить

  • Конфликт — когда две ветки поменяли одни и те же строки. Это пауза, а не поломка: обе версии целы.
  • Маркеры: <<<<<<< HEAD — начало и версия текущей ветки, ======= — разделитель, >>>>>>> имя-ветки — конец и версия сливаемой ветки.
  • Решение: оставить нужный текст, удалить все маркеры, git add, git commit.
  • В VS Code: Accept Current Change, Accept Incoming Change, Accept Both Changes — и обязательно проверить результат.
  • git merge --abort отменяет слияние целиком, пока нет коммита.
  • Сливайте с чистым git status.

Конфликты больше не страшны. Но есть другая частая неприятность: правка в работе, а надо срочно переключиться в другую ветку, и Git не пускает. Как временно убрать незаконченные правки в сторону — git stash, в следующем уроке.

На следующий урок