Что такое ветка — это всего 41 байт
В большинстве VCS до Git ветка — это «что-то большое и страшное». В SVN ветка — это копия директории в /branches/. В CVS — особое состояние файла с меткой. В Mercurial 1.x — поле в commit metadata, которое нельзя переименовать. В Git ветка — это файл с одной строкой. 41 байт. Всё.
В этом уроке мы посмотрим, что физически представляет собой ветка, и почему из этой простоты вытекает вся «культура feature-веток», которая определяет современный workflow.
Демонстрация: ветка как файл
Создадим репозиторий и посмотрим, что происходит с ветками на уровне файлов:
mkdir -p ~/git-sandbox/lesson-04-branches
cd ~/git-sandbox/lesson-04-branches
git init
echo "hello" > a.txt
git add a.txt
git commit -m "Initial commit"
# Посмотрим на ветку main
cat .git/refs/heads/main
# a1b2c3d4e5f6...
Это и есть ветка main. Один файл, одна строка с SHA коммита + newline = 41 байт. Никакой магии.
wc -c .git/refs/heads/main
# 41 .git/refs/heads/main
41 байт. 40 hex символов SHA-1 + один символ newline.
Создадим новую ветку:
git switch -c feature/login
# Switched to a new branch 'feature/login'
ls .git/refs/heads/
# feature/login main
cat .git/refs/heads/feature/login
# a1b2c3d4e5f6... ← тот же SHA, что и main (пока никаких коммитов на feature/login)
Новая ветка — это просто новый файл с тем же SHA. Создание заняло наносекунды. Файл refs/heads/feature/login — по сути refs/heads/feature/ это поддиректория, login — файл в ней (Git создаст поддиректории автоматически для веток с / в имени).
HEAD: указатель на ветку
.git/HEAD — это указатель на текущую ветку. Файл с одной строкой:
cat .git/HEAD
# ref: refs/heads/feature/login
Это говорит Git: «текущее место — ветка feature/login». Когда вы делаете коммит, Git:
- Создаёт новый commit-объект
- Обновляет файл
refs/heads/feature/loginна SHA нового коммита - HEAD автоматически указывает на feature/login, поэтому «двигается» вместе с веткой
Когда вы переключаетесь на другую ветку (git switch main), Git меняет содержимое .git/HEAD:
git switch main
cat .git/HEAD
# ref: refs/heads/main
И обновляет working tree, чтобы файлы соответствовали состоянию ветки main.
Что делает git switch -c new-branch
Команда создаёт новую ветку и переключается на неё. На уровне файлов:
- Создаёт файл
refs/heads/new-branchс SHA текущего HEAD - Обновляет
HEADнаref: refs/heads/new-branch - Working tree не меняется (новая ветка пока в той же точке)
Эквивалент через plumbing:
# То же, что git switch -c new-branch:
echo $(git rev-parse HEAD) > .git/refs/heads/new-branch
echo "ref: refs/heads/new-branch" > .git/HEAD
Конечно, никто так не делает — git switch -c короче и безопаснее. Но это демонстрирует, насколько просто создание ветки.
Просмотр веток
git branch — список локальных веток
git branch
# feature/login
# * main ← звёздочка показывает текущую ветку
Звёздочка указывает на текущую (HEAD).
git branch -a — со всеми удалёнными
git branch -a
# feature/login
# * main
# remotes/origin/feature/login
# remotes/origin/main
remotes/origin/... — это отслеживающие ветки: локальный кеш состояния веток удалённого репозитория. Подробно — модуль 6.
git branch -v — с последним коммитом
git branch -v
# feature/login a1b2c3d Add login form
# * main e5f6g7h Initial commit
git branch —merged / —no-merged
# Ветки, которые уже слиты в main
git branch --merged main
# Ветки, которые ЕЩЁ НЕ слиты в main
git branch --no-merged main
Полезно при чистке: видишь, какие ветки безопасно удалять (already merged), а какие не стоит (содержат изменения, ещё не в main).
Имена веток: что можно, что нельзя
Имя ветки — это путь к файлу в refs/heads/. Из этого следуют правила:
Конвенции, принятые в командах:
feature/<задача>илиfeat/<задача>— для новых фичfix/<bug-описание>илиbugfix/<id>— для багфиксовhotfix/<id>— для срочных исправлений в productionrelease/<version>— для подготовки релиза (например,release/v1.2.0)chore/<задача>— для служебных задач (обновление зависимостей, рефакторинг без функциональных изменений)
Многие команды связывают имя ветки с issue tracker: feat/PROJ-1234 или feat/1234-add-oauth. Это удобно — по имени ветки понятно, какая задача.
Что произойдёт при коммите на ветке
git switch -c feature/oauth
echo "oauth code" > oauth.py
git add oauth.py
git commit -m "Add OAuth"
Что произошло:
- Создан blob с содержимым «oauth code\n»
- Создан tree с записью «oauth.py -> blob»
- Создан commit-объект с этим tree и parent = предыдущий commit
- Файл
refs/heads/feature/oauthобновлён на SHA нового коммита - HEAD по-прежнему указывает на
ref: refs/heads/feature/oauth, который теперь указывает на новый коммит
Здесь ключевой момент: ветка двигается только тогда, когда вы на ней делаете коммит. main не двигалась, потому что вы на ней не коммитили. Это и есть изоляция: работа на feature/oauth не затрагивает main, пока вы не сделаете merge.
Удаление ветки
# Безопасное удаление (только если merged в текущую)
git branch -d feature/oauth
# Принудительное удаление (даже если не merged — потеряете коммиты!)
git branch -D feature/oauth
-d (lowercase) проверяет: «эта ветка уже слита в текущую? Если да — удалить можно». Защита от случайной потери работы.
-D (uppercase) удаляет без вопросов. Иногда нужно (например, экспериментальная ветка, которую вы решили не мержить), но опасно.
Если вы удалили -D ветку, на которой были коммиты, не слитые в другие ветки, — коммиты становятся unreachable. До git gc (30 дней по умолчанию) они ещё в objects/, можно восстановить через reflog (модуль 10). После 30 дней — пропали навсегда.
После удаления:
git branch
# * main
# (feature/oauth удалён)
ls .git/refs/heads/
# main
# (файл feature/oauth удалён)
Почему дешёвые ветки изменили культуру
В SVN и старом Mercurial ветки были дорогими — создание занимало секунды, в репозитории появлялась новая директория, ветки были «видны» всем. Из-за этого:
- Ветки создавались редко (на крупные релизы)
- Часто работали прямо в trunk/master
- Эксперименты делали в личных копиях или через diff-файлы
В Git ветка — 41 байт, создание мгновенное, можно создать сотню и не задуматься о storage. Из этого выросла feature-branch culture:
Современный workflow Junior DE:
- Утро —
git switch main && git pull - Получил задачу —
git switch -c feature/PROJ-1234 - Работаю, коммичу
- Дальше работаю, ещё коммичу
- Готово —
git push origin feature/PROJ-1234 - Создаю Pull Request на GitHub
- Code review, исправления, ещё push
- Merge в main
- Удаляю ветку:
git branch -d feature/PROJ-1234
Это и есть branching strategy в современном Git, и она возможна именно потому, что ветки дешёвые.
Попробуй сам
- Создайте репозиторий с одним коммитом:
mkdir -p ~/git-sandbox/lesson-04-what-is-branch
cd ~/git-sandbox/lesson-04-what-is-branch
git init
echo "initial" > a.txt
git add a.txt && git commit -m "Initial"
- Посмотрите ветку main как файл:
cat .git/refs/heads/main
ls -la .git/refs/heads/
wc -c .git/refs/heads/main
- Создайте несколько веток и посмотрите файлы:
git switch -c feature/a
git switch -c feature/b
git switch -c hotfix/critical
ls -la .git/refs/heads/
# main
# feature/a (поддиректория feature/, файл a)
# feature/b
# hotfix/critical
- Сделайте коммит на одной из веток:
git switch feature/a
echo "feature a" > a-feature.txt
git add . && git commit -m "Feature A work"
# Посмотрите, что изменилось в refs/heads/
cat .git/refs/heads/feature/a # новый SHA
cat .git/refs/heads/main # старый SHA
cat .git/refs/heads/feature/b # старый SHA
- Посмотрите HEAD:
cat .git/HEAD
# ref: refs/heads/feature/a
git switch main
cat .git/HEAD
# ref: refs/heads/main
- Удалите ветку:
git branch -D feature/b
ls .git/refs/heads/
# (feature/b исчез)
Прокатив всё это руками, вы получите глубокую интуицию: «ветка — это файл, ничего больше». Дальше любая работа с ветками будет понятна.
Feature-branch в dbt-проекте: как это выглядит на практике