Перейти к основному содержимому
07ФУНДАМЕНТ · ГЛАВА 07Git: ветки и merge

Git: ветки и merge

Прочитано 0%Квизы 0/6Тренажёры 0/5Уровень 1 · Новичок

Зачем это нужно

Ты уже умеешь сохранять состояние проекта коммитами. Но вот ситуация: проект работает, и тебе хочется попробовать рискованную идею — переделать оформление, переписать кусок кода. А вдруг не получится? Ломать работающую версию страшно, а копировать папку проекта «на всякий случай» — снова путь к хаосу из project_final_2_new, с которого начиналась прошлая глава. Для этого в Git есть ветка (branch) — параллельная линия истории внутри одного и того же репозитория. Ты сворачиваешь с основной дороги, экспериментируешь сколько хочешь, и основная версия при этом остаётся нетронутой: не вышло — просто вернулся обратно, как будто ничего и не было. А получилось — результат вливается в основную ветку одной командой: это называется слияние (merge).

GIT · ВЕТКИ И СЛИЯНИЕmasterfeaturemerge-коммиту коммита слияния два родителя — по одному от каждой ветки; цвет линии = ветка
Коммиты — точки на линии. Своя ветка расходится от общего предка и позже сливается обратно; цвет линии показывает, какой ветке принадлежит коммит.

Так устроена работа любой команды разработчиков, и на чемпионате «Профессионалы» в том числе: каждый участник — а иногда и один участник, но по разным задачам — пилит свою часть в своей ветке, а потом всё сливается вместе в одну историю. Иногда при слиянии Git встречает противоречие — конфликт (conflict): обе ветки поменяли одну и ту же строку по-разному, и машина не готова угадывать, кто прав. Звучит грозно, но на деле это обычная рабочая ситуация с понятным алгоритмом решения — научиться его проходить спокойно и быстро важнее, чем никогда не встречать конфликтов вообще. В этой главе, как и в прошлой, весь путь разобран по молекулам: что происходит внутри .git после каждой команды и что означает каждое слово в ответе терминала.

Сквозной пример: ветки в том же my-app

Продолжаем ровно тот проект my-app, на котором остановилась прошлая глава: три коммита, файл main.py с двумя строками приветствия, файл .gitignore с одной записью draft.txt. Если у тебя этой папки уже нет — не страшно, весь путь читается и без повторного запуска: главное, что каждая команда и весь вывод терминала ниже — не выдумка. Это дословный вывод настоящего Git 2.43.0 на чистой Ubuntu 24.04, полученный специально для этой главы на следующий день после главы про первый коммит — те же самые пять команд git initgit addgit 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.

Отвечено вопросов: 0 из 4
Что физически происходит с файлами проекта на диске в момент git switch -c feature-scoreboard?
После git switch -c feature-scoreboard команда git branch показывает feature-scoreboard со звёздочкой и master без неё. Что это значит?
Флаг -c у команды git switch — для чего он?
В старой команде git checkout -b feature-scoreboard и новой git switch -c feature-scoreboard — в чём разница по результату?
Отвечено верно: 0 из 4

Коммиты внутри ветки: 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 · MASTER НЕ ШЕВЕЛИТСЯmasterfeature-scoreboardобе закладки стартовали в одной точке — feature-scoreboard уехала вперёд на два коммита, master ждёт на месте
Обе закладки стартовали в одной точке — feature-scoreboard уехала вперёд вместе с HEAD, master осталась ждать на месте.

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, просто больше не подписаны отдельным именем ветки.

Отвечено вопросов: 0 из 4
Git ответил Updating fe59fd2..074d80c / Fast-forward. Создался ли новый коммит слияния?
При каком условии git merge вообще способен сделать fast-forward?
После fast-forward слияния feature-scoreboard в master — что покажет git log --oneline --all: пять коммитов или семь (пять старых плюс два новых)?
Что реально делает git branch -d feature-scoreboard после успешного fast-forward слияния?
Отвечено верно: 0 из 4

git merge: настоящее слияние с коммитом (three-way merge)

Fast-forward — самый простой случай, но он работает только пока master стоит на месте. Устроим ситуацию, где обе ветки успели уйти вперёд каждая по-своему — и посмотрим, как Git справляется с этим уже без перемотки.

