Ссылки и HEAD — как имена указывают на коммиты
Ссылки — это человеческие имена для хешей коммитов: ветка под refs/heads — это файл с одним SHA, рядом теги и remotes. HEAD — символическая ссылка на текущую ветку (прикреплён) или прямо на SHA (отделён). Коммит двигает ссылку ветки, а HEAD следует за ней.
Хранилище объектов из прошлого урока адресуется целиком 40-символьными хешами. Никто не печатает 9f2c1a…, чтобы переключить ветку, — ты печатаешь git switch main. То, что отображает слово main на хеш, — это ссылка (ref), и она почти оскорбительно проста: маленький текстовый файл, содержащий один SHA коммита. Ветки, теги и HEAD — всё это просто ссылки, поэтому «ветвление дёшево» — буквально правда: ветка — это одна строка в файле.
К концу этого урока ты сможешь объяснить, что такое ссылка ветки, ссылка тега и HEAD на самом деле на диске, и отличить прикреплённый HEAD от отделённого.
После этого урока ты сможешь объяснить, что ссылка — это имя, держащее хеш коммита, найти ветки/теги/remotes под .git/refs, описать HEAD как символическую ссылку и отличить прикреплённый HEAD от отделённого — включая то, почему коммит обновляет ссылку текущей ветки.
Ссылка — это читаемое человеком имя, которое сводится к хешу коммита. Ссылки живут под .git/refs, упорядоченные по виду:
refs/heads/<branch>— локальные ветки. Файлrefs/heads/mainсодержит одну строку: 40-символьный SHA вершины ветки.refs/tags/<tag>— теги. (Легковесный тег указывает прямо на коммит; аннотированный тег указывает на объект тега, который затем указывает на коммит.)refs/remotes/origin/<branch>— отслеживающие удалённые ссылки, твоя локальная память о том, где были ветки удалённого репозитория на момент последнего fetch.
cat .git/refs/heads/main # -> 9f2c1a… (просто SHA)
git rev-parse main # порцелайновый способ свести ссылку к хешуВетка — это один изменяемый указатель, а не контейнер коммитов. Сказать «этот коммит в main» на самом деле значит «коммит достижим переходом по ссылкам на родителей назад от того SHA, который сейчас держит refs/heads/main». Ветка не владеет коммитами; это единственный редактируемый указатель на вершину. Поэтому создание ветки — это O(1): git branch feature пишет один 41-байтовый файл, — и поэтому удаление ветки не удаляет коммитов, только имя.
HEAD — это символическая ссылка: ссылка, указывающая на другую ссылку. .git/HEAD обычно не содержит SHA — он содержит указатель на ветку:
cat .git/HEAD # -> ref: refs/heads/main
git symbolic-ref HEAD # -> refs/heads/main (какую ветку отслеживает HEAD)
git rev-parse HEAD # -> SHA, к которому эта цепочка в итоге сводитсяЭта косвенность — весь фокус. «Текущая ветка» определяется как «та ветка, на которую указывает HEAD». Переключение веток — это просто переписывание HEAD на имя другой ссылки и обновление рабочего дерева под неё.
Коммит обновляет ссылку ветки, а HEAD достаётся это бесплатно. Когда ты коммитишь, пока HEAD указывает на refs/heads/main:
- Git пишет новый объект-коммит (tree + parent = старая вершина + метаданные).
- Git обновляет
refs/heads/main, чтобы он держал SHA нового коммита. - HEAD не тронут — он по-прежнему говорит
ref: refs/heads/main, что теперь сводится к новому коммиту.
Так что продвижение HEAD — это следствие продвижения ссылки ветки, а не отдельная запись. Шаг 2 можно сделать вручную:
git update-ref refs/heads/main <new-sha> # plumbing: подвинуть вершину ветки напрямую▸Граничные случаи
Отделённый HEAD (detached HEAD). Переключись на коммит или тег напрямую — git checkout <sha>, git checkout v1.2 или git switch --detach — и HEAD перестаёт быть символическим: .git/HEAD теперь держит сырой SHA вместо ref: …. Ты ни на какой ветке. Новые коммиты создаются, и HEAD продвигается к каждому, но ни одна ссылка ветки за тобой не двигается. Переключишься прочь — и эти коммиты станут недостижимыми (восстановимы только через reflog), пока их не удалит сборка мусора. Спасение — назвать работу до ухода: git switch -c rescue превращает твои отделённые коммиты в настоящую ветку.
Ссылки упаковываются ради производительности. Репозиторий с тысячами тегов означал бы тысячи крошечных файлов. git gc (или git pack-refs) сводит их в единый файл .git/packed-refs — по строке на ссылку, <sha> <refname>. Git читает packed-refs и loose-ссылки вместе; loose-файл .git/refs/heads/main всегда побеждает упакованную запись для того же имени. Это чисто оптимизация хранения — git rev-parse main сводится одинаково в любом случае.
Смотрим, как двигаются файлы во время коммита.
Начинаем на main и смотрим три важные вещи:
git symbolic-ref HEAD # -> refs/heads/main (HEAD прикреплён)
git rev-parse HEAD # -> 9f2c1a… (текущая вершина)
cat .git/refs/heads/main # -> 9f2c1a… (тот же SHA)Делаем коммит и смотрим снова:
echo change >> file.txt && git add -A && git commit -m "edit"
git symbolic-ref HEAD # -> refs/heads/main (НЕ ИЗМЕНИЛСЯ)
cat .git/refs/heads/main # -> 4b8e7d… (НОВАЯ вершина)
git rev-parse HEAD # -> 4b8e7d… (следует за веткой)Переписан только refs/heads/main. HEAD по-прежнему называет ту же ветку; он сводится к новому коммиту чисто потому, что ветка, на которую он указывает, сдвинулась. Теперь отделяемся и коммитим:
git checkout 9f2c1a… # отделиться на старую вершину
cat .git/HEAD # -> 9f2c1a… (сырой SHA, не "ref: …")
echo x >> file.txt && git commit -am "wip"
cat .git/HEAD # -> новый SHA; refs/heads/main НЕ сдвинулсяКоммит существует, но ни одна ветка на него не указывает. git switch main сейчас бы его бросил — git switch -c keep вместо этого сохранит его как ветку.
▸Частая ошибка
Частая путаница — считать HEAD «последним коммитом». HEAD — это не коммит и не новейший коммит, это указатель, обычно на ветку, которую ты переключил. После git switch old-release HEAD указывает на коммит многомесячной давности, и git rev-parse HEAD — это тот старый SHA, а не вершина main. «Последний» возникает в картине только потому, что каждая ссылка ветки случайно указывает на собственную вершину.
Ты на ветке main и выполняешь git commit. Какую ссылку git переписывает, чтобы она указывала на новый коммит?
Ссылка — это человеческое имя, которое сводится к хешу коммита; ветки живут под refs/heads, теги под refs/tags, отслеживающие удалённые ссылки под refs/remotes. Ветка — это просто однострочный файл с SHA, поэтому создание и удаление веток почти бесплатно и не трогает коммитов. HEAD — это символическая ссылка — обычно ref: refs/heads/<branch> — так что «текущая ветка» — это та, что называет HEAD. Коммит переписывает ссылку ветки; HEAD следует автоматически. Переключись на коммит напрямую — и HEAD станет отделённым, держа сырой SHA без ветки, которая понесёт твои новые коммиты. Массовые ссылки сводятся в packed-refs ради скорости. Дальше ты увидишь, как два таких указателя веток примиряются, — настоящую механику merge и rebase.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.