open atlas
↑ К треку
Git: от нуля до сеньора GIT · 08 · 02

Ссылки и HEAD — как имена указывают на коммиты

Ссылки — это человеческие имена для хешей коммитов: ветка под refs/heads — это файл с одним SHA, рядом теги и remotes. HEAD — символическая ссылка на текущую ветку (прикреплён) или прямо на SHA (отделён). Коммит двигает ссылку ветки, а HEAD следует за ней.

GIT Senior ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior

Хранилище объектов из прошлого урока адресуется целиком 40-символьными хешами. Никто не печатает 9f2c1a…, чтобы переключить ветку, — ты печатаешь git switch main. То, что отображает слово main на хеш, — это ссылка (ref), и она почти оскорбительно проста: маленький текстовый файл, содержащий один SHA коммита. Ветки, теги и HEAD — всё это просто ссылки, поэтому «ветвление дёшево» — буквально правда: ветка — это одна строка в файле.

К концу этого урока ты сможешь объяснить, что такое ссылка ветки, ссылка тега и HEAD на самом деле на диске, и отличить прикреплённый HEAD от отделённого.

Цель

После этого урока ты сможешь объяснить, что ссылка — это имя, держащее хеш коммита, найти ветки/теги/remotes под .git/refs, описать HEAD как символическую ссылку и отличить прикреплённый HEAD от отделённого — включая то, почему коммит обновляет ссылку текущей ветки.

1

Ссылка — это читаемое человеком имя, которое сводится к хешу коммита. Ссылки живут под .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            # порцелайновый способ свести ссылку к хешу
2

Ветка — это один изменяемый указатель, а не контейнер коммитов. Сказать «этот коммит в main» на самом деле значит «коммит достижим переходом по ссылкам на родителей назад от того SHA, который сейчас держит refs/heads/main». Ветка не владеет коммитами; это единственный редактируемый указатель на вершину. Поэтому создание ветки — это O(1): git branch feature пишет один 41-байтовый файл, — и поэтому удаление ветки не удаляет коммитов, только имя.

3

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 на имя другой ссылки и обновление рабочего дерева под неё.

4

Коммит обновляет ссылку ветки, а HEAD достаётся это бесплатно. Когда ты коммитишь, пока HEAD указывает на refs/heads/main:

  1. Git пишет новый объект-коммит (tree + parent = старая вершина + метаданные).
  2. Git обновляет refs/heads/main, чтобы он держал SHA нового коммита.
  3. 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 превращает твои отделённые коммиты в настоящую ветку.

5

Ссылки упаковываются ради производительности. Репозиторий с тысячами тегов означал бы тысячи крошечных файлов. 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-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.