Ветвление: указатели, HEAD, switch и чтение истории
Ветвление — та фича, радиоторой Git вообще существует. Ирония в том, что под капотом там почти ничего нет: ветка — это текстовый файл размером 41 байт со строкой хэша, а переключение веток — это перемещение одного указателя и перезапись файлов в рабочей директории (вся механика веток — Pro Git, глава 3 «Ветвление в Git»). Когда ты это видишь, страх перед «сложными» операциями вроде rebase и detached HEAD исчезает: перед тобой просто указатели и объекты.
В продакшене ветки — это конвейер разработки: feature-ветки, ветки релизов, hotfix-ветки. Ты каждый день создаёшь, переключаешь и удаляешь их. А ещё ты каждый день читаешь историю: кто когда что влил, откуда растёт ветка, где расходились ветки и слились обратно. Умение читать git log --graph и понимать, что HEAD — это просто файл, превращает хаос git-графа в понятную картину.
Разбираем механику указателей, HEAD во всех состояниях, разницу между switch, restore и старым checkout, управление ветками и чтение истории.
Ветка — это файл. HEAD — это тоже файл
Заголовок раздела «Ветка — это файл. HEAD — это тоже файл»Из главы про internals ты знаешь: refs/heads/main — текстовый файл с 40 hex-символами и переводом строки. Давай добьём картину до конца:
.git/├── HEAD # "ref: refs/heads/main" ← символическая ссылка└── refs/ └── heads/ ├── main # "9c2d5a8f..." ← 41 байт: хэш коммита └── feature # "71bf2c1d..."Создать ветку — значит создать файл. Удалить ветку — удалить файл. Сравни два способа:
git branch feature # porcelainecho "71bf2c1d..." > .git/refs/heads/feature # руками, то же самоеЧто происходит при git commit в ветку feature:
ДО коммита: ПОСЛЕ коммита:
refs/heads/feature ──▶ C refs/heads/feature ──▶ C' (новый!)refs/heads/main ──▶ C refs/heads/main ──▶ C (на месте)HEAD = "ref: refs/heads/feature" HEAD = "ref: refs/heads/feature" │ ▼ C' = commit с parent=C (parent ──▶ C, C не тронут)Ключевой вывод: коммит не «принадлежит» ветке — ветка просто указывает на коммит. Коммит знает своего родителя, но не знает, какие ветки на него смотрят. Ветка — закладка, которую можно передвигать куда угодно (git reset, см. главу про merge/rebase). Это инверсия ментальной модели «ветка — это линия коммитов»: на самом деле линия коммитов — это просто цепочка parent-указателей, а ветка — подвижная метка на одном из них.
A ←──── B ←──── C ←──── D ← цепочка parent-указателей ▲ ▲ │ │ refs/heads/ refs/heads/ main featureВетка feature «указывает» на D, но «содержит» A→B→C→D, потому что каждый коммит тянет за собой всю цепочку родителей. Отсюда же следует, почему ветки в Git дешёвые: создание ветки — это запись 41 байта на диск, никакого копирования файлов.
HEAD: указатель на указатель
Заголовок раздела «HEAD: указатель на указатель»HEAD — файл .git/HEAD. У него два формата:
Формат 1 — attached HEAD (символическая ссылка на ветку):
.git/HEAD: "ref: refs/heads/feature"
HEAD ──▶ refs/heads/feature ──▶ D
git commit: создаётся E, файл feature перезаписывается на хэш E, HEAD не меняетсяФормат 2 — detached HEAD (прямой хэш):
.git/HEAD: "71bf2c1d..." (сам хэш, без "ref:")
HEAD ──▶ C (ни одна ветка не указывает на C)
git commit: создаётся C', HEAD перезаписывается на C', НО ни одна ветка не двигается! C' достижим только через HEAD и reflogПроверить, в каком состоянии ты, проще всего так:
git status# "On branch feature" ← attached# "HEAD detached at 71bf2c1" ← detachedDetached HEAD: почему, опасности, что делать
Заголовок раздела «Detached HEAD: почему, опасности, что делать»Detached HEAD возникает в трёх ситуациях: git checkout <хэш коммита> (или тег), git checkout origin/main (remote-ref — не ветка!), редкие сценарии вроде git rebase на середине.
Диагностика и схема состояния:
A ←──── B ←──── C ←──── D ←──── E ▲ ▲ │ │ (тег v1.0) refs/heads/main
git checkout v1.0 (или git checkout B)
HEAD ──▶ B ← прямой хэш, "detached"
refs/heads/main ──▶ E ← main на месте, работаешь «мимо» неёЭто не ошибка и не поломка — Git просто честно говорит: «ты не на ветке, новые коммиты ни к какой ветке не привяжутся». Опасность одна: сделаешь коммиты в detached HEAD, переключишься на ветку — и новые коммиты останутся без имени. Через reflog они видны неделями, но без имени их легко забыть и потерять при gc (спасательные рецепты — Oh Shit, Git!?!).
Что делать:
# Вариант 1: просто смотрел — вернись на веткуgit switch main # или: git switch -
# Вариант 2: случайно сделал коммиты — привяжи их к веткеgit switch -c my-fix # создать ветку прямо на текущем HEADgit switch main
# Вариант 3: уже ушёл, коммиты потерялись — reflog спасётgit reflog # найди хэш потерянного коммитаgit switch -c rescued <хэш>switch / restore / checkout: разница
Заголовок раздела «switch / restore / checkout: разница»Исторически git checkout делал всё сразу: переключал ветки, восстанавливал файлы из index и из коммитов, создавал ветки с -b. Это приводило к неприятным сюрпризам: git checkout файл молча перезаписывал твои правки. В Git 2.23 (2019) команду разделили на две с явными именами (git-switch(1) и git-restore(1)):
| Задача | Современная команда | Старый аналог |
|---|---|---|
| Переключиться на ветку | git switch feature |
git checkout feature |
| Создать и переключиться | git switch -c feature |
git checkout -b feature |
| Восстановить файл из index | git restore файл |
git checkout -- файл |
| Восстановить файл из HEAD | git restore --source=HEAD файл |
git checkout HEAD -- файл |
| Убрать файл из index | git restore --staged файл |
git reset HEAD файл |
git switch main # только ветки: безопасноgit restore --staged app.ts # убрать из index (unstage), файл не трогатьgit restore app.ts # перезаписать файл из index (потеря правок!)git restore --source=HEAD~1 app.ts # перезаписать из конкретного коммитаПравило: switch — для перемещения между ветками, restore — для возврата файлов. Старый checkout ты всё равно встретишь в туториалах, старом коде и на серверах со старым Git — знать его обязательно, но в своём коде пиши явные команды.
Управление ветками: создать, удалить, переименовать
Заголовок раздела «Управление ветками: создать, удалить, переименовать»git switch -c feature/login # создать от текущего HEAD и переключитьсяgit branch feature/login # создать, остаться где былgit branch -m old-name new-name # переименовать текущую веткуgit branch -m new-name # переименовать, находясь в нейgit branch -d feature/login # удалить (безопасно: только если влита)git branch -D feature/login # принудительно удалить (есть невлитые коммиты)git branch --list 'feat/*' # отфильтроватьСхема удаления и его защиты:
main: A ←──── B ←──── C ▲feature: D ←──── E ← невлитые коммиты
git branch -d feature# error: The branch 'feature' is not fully merged.# (в D и E нет пути parent-ами к main — данные будут недостижимы)
решения: 1) git merge feature ← влить сначала (правильный путь) 2) git branch -D feature ← «да, я знаю, что теряю D и E» (объекты ещё живут в reflog ~90 дней, но без имени)Переименование — тоже просто файл: git branch -m фактически делает mv .git/refs/heads/old .git/refs/heads/new. Если ветка была текущей, Git дополнительно правит .git/HEAD (ref: refs/heads/old → ref: refs/heads/new).
Именование веток: конвенции, которые окупаются
Заголовок раздела «Именование веток: конвенции, которые окупаются»Ветке — имя сразу, а не «test2-final». Имя — первое, что видят в git log, code review и списках PR; оно должно отвечать на вопрос «что и зачем» без открытия диффа. Сложившиеся префиксы:
feature/login-oauth ← новая функциональностьfix/session-race ← исправление бага (часто с номером: fix/PROJ-1234)hotfix/payment-timeout ← срочный фикс в релиз (из Git Flow)refactor/api-typing ← поведение то же, код другойchore/update-deps ← рутина: зависимости, конфигиrelease/2.4.0 ← ветка подготовки релизаТри правила: строчные буквы и дефисы вместо пробелов (пробелы в именах веток — боль при скриптах и tab-completion), префикс-тип от conventional commits (история веток в remote читается как список задач), ссылка на тикет, если трекер есть (feature/PROJ-145-oauth). Команда git branch --list 'fix/*' тогда превращается в фильтр багфиксов, а не в лотерею.
Чтение истории: git log и ASCII-графы
Заголовок раздела «Чтение истории: git log и ASCII-графы»git log (см. git-log(1)) показывает историю, достижимую из HEAD. Ключевые режимы:
git log --oneline # один коммит = одна строкаgit log --oneline --graph # ASCII-граф ветвленийgit log --oneline --graph --all # + ВСЕ refs (все ветки, теги)git log --oneline --graph --decorate # + имена веток/тегов на коммитахgit log -p # + полный diff каждого коммитаgit log --stat # + статистика (файлы, +/- строк)git log --author="Иван" --since="2 weeks ago"git log --grep="feat(auth)" # поиск по сообщениямНастрой alias, это самая частая команда в жизни разработчика:
git config --global alias.lg "log --oneline --graph --decorate --all"git lgКак читать ASCII-граф (потренироваться на интерактивных схемах — в тренажёре Learn Git Branching). Пример вывода git lg после типичной feature-работы:
* 4e8f1c2 (HEAD -> main) Merge branch 'feature/login'|\| * 71bf2c1 (feature/login) feat(auth): экран входа| * 9c2d5a8 feat(auth): форма и валидация|/* 3b18e51 fix(ui): отступы в шапке* ce01362 initРазбор: коммиты идут сверху вниз от новых к старым; | — продолжение линии, \ и / — точки расхождения и слияния; (HEAD -> main) — где мы сейчас; (feature/login) — какие ветки на каких коммитах висят. Merge commit 4e8f1c2 имеет двух родителей — поэтому |\ слева: одна линия продолжилась вниз (первая родительская линия, от main), вторая свернулась из feature (/).
Симметричная картина для rebase (подробности — в следующей главе) будет выглядеть так — обрати внимание, как линия feature «переписывается» поверх main:
# после rebase feature на main:* 8a9d0e2 (feature/login) feat(auth): экран входа* 2f4b6c9 feat(auth): форма и валидация* 3b18e51 (main) fix(ui): отступы в шапке* ce01362 initОдинаковые сообщения, но другие хэши — rebase создал копии коммитов. Умение видеть это в графе — половина понимания rebase.
Для истории одного файла есть два полезных флага:
git log --follow --oneline -- src/app.ts # следить за переименованиямиgit blame src/app.ts # кто написал каждую строкуСвязь с соседними темами
Заголовок раздела «Связь с соседними темами»- Internals — ветки и HEAD здесь — это те самые файлы в
.git/refs/и.git/HEAD; commit — те самые неизменяемые объекты. - Staging/commit —
git switchтребует чистого статуса или аккуратного переноса правок (uncommitted changes следуют за тобой в другую ветку, если нет конфликтов) — фундамент для понимания stash. - Merge/rebase — вся механика слияний и перебазирования — это операции над указателями веток и цепочками родителей; теперь ты готов к ним по-настоящему.
Типичные ошибки и грабли
Заголовок раздела «Типичные ошибки и грабли»- Коммиты «в никуда» в detached HEAD. Симптом: после
git switch mainправки пропали. Лечение:git reflog+git switch -c имя <хэш>. Профилактика: смотри первую строкуgit statusперед каждым коммитом. git checkout origin/featureвместоgit switch feature. Ты попадёшь в detached HEAD на remote-ref, а не на локальную ветку. Правильно:git switch feature(Git сам создаст локальную ветку, отслеживающую remote).git branch -dругается, а нужно удалить срочно.-d— это фича, а не баг: он защищает от потери невлитых коммитов. Сначала подумай, куда делись D и E; если точно мусор —-D.- Путать
git restore файлиgit restore --staged файл. Первый затирает правки в рабочей директории (нельзя отменить!), второй просто убирает из index. Это самая частая потеря данных новичка. - Читать
git logбез--allи не видеть соседние ветки. История «исчезает» после переключения — нет, она просто не достижима из HEAD. Привыкай всегда к--graph --all(aliaslg). - Создавать ветки «от головы», когда HEAD в detached. Проверяй
git statusпередgit switch -c: если ты в detached, новая ветка укажет на «чужой» коммит, а не на конец main.
Вопросы на собеседовании
Заголовок раздела «Вопросы на собеседовании»- Что такое ветка в Git с точки зрения внутреннего устройства?
Текстовый файл в
.git/refs/heads/со строкой хэша коммита (41 байт). Создание ветки — запись файла; коммит в ветку — перезапись этого файла на хэш нового коммита. - Что такое HEAD и чем attached HEAD отличается от detached?
Файл
.git/HEAD. Attached:ref: refs/heads/<ветка>— HEAD указывает на ветку, коммит двигает ветку. Detached: прямой хэш коммита — коммиты никуда не привязываются и теряются при переключении (спасёт reflog). - Чем
git switchотличается отgit checkout?switch— только переключение веток (безопаснее);restore— восстановление файлов.checkout— историческая «комбайн», делающая оба действия, из-за чего легко потерять правки. - Почему ветки в Git дёшевы? Ветка — 41 байт на диск; объекты коммитов общие. Нет копирования файлов или истории — только новый указатель.
git branch -dотказала удалять ветку. Почему и что делать? В ветке есть коммиты, недостижимые из текущей (невлитые). Влить (git merge), или осознанно удалить принудительно (-D), или сначалаgit cherry-pick/перенести нужное.- Как прочитать ASCII-граф
git log --graph? Новые коммиты сверху;|— линия ветки;\//— расхождение/слияние; метки(HEAD -> main),(feature)— какие refs на каких коммитах; merge-коммит — точка с двумя входами. git checkout <тег>— куда попадёшь? В detached HEAD: тег — это ref на коммит, а не ветка. Для работы нужнаgit switch -c имя <тег>.
Практика
Заголовок раздела «Практика»- В песочном репозитории создай три ветки руками: через
git branchи через создание файлов в.git/refs/heads/(предварительно узнай хэши черезgit rev-parse). Критерий:git for-each-refиls .git/refs/heads/показывают одинаковые ветки. - Переключись на конкретный коммит по хэшу, сделай коммит, убедись через
git statusиcat .git/HEAD, что ты в detached HEAD. Затем «потеряй» коммит (переключись на main) и спаси его через reflog. Критерий: коммит снова адресуется именованной веткой. - Настрой alias
lgи разыграй сценарий: две ветки, три коммита в каждой, merge одной в другую. Критерий: поgit lgты можешь объяснить, какой коммит merge, кто его родители и почему граф выглядит именно так. - Создай ветку, сделай в ней коммит, вернись на main. Попробуй
git branch -d— получи ошибку. Влей ветку и удали. Критерий: после merge удаление проходит; объясни, что изменилось в графе достижимости. - Переименуй текущую ветку и найди, что изменилось в
.git/HEADи в.git/refs/heads/. Критерий:git statusпоказывает новое имя, старый файл refs исчез. - Отрепетируй «спасение из detached HEAD» три раза подряд: commit → switch away → reflog → switch -c. Критерий: время спасения меньше двух минут, без паники и без пересоздания репозитория.
Что почитать
Заголовок раздела «Что почитать»- Pro Git, глава 3: Ветвление в Git — лучшее объяснение указателей и трёх областей.
- git-switch(1), git-restore(1) — man-страницы — официальное описание новых команд и мотивация разделения checkout.
- Oh Shit, Git!?! (Katie Sylor-Miller) — каталог «спасательных» рецептов, включая detached HEAD.
- Learn Git Branching (интерактивный тренажёр) — лучший способ увидеть указатели в движении.
- git-log(1) — man-страница — все флаги форматирования и отбора.
- GitHub Flow — рабочий процесс на ветках, который используется в большинстве команд.