Откуда берётся конфликт
Конфликт слияния в 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 вписал прямо в файл обе версии, разделив их специальными строками — маркерами конфликта:
<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 включительно и напишите вместо них одну:
<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, в следующем уроке.