GIT MERGE · ДВА СЦЕНАРИЯfast-forwardметка едет вперёдновый коммит не создаётсяthree-way mergemerge-коммит: два родителяобе ветки успели уйти вперёд
Fast-forward просто передвигает метку вперёд по прямой; когда обе ветки успели разойтись, Git создаёт merge-коммит сразу с двумя родителями.
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).
Отвечено вопросов: 0 из 4
Чем ситуация с feature-timer отличалась от предыдущей с feature-scoreboard и привела не к fast-forward, а к merge-коммиту?
В выводе git log -1 у merge-коммита есть строка Merge: d895572 1c96c1a. Что она означает?
В графе git log --graph коммит merge-коммита отмечен * , а строкой ниже — |\, ещё ниже — |/. Что показывает такая картинка?
Сообщение merge-коммита Merge branch 'feature-timer' появилось само. Что произошло бы в обычном терминале (не в скрипте) на этом шаге?
Отвечено верно: 0 из 4
Квест «Создай ветку, закоммить в ней и слей её в main»: слей ветку командой git merge
Репозиторий уже создан, есть первый коммит. Попробуй ветки: git branch, git switch, git merge. Список команд — help.
project (main) $
Команды: git config · init · status · add · commit -m · diff · log · branch · switch · merge · restore · edit <файл> <текст> — изменить файл · help · clear
Файлы проекта
README.mdзакоммичен
Проект PGK
История коммитов
начало проектаe7c9a4bHEAD → main
Как это устроено под капотом
Внутри — не настоящий git, а его модель в памяти: коммит — объект с хешем, сообщением, списком родителей и полным снимком файлов, а ветка — просто указатель на хеш коммита. Команды меняют этот граф объектов, при этом формулировки вывода сняты с настоящего git 2.x, чтобы глаз привыкал к реальному терминалу. Граф справа рисуется по тем же объектам: SVG-кружки — коммиты, линии — связи «родитель — потомок», а merge с расхождением создаёт коммит с двумя родителями, ровно как в настоящем git. Push и pull в режиме с origin — упрощённая пересылка недостающих коммитов между двумя такими репозиториями.

А теперь закрепи разницу между перемоткой и настоящим слиянием руками — получи в одном репозитории оба ответа Git: сначала Fast-forward, затем Merge made by the 'ort' strategy.

Квест «Fast-forward против merge-коммита»: получи оба вида слияния — перемотку и merge-коммит
шаг 1 — fast-forward: слей ветку, пока main не двигался (без расхождения) шаг 2 — merge-коммит: вернись, создай расхождение (свой коммит и в main, и в ветке) и слей снова
Репозиторий уже создан, есть первый коммит. Попробуй ветки: git branch, git switch, git merge. Список команд — help.
project (main) $
Команды: git config · init · status · add · commit -m · diff · log · branch · switch · merge · restore · edit <файл> <текст> — изменить файл · help · clear
Файлы проекта
README.mdзакоммичен
Проект PGK
История коммитов
начало проектаe7c9a4bHEAD → main
Как это устроено под капотом
Внутри — не настоящий git, а его модель в памяти: коммит — объект с хешем, сообщением, списком родителей и полным снимком файлов, а ветка — просто указатель на хеш коммита. Команды меняют этот граф объектов, при этом формулировки вывода сняты с настоящего git 2.x, чтобы глаз привыкал к реальному терминалу. Граф справа рисуется по тем же объектам: SVG-кружки — коммиты, линии — связи «родитель — потомок», а merge с расхождением создаёт коммит с двумя родителями, ровно как в настоящем git. Push и pull в режиме с origin — упрощённая пересылка недостающих коммитов между двумя такими репозиториями.

Конфликт: когда 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 · РАЗМЕТКА КОНФЛИКТАmain.py<<<<<<< HEADprint("Привет, чемпионы!")=======print("Привет, финалисты!")>>>>>>> feature-name-fixмежду маркерами — твоя версия (HEAD) сверху, версия вливаемой ветки снизу
Git не удаляет ни одну из версий — вставляет обе прямо в файл между маркерами и ждёт, пока ты сам решишь, что должно остаться.

Алгоритм решения всегда один и тот же:

  1. Найди маркеры <<<<<<< — их всегда ровно три, как в этом файле.
  2. Реши, какой текст должен остаться: одна из версий, обе, или вообще новая формулировка.
  3. Удали все три строки-маркера — <<<<<<<, ======= и >>>>>>> — целиком, вместе с именами веток.
  4. Скажи 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).
