Ссылки и HEAD: что такое ветка на самом деле

Развенчиваем главный миф git: ветка — это не «копия проекта», а крошечный файл с одним хешем.

Ссылка (ref) — это просто человекочитаемое имя для хеша коммита. Ветка, тег и HEAD — всё это ссылки, и почти все они умещаются в одну строку текста.

Новички часто представляют ветку как отдельную папку с файлами, которую git куда-то копирует при git switch. На самом деле всё устроено радикально проще и дешевле. Понимание этого объясняет, почему создать ветку — мгновенная операция, что значит «detached HEAD» и чем аннотированный тег отличается от lightweight.

Зачем это знать на практике

Когда вы понимаете, что ветка — это указатель, исчезает целый класс страхов и недоразумений: вы спокойно создаёте десятки веток (они почти ничего не стоят), осознанно работаете в detached HEAD при проверке старого коммита и понимаете, почему git reset «переписывает историю» — он всего лишь двигает указатель ветки на другой коммит.

Что такое ветка на самом деле

Ветка — это файл в каталоге .git/refs/heads/, и внутри него ровно одна строка: хеш коммита, на который ветка указывает. Убедимся:

cat .git/refs/heads/main
# 4b6f0d8a9c1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a

Вот и вся ветка — 41 байт. Когда вы делаете коммит, git создаёт новый commit-объект и переписывает этот файл, чтобы он указывал на свежий коммит. Никакого копирования файлов проекта не происходит. Именно поэтому создание ветки — это запись одной строки, операция за миллисекунды независимо от размера проекта.

Каноничный способ читать и менять ссылки — команда git update-ref и пламбинг-команда git rev-parse, которая разворачивает имя в хеш:

git rev-parse main
# 4b6f0d8a9c1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a

HEAD: где я нахожусь

Если ветки — это закладки в книге, то HEAD — палец, которым вы водите по странице: он говорит, где вы сейчас. HEAD лежит прямо в .git/HEAD и обычно содержит не хеш, а символическую ссылку на ветку:

cat .git/HEAD
# ref: refs/heads/main

Читается как «HEAD сейчас — это ветка main». Поэтому когда вы коммитите, git по цепочке HEAD → main находит, какую именно ветку двигать вперёд. Переключение веток (git switch feature) меняет содержимое .git/HEAD на ref: refs/heads/feature и обновляет рабочую директорию под этот коммит.

Detached HEAD

Иногда HEAD указывает не на ветку, а напрямую на хеш коммита. Это и есть detached HEAD — «открепившийся HEAD». Так происходит, когда вы проверяете конкретный коммит или тег:

git switch --detach 4b6f0d8
# или: git checkout v1.0.0
# HEAD теперь содержит хеш напрямую, а не "ref: refs/heads/..."

В этом состоянии можно смотреть код, собирать, экспериментировать. Опасность в одном: новые коммиты, сделанные в detached HEAD, ни на какую ветку не записываются. Стоит переключиться обратно — и на них не останется ни одной ссылки. Они не пропадают мгновенно (их ещё держит reflog), но «потеряться» легко. Правильный выход — создать ветку до того, как уйдёте: git switch -c new-branch закрепит ваши коммиты.

Теги: lightweight против аннотированных

Тег — это устойчивое имя для конкретного коммита, обычно для релиза. Но тегов два сорта, и разница принципиальна.

Lightweight-тег — буквально ещё одна ссылка в .git/refs/tags/, такая же строка с хешем, как ветка, только не двигается:

git tag v1.0-light
cat .git/refs/tags/v1.0-light
# 4b6f0d8a9c1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a

Аннотированный тег — это полноценный объект в базе (тот самый четвёртый тип). У него есть автор, дата, сообщение и, при желании, GPG-подпись. Ссылка в refs/tags/ указывает не на коммит, а на этот tag-объект, который уже указывает на коммит:

git tag -a v1.0 -m "Первый релиз"
git cat-file -t v1.0      # tag
git cat-file -p v1.0
# object 4b6f0d8...
# type commit
# tag v1.0
# tagger Anna <[email protected]> 1718000000 +0300
#
# Первый релиз

Для релизов почти всегда нужен аннотированный тег: он хранит, кто и когда выпустил версию, и его можно подписать. Lightweight-тег — удобная закладка «для себя».

Как это работает под капотом

Сложим картину. В .git/refs/heads/ лежат ветки, в .git/refs/tags/ — теги; и то, и другое — файлы с хешами. Удалённые ветки кешируются в .git/refs/remotes/. HEAD в корне .git/ символически ссылается на текущую ветку. Когда ссылок становится много, git упаковывает их в один файл .git/packed-refs ради скорости — но смысл тот же.

Отсюда следует важное: «переключить ветку», «сделать reset», «создать тег» — это операции над указателями, а не над содержимым коммитов. Коммиты иммутабельны (из прошлого урока), меняются только ссылки на них. Это делает почти все навигационные операции дешёвыми и обратимыми.

Частые ошибки

  • Думать, что ветка хранит файлы. Ветка хранит один хеш. Файлы — в blob'ах и tree, на которые через коммит указывает ветка.
  • Коммитить в detached HEAD и удивляться пропаже. Перед уходом создавайте ветку (git switch -c), иначе коммиты останутся без ссылки.
  • Считать lightweight- и аннотированный тег одним и тем же. Первый — просто ссылка, второй — объект с автором и сообщением; для релизов нужен второй.
  • Редактировать файлы в .git/refs/ руками. Лучше использовать git update-ref / git tag — они не дают сделать ссылку несогласованной.

Итоги

  • Ветка — файл в .git/refs/heads/ с одним хешем; коммит сдвигает этот указатель.
  • HEAD (файл .git/HEAD) обычно символически ссылается на текущую ветку и определяет, что двигать при коммите.
  • Detached HEAD — HEAD указывает прямо на хеш; коммиты тут не привязаны к ветке, их легко «потерять».
  • Lightweight-тег — просто ссылка; аннотированный — объект с автором, датой и сообщением (нужен для релизов).
  • Навигация и reset двигают указатели, а не переписывают коммиты.
Проверьте себя
1. Что физически представляет собой ветка в git?
AОтдельную копию всех файлов проекта на диске
BФайл в .git/refs/heads/ с одной строкой — хешем коммита
CЗапись в базе данных с полной историей изменений
DАннотированный объект с автором и сообщением
2. Что обычно содержит файл .git/HEAD в нормальном (не detached) состоянии?
AХеш текущего коммита напрямую
BСимволическую ссылку на текущую ветку, например 'ref: refs/heads/main'
CСписок всех веток репозитория
DСодержимое последнего коммита
3. Чем аннотированный тег отличается от lightweight-тега?
AНичем, это два названия одного и того же
BАннотированный — это объект с автором, датой и сообщением, а lightweight — просто ссылка на коммит
CLightweight-тег нельзя удалить, а аннотированный можно
DАннотированный тег указывает на blob, а lightweight — на tree