6.1 Ветка - это файл с одним SHA
Возьмём свежий репозиторий, сделаем коммит, заглянем в .git/:
ls .git/refs/heads/
# main
cat .git/refs/heads/main
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
Один файл. Внутри - 40 символов SHA коммита. Это всё.
Создать новую ветку - создать ещё один такой файл. Удалить - удалить файл. Передвинуть на другой коммит - переписать 40 символов. Все три операции для Git мгновенные.
git branch feature # создаст .git/refs/heads/feature
cat .git/refs/heads/feature
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
#
# Тот же SHA, что и у main - feature создан "от main".
После создания у нас два указателя на один коммит. Если сделать коммит на feature - её SHA сменится, main останется. Если сделать коммит на main - наоборот.
См. branch.
6.2 HEAD - указатель на указатель
У Git есть один особый файл - .git/HEAD. В нём написано, на
какую ветку мы сейчас смотрим.
cat .git/HEAD
# ref: refs/heads/main
Это значит: «текущая ветка - main». Все команды, которые
работают с «текущим коммитом» (git status, git commit,
git log), читают HEAD, идут по ссылке на refs/heads/main,
оттуда достают SHA, и с этим SHA работают.
Когда мы переключаемся на другую ветку:
git switch feature
cat .git/HEAD
# ref: refs/heads/feature
Поменялась одна строка в HEAD. И всё. Файлы в рабочем дереве
Git привёл к состоянию feature (если оно отличалось от main).
Когда коммитим:
- Создаётся commit-объект.
- Берётся текущая ветка из HEAD (
refs/heads/feature). - Эта ветка двигается на новый SHA.
- HEAD продолжает указывать на ту же ветку - то есть автоматом «следует» за ней.
Получается «указатель на указатель»: HEAD → ветка → коммит.
6.3 Detached HEAD
Иногда HEAD указывает не на ветку, а прямо на коммит:
cat .git/HEAD
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
Это detached HEAD - «оторванный». Происходит когда переключаешься на коммит напрямую, на тег, или на удалённую ветку без локального аналога:
git switch --detach v1.0 # явно
git checkout HEAD~3 # неявно
git checkout origin/main # неявно
В этом состоянии можно ходить по коду, делать локальные эксперименты. Можно даже коммитить - но если потом переключиться на ветку, не создав новую ветку из текущего HEAD, новые коммиты потеряются (никакой указатель на них не указывает).
Git при detached предупреждает большим оранжевым текстом и подсказывает:
You are in 'detached HEAD' state. You can look around, make
experimental changes ... If you want to create a new branch
to retain commits you create, you may do so by running:
git switch -c <new-branch-name>
Если ты эти коммиты сделал и нужно сохранить - создай ветку из текущего HEAD:
git switch -c rescue
Эта ветка теперь держит твои коммиты, они не потеряются. См. detached-head.
6.3.1 Подводный камень: detached HEAD после git pull --rebase
Менее очевидный сценарий, как попасть в detached HEAD -
прерванный git rebase. Если в середине rebase возникает
конфликт, и ты вводишь git rebase --abort - всё нормально.
Но если просто закроешь терминал или сделаешь git checkout
на другую ветку без --abort, можно остаться с HEAD,
указывающим на промежуточный «синтезированный» коммит.
Симптомы - git status пишет «HEAD detached at ...» вместо
«On branch ...». Лечится тем же - git switch -c rescue если
нужны коммиты, или git switch main если нет.
Reflog (git reflog) покажет полную последовательность
состояний HEAD и поможет понять, где именно произошёл detach.
6.4 switch и restore - новые команды
Исторически git checkout делал три вещи:
- переключал ветку
- откатывал файлы в рабочем дереве
- создавал ветку из коммита
Это было неудобно - одна команда для трёх операций приводила к опечаткам с потерей данных. В Git 2.23 (2019) ввели две специализированных:
git switch- только про веткиgit restore- только про файлы
# Старый стиль (всё ещё работает)
git checkout feature # переключиться на feature
git checkout -b feature # создать и переключиться
git checkout -- file.txt # откатить файл
git checkout HEAD~3 -- file.txt # достать файл из старого коммита
# Новый стиль
git switch feature # переключиться
git switch -c feature # создать и переключиться
git restore file.txt # откатить файл
git restore --source=HEAD~3 file.txt # достать файл из старого коммита
Семантика та же, но имена ясные. Если только учишь Git -
используй switch и restore. Если читаешь чужие туториалы и
видишь checkout - это то же самое, просто старая запись.
Одно отличие: switch отказывается переключаться, если есть
несохранённые изменения, которые будут перезаписаны. checkout
в той же ситуации мог проглотить и забыть; switch строже.
6.5 Базовый цикл с веткой
Типичная история работы с новой фичей:
# 1. Начинаем с обновлённой main
git switch main
git pull --ff-only
# 2. Создаём ветку для фичи
git switch -c feat/login-form
# 3. Работаем: правим, добавляем, коммитим
vim src/auth/login.ts
git add src/auth/login.ts
git commit -m "add login form skeleton"
# ... ещё несколько итераций ...
# 4. Запушили (с tracking)
git push -u origin feat/login-form
# 5. Создали PR, обсудили, поправили
# ... ещё коммиты, ещё push'ы ...
# 6. После merge PR - переключаемся обратно
git switch main
git pull --ff-only
# 7. Удаляем локальную ветку, она больше не нужна
git branch -d feat/login-form
-d (lowercase) удаляет только смерженные ветки. Если ветка
не была смержена в текущую - Git откажет. Это страховка от
потери работы. Принудительно - -D (uppercase), без вопросов.
Удалённая ветка после merge обычно удаляется автоматически
(на GitHub есть галочка в настройках репо «automatically delete
head branches»). Если не - git push origin --delete feat/login-form.
6.6 Fast-forward merge
Самая простая форма слияния. Возникает, когда у целевой ветки нет новых коммитов после ветвления.
до: main: A → B
feat: A → B → C → D
git switch main
git merge feat
после: main: A → B → C → D ← main "перемотался" на D
feat: A → B → C → D
Никакой merge-коммит не создаётся. Git просто переписывает
файл .git/refs/heads/main со старого SHA на новый. История
остаётся линейной.
Когда это работает: на main не было коммитов после того, как от неё отвели feat. Часто бывает с короткоживущими feature-ветками: завёл, поработал час, помержил.
Когда не работает: на main кто-то успел запушить свой коммит. Тогда true merge с трёхсторонним слиянием - см. главу 8.
Если хочешь, чтобы факт ветвления оставался в истории даже
при возможности fast-forward - git merge --no-ff feat. Это
создаст merge-коммит даже когда не обязательно. Полезно для
команд, которые хотят видеть «вот тут была фича».
См. fast-forward и merge.
6.7 Tracking-ветки
Локальная ветка может отслеживать удалённую. После настройки
Git знает, куда push и откуда pull, без явных аргументов.
# При первом push с -u - установить tracking
git push -u origin feature
▸теперь feature → origin/feature
# При создании от удалённой
git switch -c local-name origin/remote-name
▸tracking устанавливается автоматически
# Поменять tracking уже существующей ветки
git branch -u origin/main main
После tracking:
git status
# On branch feature
# Your branch is ahead of 'origin/feature' by 2 commits.
Эта строка про ahead/behind - это и есть результат tracking'а.
Без него status молчит про remote.
Все tracking-ветки лежат в .git/refs/remotes/origin/:
ls .git/refs/remotes/origin/
# HEAD main feature
Они обновляются при git fetch (и при pull, который внутри
делает fetch). Это «слепок» того, что на сервере. Сравнивая
main с origin/main, Git и считает ahead/behind.
6.8 Конвенции именования
Git не диктует, как называть ветки. Но команды и компании обычно договариваются о схеме. Самые частые:
main- основная ветка (раньшеmaster, многие проекты переименовали).develop- интеграционная (если используется GitFlow).feat/<short-name>илиfeature/<short-name>- фичи.fix/<short-name>илиbugfix/<short-name>- багфиксы.hotfix/<short-name>- срочные багфиксы в проде.release/<version>- подготовка релиза (в GitFlow).chore/<short-name>- обслуживающие задачи.docs/<short-name>- документация.
Префикс с слэшем - не магия, это просто текст. Git разрешает в именах веток слэши, и многие GUI/инструменты группируют по ним («все feat/...» в одной папке в дереве).
Иногда добавляют автора или тикет:
dmitry/feat/login-formfeat/JIRA-1234-login-form
Главное - договориться в команде один раз и придерживаться. Технически Git разрешит любое имя из ASCII без спецсимволов.
Подробнее про стратегии - глава 11 (GitFlow / GitHub Flow / trunk-based).
6.9 Что делать, когда наошибался
Несколько сценариев, которые случаются у всех.
«Закоммитил не в ту ветку».
Если коммит ещё не запушен:
# Запомним SHA коммита
LAST=$(git rev-parse HEAD)
# Откатим текущую ветку на один коммит назад
git reset --hard HEAD~1
# Переключимся на правильную ветку
git switch correct-branch
# Применим коммит сюда
git cherry-pick $LAST
cherry-pick - это «возьми один коммит из другого места и применить сюда». Подробно - в главе 8.
«Удалил ветку, в которой были несмерженные коммиты».
git reflog
# ... покажет последние позиции HEAD,
# в одной из них будет нужный SHA ...
git branch rescue <sha-из-reflog>
Reflog хранит позиции HEAD 30+ дней. Пока не было git gc --prune=now - коммиты живы. Подробно - в главе 9.
«Переключился, и пропали изменения».
Если изменения были застейджены или закоммичены - они в
reflog. Если были только в рабочем дереве (не add, не
commit) - потеряны. Это и есть причина коммитить чаще: не
жди готовности, ставь промежуточные снимки.
«Случайно сделал коммит в detached HEAD».
git switch -c rescue
# Ветка из текущего HEAD, коммит спасён
После этого можно git switch main и git merge rescue или
cherry-pick.
Уроки в sandbox
lab-6.1. Полный цикл с веткой: создать, разработать, помержить, удалить
Цель - пройти типичный workflow с веткой и увидеть всё в
git log --graph. После лабы становится понятно, чем
fast-forward отличается от non-ff, и как branch -d страхует
от потери работы.
Создай новый репозиторий, сделай первый коммит:
mkdir branches-lab && cd branches-lab && git init && echo "first" > a.txt && git add a.txt && git commit -m "first commit".Создай ветку feature и переключись на неё:
git switch -c feature. Проверь содержимое.git/HEADи.git/refs/heads/feature- оба должны совпадать с SHA первого коммита.Сделай два коммита на feature: добавь файлы
b.txtиc.txt, каждый коммитом по отдельности. После -git log --oneline --graph --all. Должно быть видно три коммита на feature, main стоит на первом.Вернись на main:
git switch main. Проверь, чтоb.txtиc.txtисчезли из рабочего дерева (они есть только на feature).Сделай fast-forward merge:
git merge feature. Снова посмотриgit log --oneline --graph --all. Заметь: merge-коммита нет, main просто перемотался.Удали ветку:
git branch -d feature. Заметь - Git разрешил, потому что feature смержена.Воспроизведи non-ff. Сделай два коммита на main, потом создай новую feat-ветку, два коммита там тоже, переключись на main,
git merge feat. Теперь Git создаст merge-коммит - это three-way merge, тема главы 8.Попробуй удалить эту feat без merge'а: верни состояние через
git reset --hard <sha-до-merge>, потомgit branch -d feat- увидишь отказ. Принудительно:git branch -D feat.
sandbox с автопроверкой - открыть в песочнице
Резюме
- Ветка в Git - файл в `.git/refs/heads/` с одним SHA коммита внутри. Создание, переключение, удаление - мгновенные операции.
- HEAD - указатель на текущую ветку (или прямо на коммит в detached-режиме). Коммиты двигают ветку, на которую указывает HEAD.
- Detached HEAD - HEAD указывает не на ветку. Безопасно для просмотра, но коммиты в этом состоянии теряются, если не создать ветку.
- С Git 2.23 есть `git switch` (для веток) и `git restore` (для файлов). Старый `git checkout` всё ещё работает, но новые команды менее опасны.
- Fast-forward merge - простое перемещение указателя без создания merge-коммита. Возможен, если у целевой ветки нет новых коммитов после ветвления.
- Tracking-ветки связывают локальную ветку с удалённой. После `-u` команды `push`/`pull` знают, куда идти, и `status` показывает ahead/behind.
- `git branch -d` отказывает удалять несмерженную ветку (страховка). `-D` - принудительно.
- Большинство «ошибок» с ветками поправимы через reflog. Главное - не делать `git gc --prune=now` сразу после катастрофы.
Контрольные вопросы
Чем `git switch feature` отличается от `git checkout feature`?
Показать ответ
Семантически - ничем, обе команды переключают ветку. Но
switchпоявился в 2019 как специализированная команда только для веток. Он строже: отказывается переключаться, если есть unstaged изменения, которые будут перезаписаны.checkoutв той же ситуации мог иногда «проглотить» правки без предупреждения. Если есть выбор - используйswitch(иrestoreдля файлов), это безопаснее.checkoutсохраняется для совместимости со старыми скриптами и туториалами.Я в detached HEAD сделал три коммита и переключился на main. Где мои коммиты, как их вернуть?
Показать ответ
Коммиты живы - они в
.git/objects/, но ни одна ветка на них не указывает. Откройgit reflog- увидишь последние позиции HEAD, включая SHA каждого из тех трёх коммитов. Возьми SHA последнего (вершины) и сделайgit branch rescue <sha>илиgit switch -c rescue <sha>. Теперь веткаrescueдержит твои коммиты, и они не потеряются при следующемgc. Дальше можноgit switch main && git merge rescueилиcherry-pick, смотря что нужно.Почему `git branch -d feature` иногда отказывает с «not fully merged», а иногда нет?
Показать ответ
Отказывает, если в feature есть коммиты, которых нет в текущей ветке (или в её апстриме). Это значит, что после удаления эти коммиты станут недостижимыми, и Git их в итоге соберёт
gc-ом. Команда страхует от потери работы. Если ветка реально не нужна -git branch -D feature(uppercase) удалит без вопросов. Перед-Dможно посмотреть, что внутри:git log main..feature --oneline.Что значит, когда `git status` пишет `Your branch is ahead of 'origin/main' by 2 commits`?
Показать ответ
У тебя локально на ветке
mainесть 2 коммита, которых нет вorigin/main(удалённой версии). Чтобы их увидели другие, нужноgit push. Если параллельно появилась строкаbehind by N commits, значит наоборот: на сервере есть коммиты, которых нет у тебя, иgit pullих затащит. Если и ahead, и behind - ветка разошлась с удалённой, нужно merge или rebase (см. главу 8). Сама строка появляется только если у локальной ветки настроен tracking; без негоstatusпро remote молчит.В чём разница между удалением локальной и удалённой ветки?
Показать ответ
git branch -d featureудаляет ветку только в твоём локальном.git/refs/heads/. На удалённом сервере (origin) ветка остаётся. Чтобы удалить и удалённую - отдельная команда:git push origin --delete feature(или старая записьgit push origin :feature). Многие хостинги (GitHub, GitLab) автоматически удаляют ветку после merge PR - это настройка в репозитории. После удалённого удаления у тебя вgit branch -aвсё ещё может висетьorigin/feature- это локальный кэш. Очистить -git fetch --pruneилиgit remote prune origin.