Квест «Измени одну и ту же строку в двух ветках, получи конфликт при merge и разреши его»: получи merge-конфликт и разреши его
Репозиторий уже создан, есть первый коммит. Попробуй ветки: git branch, git switch, git merge. Список команд — help.
project (main) $
Команды: git config · init · status · add · commit -m · diff · log · branch · switch · merge · restore · edit <файл> <текст> — изменить файл · help · clear
Файлы проекта
README.mdзакоммичен
Проект PGK
История коммитов
начало проектаe7c9a4bHEAD → main
Как это устроено под капотом
Внутри — не настоящий git, а его модель в памяти: коммит — объект с хешем, сообщением, списком родителей и полным снимком файлов, а ветка — просто указатель на хеш коммита. Команды меняют этот граф объектов, при этом формулировки вывода сняты с настоящего git 2.x, чтобы глаз привыкал к реальному терминалу. Граф справа рисуется по тем же объектам: SVG-кружки — коммиты, линии — связи «родитель — потомок», а merge с расхождением создаёт коммит с двумя родителями, ровно как в настоящем git. Push и pull в режиме с origin — упрощённая пересылка недостающих коммитов между двумя такими репозиториями.
Отвечено вопросов: 0 из 4
Что означают три маркера <<<<<<< HEAD, ======= и >>>>>>> feature-name-fix внутри файла с конфликтом?
После git merge с конфликтом команда завершилась ненулевым кодом возврата. Значит ли это, что что-то сломалось?
Каким должен быть файл после решения конфликта, прежде чем делать git add?
Почему после конфликтного слияния git commit сработал без флага -m?
Отвечено верно: 0 из 4

Типичные ответы 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: разница ровно в одной проверке, но именно она отделяет «удалить хвост уже слитой работы» от «выбросить единственную копию незавершённой». Пользоваться заглавной версией стоит осознанно, а не по привычке.

Отвечено вопросов: 0 из 4
git merge feature-name-fix во второй раз ответил Already up to date. Что произошло с историей?
git switch does-not-exist отвечает fatal: invalid reference: does-not-exist. Какой код возврата у любой fatal-ошибки Git?
Сообщение об ошибке при заблокированном переключении говорит "overwritten by checkout", хотя запускали git switch. Почему?
Чем git branch -D отличается от git branch -d?
Отвечено верно: 0 из 4

Символы и команды главы

Команда / символЧто делаетПример
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 -cgit 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отменяет незавершённое слияние, откатывает как до mergegit 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

Слова главы

branch
1 / 8

Потренируйся печатать

Цель: скорость ≥ 140 зн/мин, точность ≥ 90%

git switch -c feature-timer
клавиша ``клавиша 11клавиша 22клавиша 33клавиша 44клавиша 55клавиша 66клавиша 77клавиша 88клавиша 99клавиша 00клавиша --клавиша ==клавиша ⌫клавиша TabTabклавиша QQклавиша WWклавиша EEклавиша RRклавиша TTклавиша YYклавиша UUклавиша IIклавиша OOклавиша PPклавиша [[клавиша ]]клавиша \\клавиша Caps LockCaps Lockклавиша AAклавиша SSклавиша DDклавиша FFклавиша GGклавиша HHклавиша JJклавиша KKклавиша LLклавиша ;;клавиша ''клавиша EnterEnterклавиша ShiftShiftклавиша ZZклавиша XXклавиша CCклавиша VVклавиша BBклавиша NNклавиша MMклавиша ,,клавиша ..клавиша //клавиша ShiftShiftклавиша CtrlCtrlклавиша WinWinклавиша AltAltклавиша ПробелПробелклавиша AltAltклавиша WinWinклавиша MenuMenuклавиша CtrlCtrl
следующая: G — указательный левой руки

Полный рабочий цикл слияния и уборки — набери его целиком одной строкой, как делают в реальной работе:

Цель: скорость ≥ 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 — man-страница команды переключения веток.
  • git-merge — man-страница слияния: стратегии, флаги, разбор конфликтов.
  • git-branch — man-страница управления ветками: создание, удаление, переименование.

Челлендж ⭐

Устрой конфликт своими руками в отдельном свежем репозитории — и победи его самостоятельно, не подглядывая в решение раньше времени.

  1. Создай папку team, зайди в неё и сделай git init. Создай файл team.txt со строкой Название команды: пока думаем и закоммить его.
  2. Создай ветку new-name и переключись на неё одной командой (git switch -c). В файле замени строку на Название команды: Молния. Закоммить.
  3. Вернись на master (или main — какое имя досталось твоей ветке при git init, то и используй). Замени ту же строку на Название команды: Огонь. Закоммить.
  4. Влей new-name в основную ветку. Git объявит конфликт — так и задумано.
  5. Реши конфликт: оставь в файле то название, которое нравится больше, или придумай третье, — убери маркеры, заверши слияние.

Проверка: 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 — вход прямо в виджете выше.