Git: ветки и merge
Зачем это нужно
Ты уже умеешь сохранять состояние проекта коммитами. Но вот ситуация: проект работает, и тебе хочется попробовать рискованную идею — переделать оформление, переписать кусок кода. А вдруг не получится? Ломать работающую версию страшно, а копировать папку проекта «на всякий случай» — снова путь к хаосу из project_final_2_new, с которого начиналась прошлая глава. Для этого в Git есть ветка (branch) — параллельная линия истории внутри одного и того же репозитория. Ты сворачиваешь с основной дороги, экспериментируешь сколько хочешь, и основная версия при этом остаётся нетронутой: не вышло — просто вернулся обратно, как будто ничего и не было. А получилось — результат вливается в основную ветку одной командой: это называется слияние (merge).
Так устроена работа любой команды разработчиков, и на чемпионате «Профессионалы» в том числе: каждый участник — а иногда и один участник, но по разным задачам — пилит свою часть в своей ветке, а потом всё сливается вместе в одну историю. Иногда при слиянии Git встречает противоречие — конфликт (conflict): обе ветки поменяли одну и ту же строку по-разному, и машина не готова угадывать, кто прав. Звучит грозно, но на деле это обычная рабочая ситуация с понятным алгоритмом решения — научиться его проходить спокойно и быстро важнее, чем никогда не встречать конфликтов вообще. В этой главе, как и в прошлой, весь путь разобран по молекулам: что происходит внутри .git после каждой команды и что означает каждое слово в ответе терминала.
Сквозной пример: ветки в том же my-app
Продолжаем ровно тот проект my-app, на котором остановилась прошлая глава: три коммита, файл main.py с двумя строками приветствия, файл .gitignore с одной записью draft.txt. Если у тебя этой папки уже нет — не страшно, весь путь читается и без повторного запуска: главное, что каждая команда и весь вывод терминала ниже — не выдумка. Это дословный вывод настоящего Git 2.43.0 на чистой Ubuntu 24.04, полученный специально для этой главы на следующий день после главы про первый коммит — те же самые пять команд git init → git add → git commit, разобранные там, привели ровно к этому состоянию.
Убедимся, что мы на одной странице — состояние my-app, с которого стартуем:
cd my-app
git log --oneline
fe59fd2 add gitignore for drafts
2f16b54 add welcome message
334596a add main screen
git branch: какие ветки уже есть
Спросим Git, сколько веток вообще существует в этом репозитории прямо сейчас:
git branch
* master
Одна-единственная строка — ветка master, та самая, что создалась сама при первом git init в прошлой главе. Звёздочка перед именем — не украшение, а рабочий индикатор: она отмечает текущую ветку, ту, на которой стоит HEAD и куда лягут следующие коммиты.
git switch -c: создать ветку и сразу на неё перейти
Хотим попробовать доработку — добавить в main.py счёт и число игроков — но не портить пока рабочую версию в master. Заводим отдельную ветку:
git switch -c feature-scoreboard
Switched to a new branch 'feature-scoreboard'
«Переключился на новую ветку feature-scoreboard» — коротко и по делу. Проверим список веток снова:
git branch
* feature-scoreboard
master
Звёздочка переехала: теперь текущая ветка — feature-scoreboard, а master просто стоит в списке рядом, никуда не делась и не изменилась.
Ветка — не копия проекта. Внутри .git ветка — это один короткий текстовый файл в refs/heads/, который хранит всего один hash: на какой коммит она указывает. git switch -c не копирует ни один файл проекта — вот почему создание ветки происходит мгновенно, даже если в проекте гигабайты кода.
Убедиться легко:
cat .git/refs/heads/feature-scoreboard
cat .git/refs/heads/master
fe59fd2533f5042944c1211e96e7b96c92f5aa59
fe59fd2533f5042944c1211e96e7b96c92f5aa59
Прямо сейчас обе ветки — это две подписанные бумажки-закладки, указывающие ровно на один и тот же коммит fe59fd2. Разница появится, как только сделаем коммит: закладка feature-scoreboard сдвинется вперёд вместе с HEAD, а master останется лежать на месте, пока мы сами её туда не позовём.
В старых статьях и видео вместо git switch -c встретится git checkout -b feature-scoreboard — это ровно та же операция создания и переключения, просто старой универсальной командой checkout, которая умеет ещё много всего другого (например, отменять правки в файле) и поэтому исторически была источником путаницы. git switch — новее, появилась в Git 2.23 специально для того, чтобы у переключения веток была своя, ничем не перегруженная команда. Обе работают и сейчас, но новые материалы и эта глава используют switch.
Коммиты внутри ветки: master не шевелится
Теперь работаем на feature-scoreboard как обычно — правим файл, коммитим:
printf 'print("Привет, чемпионат!")\nprint("Удачи!")\nprint("Счёт: 0")\n' > main.py
git add main.py
git commit -m "add score line"
[feature-scoreboard dd98912] add score line
1 file changed, 1 insertion(+)
Заметь: квадратные скобки теперь показывают feature-scoreboard, а не master — коммит лёг именно в текущую ветку. Добавим вторую строку и закоммитим ещё раз:
printf 'print("Привет, чемпионат!")\nprint("Удачи!")\nprint("Счёт: 0")\nprint("Игроков: 2")\n' > main.py
git add main.py
git commit -m "add player count line"
[feature-scoreboard 074d80c] add player count line
1 file changed, 1 insertion(+)
Посмотрим на историю целиком, с новым флагом:
git log --oneline --graph --all
* 074d80c add player count line
* dd98912 add score line
* fe59fd2 add gitignore for drafts
* 2f16b54 add welcome message
* 334596a add main screen
Прямая линия без единой развилки — и это не ошибка вывода. Веток в репозитории уже две, но master до сих пор указывает на fe59fd2 — тот самый коммит, что лежит прямо посреди этой цепочки. Разветвление на графике появляется только там, где у веток разные дальнейшие коммиты; здесь master попросту ещё не ушёл никуда от точки, где родилась feature-scoreboard, поэтому взгляду не на чем расходиться. Сверим это командой git branch, но теперь с -v — «verbose», показать ещё и коммит, на котором стоит каждая ветка:
git branch -v
* feature-scoreboard 074d80c add player count line
master fe59fd2 add gitignore for drafts
Вот она, разница, спрятанная за одинаковой картинкой графа: feature-scoreboard ушла на два коммита вперёд, а master стоит там же, где стояла до git switch -c.
git merge: перемотка (fast-forward)
Эксперимент удался — пора вернуть счёт и число игроков в основную ветку. Встаём на ветку-приёмник и зовём к себе изменения:
git switch master
Switched to branch 'master'
cat main.py
print("Привет, чемпионат!")
print("Удачи!")
Именно та старая версия — ведь на master мы ничего не коммитили, пока сидели в feature-scoreboard. Сливаем:
git merge feature-scoreboard
Updating fe59fd2..074d80c
Fast-forward
main.py | 2 ++
1 file changed, 2 insertions(+)
cat main.py
print("Привет, чемпионат!")
print("Удачи!")
print("Счёт: 0")
print("Игроков: 2")
Обе строки на месте. А теперь ключевой момент, который часто ускользает: посмотрим на историю снова.
git log --oneline --graph --all
* 074d80c add player count line
* dd98912 add score line
* fe59fd2 add gitignore for drafts
* 2f16b54 add welcome message
* 334596a add main screen
Ни одной новой строки по сравнению с выводом до слияния — те же пять коммитов, те же хеши. Fast-forward в принципе не создаёт нового коммита: он буквально «перематывает» закладку master вперёд по уже существующей цепочке, как будто с самого начала коммитил именно в master. Это возможно только потому, что master за время работы над веткой не получила ни одного собственного коммита — ей было некуда «расходиться» с feature-scoreboard, и слияние превращается в простое перемещение указателя.
Ветка своё дело сделала — коммиты уже в master, закладка feature-scoreboard больше не нужна:
git branch -d feature-scoreboard
Deleted branch feature-scoreboard (was 074d80c).
-d — «удалить, но безопасно»: Git заранее проверяет, что все коммиты ветки уже присутствуют где-то ещё (в нашем случае — в master, после fast-forward), и только тогда соглашается удалить закладку. Сами коммиты dd98912 и 074d80c при этом никуда не делись — они остаются в истории master, просто больше не подписаны отдельным именем ветки.
git merge: настоящее слияние с коммитом (three-way merge)
Fast-forward — самый простой случай, но он работает только пока master стоит на месте. Устроим ситуацию, где обе ветки успели уйти вперёд каждая по-своему — и посмотрим, как Git справляется с этим уже без перемотки.
git switch -c feature-timer
Switched to a new branch 'feature-timer'
printf 'print("Привет, чемпионат!")\nprint("Удачи!")\nprint("Счёт: 0")\nprint("Игроков: 2")\nprint("Таймер: 5:00")\n' > main.py
git add main.py
git commit -m "add timer line"
[feature-timer 1c96c1a] add timer line
1 file changed, 1 insertion(+)
Пока ветка feature-timer дописывала main.py, вернёмся на master и сделаем там свой коммит — но не в том же файле, а в .gitignore, чтобы точно не пересечься:
git switch master
Switched to branch 'master'
printf 'draft.txt\n*.log\n' > .gitignore
git add .gitignore
git commit -m "ignore log files"
[master d895572] ignore log files
1 file changed, 1 insertion(+)
Теперь у обеих веток есть собственный, ничем не связанный друг с другом коммит. Наконец-то граф покажет настоящую развилку:
git log --oneline --graph --all
* 1c96c1a add timer line
| * d895572 ignore log files
|/
* 074d80c add player count line
* dd98912 add score line
* fe59fd2 add gitignore for drafts
* 2f16b54 add welcome message
* 334596a add main screen
Вот теперь линия разошлась на два столбика: верхний * — коммит feature-timer, соседний * чуть правее — коммит master, а |/ внизу показывает место, где они разделились, — коммит 074d80c, их общий предок. Сольём:
git merge feature-timer
Merge made by the 'ort' strategy.
main.py | 1 +
1 file changed, 1 insertion(+)
На этот раз ни слова про fast-forward — вместо этого «слияние сделано стратегией ort» (это имя алгоритма слияния по умолчанию с Git 2.33, сменившего более старый recursive; расшифровывать название не нужно, достаточно знать, что это обычный, стандартный способ). Раз обе ветки успели разойтись, Git не может просто передвинуть закладку — он создаёт новый коммит, у которого не один родитель, как обычно, а сразу два: последний коммит master и последний коммит feature-timer. Посмотрим на этот коммит напрямую:
git log -1
commit f958c3f7168aa399da131b53a68694e8b8c18989
Merge: d895572 1c96c1a
Author: Ivan Petrov <ivan@example.com>
Date: Tue Sep 1 02:51:16 2026 +0000
Merge branch 'feature-timer'
Взглянем на итоговую картину:
git log --oneline --graph --all
* f958c3f Merge branch 'feature-timer'
|\
| * 1c96c1a add timer line
* | d895572 ignore log files
|/
* 074d80c add player count line
* dd98912 add score line
* fe59fd2 add gitignore for drafts
* 2f16b54 add welcome message
* 334596a add main screen
Ромб из символов *, |\, * |, |/ — это и есть слияние на письме: f958c3f наверху — новый merge-коммит, от него вниз расходятся две линии к обоим родителям (d895572 и 1c96c1a), а ниже — |/ — они снова сходятся к общему предку 074d80c, откуда когда-то разошлись. Именно так рисуют дерево коммитов в тренажёре из следующего раздела — теперь за каждой линией стоит команда, которую ты только что запускал сам. Дело сделано — уборка:
git branch -d feature-timer
Deleted branch feature-timer (was 1c96c1a).
Проект PGK
Как это устроено под капотом
А теперь закрепи разницу между перемоткой и настоящим слиянием руками — получи в одном репозитории оба ответа Git: сначала Fast-forward, затем Merge made by the 'ort' strategy.
Проект PGK
Как это устроено под капотом
Конфликт: когда Git не может выбрать сам
До сих пор обе ветки трогали разные строки или разные файлы — Git прекрасно справлялся сам. А что если обе ветки поменяют одну и ту же строку по-разному? Проверим прямо на первой строке main.py — «Привет, чемпионат!» давно пора обновить под финал.
git switch -c feature-name-fix
Switched to a new branch 'feature-name-fix'
printf 'print("Привет, финалисты!")\nprint("Удачи!")\nprint("Счёт: 0")\nprint("Игроков: 2")\nprint("Таймер: 5:00")\n' > main.py
git commit -am "update greeting for finals"
[feature-name-fix f1d457a] update greeting for finals
1 file changed, 1 insertion(+), 1 deletion(-)
Флаг -a у commit — «all»: сразу добавляет в коммит изменения уже отслеживаемых файлов, без отдельного git add. Работает только для файлов, которые Git уже знает, — на untracked-файлы, как в прошлой главе, не действует.
Вернёмся на master и поправим ту же самую первую строку — но по-другому:
git switch master
Switched to branch 'master'
printf 'print("Привет, чемпионы!")\nprint("Удачи!")\nprint("Счёт: 0")\nprint("Игроков: 2")\nprint("Таймер: 5:00")\n' > main.py
git commit -am "update greeting to champions"
[master 4929e77] update greeting to champions
1 file changed, 1 insertion(+), 1 deletion(-)
Оба коммита изменили ровно одну и ту же строку — по-разному. Сливаем и смотрим, что скажет Git:
git merge feature-name-fix
Auto-merging main.py
CONFLICT (content): Merge conflict in main.py
Automatic merge failed; fix conflicts and then commit the result.
Ничего не сломалось. Команда завершилась с ненулевым кодом возврата — это ожидаемо, Git таким образом сигналит скриптам и IDE «слияние остановлено», а не «произошёл сбой». Спросим git status, что происходит:
git status
On branch master
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: main.py
no changes added to commit (use "git add" and/or "git commit -a")
Откроем сам файл — Git уже вставил в него разметку конфликта прямо на месте спорной строки:
cat main.py
<<<<<<< HEAD
print("Привет, чемпионы!")
=======
print("Привет, финалисты!")
>>>>>>> feature-name-fix
print("Удачи!")
print("Счёт: 0")
print("Игроков: 2")
print("Таймер: 5:00")
Читается так: между <<<<<<< HEAD и ======= — версия ветки, на которой ты стоишь сейчас (HEAD — указатель Git на текущую ветку и её последний коммит, тот же master); между ======= и >>>>>>> feature-name-fix — версия вливаемой ветки, её имя подписано прямо в последнем маркере. Остальные четыре строки файла не тронуты — конфликт локальный, ровно там, где обе стороны реально разошлись.
Алгоритм решения всегда один и тот же:
- Найди маркеры
<<<<<<<— их всегда ровно три, как в этом файле. - Реши, какой текст должен остаться: одна из версий, обе, или вообще новая формулировка.
- Удали все три строки-маркера —
<<<<<<<,=======и>>>>>>>— целиком, вместе с именами веток. - Скажи Git, что разобрался:
git add, затемgit commit.
Объединим обе идеи в одну строку:
printf 'print("Привет, чемпионы-финалисты!")\nprint("Удачи!")\nprint("Счёт: 0")\nprint("Игроков: 2")\nprint("Таймер: 5:00")\n' > main.py
cat main.py
print("Привет, чемпионы-финалисты!")
print("Удачи!")
print("Счёт: 0")
print("Игроков: 2")
print("Таймер: 5:00")
Ни единого символа <, = или > не осталось — только чистый код. Отмечаем, что конфликт решён, и смотрим на статус ещё раз:
git add main.py
git status
On branch master
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: main.py
«Все конфликты исправлены, но слияние ещё не завершено» — Git заметил, что спорных мест больше нет, и ждёт последнего шага:
git commit
[master 556acb5] Merge branch 'feature-name-fix'
Обрати внимание: в этот раз после git commit не понадобился -m — раз мы посреди слияния, Git уже знает, что писать (то же поведение, что и у бесконфликтного merge-коммита из прошлого раздела), и в интерактивном терминале просто открыл бы редактор с готовым текстом. Проверим итог:
git log -1
commit 556acb5b08dd147144c07bae1a9ae545bdc08a5a
Merge: 4929e77 f1d457a
Author: Ivan Petrov <ivan@example.com>
Date: Tue Sep 1 02:51:16 2026 +0000
Merge branch 'feature-name-fix'
Merge-коммит с двумя родителями — ровно как и в разделе про feature-timer: с точки зрения истории конфликтный merge и бесконфликтный устроены одинаково, разница была только в промежуточном шаге, где пришлось руками решить, какая строка правильная. Вся картина целиком:
git log --oneline --graph --all
* 556acb5 Merge branch 'feature-name-fix'
|\
| * f1d457a update greeting for finals
* | 4929e77 update greeting to champions
|/
* f958c3f Merge branch 'feature-timer'
|\
| * 1c96c1a add timer line
* | d895572 ignore log files
|/
* 074d80c add player count line
* dd98912 add score line
* fe59fd2 add gitignore for drafts
* 2f16b54 add welcome message
* 334596a add main screen
Два ромба один под другим — оба слияния, которые сделали в этой главе, целиком видны в одном графе. И последняя проверка: раз feature-name-fix теперь полностью влита (её коммит f1d457a стал предком master через merge-коммит), безопасное удаление сработает без вопросов:
git branch -d feature-name-fix
Deleted branch feature-name-fix (was f1d457a).
Проект PGK
Как это устроено под капотом
Типичные ответы Git на четыре частые ситуации
Не все из них — ошибки в строгом смысле; одна вообще просто информационная строка. Но каждая встречается почти на каждом проекте, и тексты ниже — снова дословный, живьём проверенный вывод, а не пересказ по памяти.
1. Already up to date — уже актуально
Попробуем влить feature-name-fix ещё раз — она ведь уже влита:
git merge feature-name-fix
Already up to date.
Перевод: «уже актуально». Причина: это не ошибка и не предупреждение — Git проверил, что последний коммит feature-name-fix уже является предком master (он и правда стал им после прошлого слияния), и попросту нечего добавлять. Что делать: ничего — если видишь эту строку, значит, всё нужное уже здесь.
2. invalid reference — такой ветки не существует
git switch does-not-exist
fatal: invalid reference: does-not-exist
Перевод: «фатальная ошибка: недействительная ссылка». Причина: Git ищет ветку, тег или коммит с именем does-not-exist — и не находит ничего похожего. Все фатальные ошибки Git возвращают код 128, если это важно для скрипта или CI. Как починить: свериться со списком реальных имён через git branch, поправить опечатку — либо, если ветки правда ещё нет, добавить -c, чтобы её создать.
3. изменения помешали переключиться
Заведём вторую ветку с изменением в main.py и вернёмся на master:
git switch -c feature-blocker
printf 'print("Привет, чемпионы-финалисты!")\nprint("Удачи!")\nprint("Счёт: 0")\nprint("Игроков: 2")\nprint("Таймер: 5:00")\nprint("blocker branch line")\n' > main.py
git commit -am "add blocker branch line"
git switch master
Теперь, не коммитя, поправим main.py прямо на master и попробуем переключиться на feature-blocker, которая тоже трогает этот файл:
printf 'print("Привет, чемпионы-финалисты!")\nprint("Удачи!")\nprint("Счёт: 0")\nprint("Игроков: 2")\nprint("Таймер: 5:00")\nprint("незакоммиченная правка")\n' > main.py
git switch feature-blocker
error: Your local changes to the following files would be overwritten by checkout:
main.py
Please commit your changes or stash them before you switch branches.
Aborting
Перевод: «твои локальные изменения в следующих файлах будут перезаписаны переключением (checkout)... закоммить изменения или спрятать их (stash) перед переключением веток». Обрати внимание на слово checkout внутри сообщения — команда, которую мы запускали, называлась switch, но внутри Git это ошибка того же самого старого механизма, и сообщение об этом до сих пор не переименовали. Путаться не стоит: это именно про твой git switch. Причина: у незакоммиченной правки и у ветки, на которую переключаешься, разные версии одной и той же строки — Git отказывается тихо стереть работу, которую ты ещё не сохранил ни в одном коммите. Как починить: один из трёх путей — закоммитить правку (git add + git commit), спрятать её (git stash, тема для отдельного разбора), либо просто отменить, если она не нужна:
git restore main.py
git status
On branch master
nothing to commit, working tree clean
4. git branch -d отказывается удалить невлитую ветку
git switch -c feature-unmerged
echo "unmerged draft idea" > idea.txt
git add idea.txt
git commit -m "wip: unmerged idea"
git switch master
git branch -d feature-unmerged
error: the branch 'feature-unmerged' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feature-unmerged'
Перевод: «ветка не полностью влита. Если уверен, что хочешь удалить её, выполни git branch -D». Причина: -d — безопасное удаление: коммит wip: unmerged idea не входит ни в одну другую ветку, и удаление закладки означало бы, что до него больше не добраться обычным путём (сам объект в .git/objects какое-то время ещё проживёт, но найти его будет заметно сложнее). Как починить: либо сначала слить ветку (как во всех примерах этой главы), либо, если она правда больше не нужна, — заглавная -D, которая делает то же самое, но без проверки:
git branch -D feature-unmerged
Deleted branch feature-unmerged (was b488e60).
-D — это буквально -d --force: разница ровно в одной проверке, но именно она отделяет «удалить хвост уже слитой работы» от «выбросить единственную копию незавершённой». Пользоваться заглавной версией стоит осознанно, а не по привычке.
Символы и команды главы
| Команда / символ | Что делает | Пример |
|---|---|---|
git branch | показывает список веток; текущая отмечена звёздочкой | git branch |
git branch -v | список веток вместе с последним коммитом каждой | git branch -v |
git switch <name> | переключает рабочую папку и HEAD на существующую ветку | git switch master |
git switch -c <name> | создаёт новую ветку и сразу переключается на неё | git switch -c fix-title |
git checkout -b <name> | старый эквивалент git switch -c | git checkout -b fix-title |
git merge <name> | вливает коммиты указанной ветки в текущую | git merge feature-timer |
| Fast-forward | тип слияния без нового коммита: закладка ветки просто сдвигается вперёд | Fast-forward в выводе merge |
Merge made by the 'ort' strategy. | настоящее слияние: создан merge-коммит с двумя родителями | вывод git merge при разошедшихся ветках |
<<<<<<< ======= >>>>>>> | разметка конфликта: своя версия / чужая версия | внутри файла во время конфликта |
git status во время конфликта | показывает both modified и подсказки для решения | git status |
git merge --abort | отменяет незавершённое слияние, откатывает как до merge | git merge --abort |
git log --graph --all | рисует ASCII-дерево всех веток разом | git log --oneline --graph --all |
git branch -d <name> | безопасно удаляет уже влитую ветку | git branch -d fix-title |
git branch -D <name> | удаляет ветку без проверки, что она влита | git branch -D fix-title |
Already up to date. | сливать нечего — коммит уже есть в текущей ветке | вывод git merge |
| HEAD | указатель на текущую ветку и её последний коммит | видно в <<<<<<< HEAD |
Слова главы
Потренируйся печатать
Цель: скорость ≥ 140 зн/мин, точность ≥ 90%
git switch -c feature-timer
Полный рабочий цикл слияния и уборки — набери его целиком одной строкой, как делают в реальной работе:
Цель: скорость ≥ 150 зн/мин, точность ≥ 90%
git switch master && git merge feature-timer && git branch -d feature-timer
Углубиться
Три снимка для одного слияния. Настоящее слияние (не перемотку) Git строит не из двух версий файла, а из трёх: последнего коммита текущей ветки, последнего коммита вливаемой и их общего предка — коммита, где ветки когда-то разошлись (в примере с feature-timer это был 074d80c). Это называется трёхсторонним слиянием (three-way merge). Строки, изменённые только с одной стороны относительно предка, Git берёт автоматически; конфликтом объявляет только те строки, что разошлись с обеих сторон по-разному. Вот почему конфликты на практике случаются заметно реже, чем кажется новичкам, — большая часть правок в реальном проекте просто не пересекается.
У разметки конфликта можно попросить третий блок. По умолчанию Git показывает только «твоя версия» и «чужая версия». Команда git config merge.conflictStyle zdiff3 включает более подробный стиль — добавляет между ними ещё и то, как строка выглядела у общего предка. Проверено вживую на этой же самой строке из главы:
<<<<<<< HEAD
print("Привет, чемпионы!")
||||||| 2124d9d
print("Привет, чемпионат!")
=======
print("Привет, финалисты!")
>>>>>>> feature-name-fix
print("Удачи!")
Средний блок между ||||||| и ======= — общий предок: то, с чего обе версии стартовали. Часто именно это подсказывает, чья правка «новее по смыслу», а не просто «чья была написана позже по хронологии».
git merge --abort — кнопка «отменить всё». Если посреди решения конфликта понял, что сливать было рано или не ту ветку, эта команда откатывает рабочую папку ровно к состоянию перед git merge — как будто слияния не начиналось вовсе. Работает, только пока конфликт не закоммичен: после git commit слияние уже завершено, и отменить его — отдельная тема (git revert, не в этой главе).
Дальше по теме:
- git switch — официальная документация — все флаги команды переключения, включая работу с detached HEAD
- git merge — официальная документация — раздел «How conflicts are presented» подробно разбирает разметку конфликта и стили
diff3/zdiff3 - git branch — официальная документация — полный список флагов, включая
-m(переименовать) и-a(показать вместе с удалёнными)
Куда дальше: проверенные ресурсы
- Pro Git: Основы ветвления и слияния — официальный перевод главной книги про Git; та же тема, что в этой главе, с дополнительными картинками деревьев коммитов.
- Хабр: раскладываем Git по полочкам — терминология — чёткие определения branch, merge и fast-forward с диаграммами, если после этой главы хочется увидеть те же понятия ещё раз другими словами.
- Хабр: как решить конфликт в Git — merge, rebase, cherry-pick — конфликты не только при merge: та же логика возникает при
rebaseиcherry-pick, с которыми ты столкнёшься позже. - Atlassian: Git Merge — разбор fast-forward и трёхстороннего слияния с наглядными схемами веток на русском.
- git-switch — man-страница команды переключения веток.
- git-merge — man-страница слияния: стратегии, флаги, разбор конфликтов.
- git-branch — man-страница управления ветками: создание, удаление, переименование.
-
Шпаргалка по Git на dev-notes.ru — все команды главы (и не только) на одной странице: удобно держать открытой во время практики или на чемпионате.
-
Видео и материалы сообщества — ниже — ролики по теме и ссылки от студентов и преподавателей смотри в самом низу страницы.
Челлендж ⭐
Устрой конфликт своими руками в отдельном свежем репозитории — и победи его самостоятельно, не подглядывая в решение раньше времени.
- Создай папку
team, зайди в неё и сделайgit init. Создай файлteam.txtсо строкойНазвание команды: пока думаеми закоммить его. - Создай ветку
new-nameи переключись на неё одной командой (git switch -c). В файле замени строку наНазвание команды: Молния. Закоммить. - Вернись на
master(илиmain— какое имя досталось твоей ветке приgit init, то и используй). Замени ту же строку наНазвание команды: Огонь. Закоммить. - Влей
new-nameв основную ветку. Git объявит конфликт — так и задумано. - Реши конфликт: оставь в файле то название, которое нравится больше, или придумай третье, — убери маркеры, заверши слияние.
Проверка: git log --oneline --graph показывает коммит слияния с двумя родителями, а cat team.txt — чистый файл без единого символа <, = или >.
⭐ Ступень со звёздочкой. Прежде чем выполнять git branch -d new-name в конце, ответь себе вслух, не заглядывая в терминал: сработает ли эта команда без ошибки и без флага -D, хотя ветка только что участвовала в конфликте? Сравни свой ответ с разделом «Типичные ответы Git», пункт про not fully merged — а затем проверь командой.
Решение (сначала попробуй сам)
mkdir team
cd team
git init
echo "Название команды: пока думаем" > team.txt
git add team.txt
git commit -m "начальное название"
git switch -c new-name
echo "Название команды: Молния" > team.txt
git commit -am "вариант Молния"
git switch master
echo "Название команды: Огонь" > team.txt
git commit -am "вариант Огонь"
git merge new-name
# Auto-merging team.txt
# CONFLICT (content): Merge conflict in team.txt
Открой team.txt в редакторе (например, nano team.txt) — внутри будет знакомая по главе разметка:
<<<<<<< HEAD
Название команды: Огонь
=======
Название команды: Молния
>>>>>>> new-name
Оставь одну строку, например Название команды: Огненная Молния, удали три строки с маркерами, сохрани файл и заверши слияние:
git add team.txt
git commit -m "решили конфликт: объединили названия"
git log --oneline --graph
Ответ на ⭐: git branch -d new-name сработает без единой ошибки и без -D. Причина — ровно та же, что и с feature-name-fix в главе: после того как конфликт решён и git commit завершил слияние, коммит из new-name становится предком master через новый merge-коммит. Условие для -d — «коммиты ветки уже присутствуют где-то ещё» — выполнено, даже несмотря на то, что путь к этому был не прямым fast-forward, а конфликтным слиянием.
Сводная проверка по всей главе: 8 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Что должен уметь
- Объяснять, что такое ветка (branch): лёгкий указатель на коммит, а не копия файлов проекта — и почему её создание мгновенно.
- Создавать ветку и переключаться между ветками через
git switch/git switch -c— и узнавать ту же операцию в старой записиgit checkout -b. - Отличать fast-forward от настоящего merge-коммита по выводу
git merge, объяснять, почему у первого нет нового коммита, а у второго — два родителя. - Читать ASCII-граф
git log --graph --all: находить точки расхождения и слияния веток. - Проходить конфликт от начала до конца: понимать
CONFLICTиgit statusво время конфликта, читать разметку<<<<<<</=======/>>>>>>>, убирать маркеры, завершать черезgit addиgit commit. - Узнавать по точному тексту четыре типичные ситуации —
Already up to date,invalid reference, заблокированное незакоммиченными правками переключение, отказgit branch -dудалить невлитую ветку — и знать, что делать в каждой, включая разницу между-dи-D. - Убирать за собой уже влитые ветки командой
git branch -d.
Комментарии
Комментарии появятся после настройки. Нужен аккаунт GitHub — вход прямо в виджете выше.