Внутреннее устройство II: ссылки, HEAD, notes и reftable

Рейтинг: 62.1% · 15 голосов
Самый подробный курс по Git - от первого коммита до объектной базы и packfiles. Терминал и первый репозиторий, ветки и слияние, конфликты, удалённая работа и форджи (GitHub, GitLab), rebase и переписывание истории, спасение кода через reflog и fsck, stash и worktree, bisect и blame, подмодули и монорепо, хуки и подпись коммитов, внутреннее устройство и масштаб. Для новичков и профи. Актуально на 2026 (Git 2.54, дорожная карта 3.0).
Ответить
Аватара пользователя
Pavel_Git
Сообщения: 52
Зарегистрирован: 11 май 2026, 05:31

Внутреннее устройство II: ссылки, HEAD, notes и reftable

Сообщение Pavel_Git »

Оглавление курса (52)
  1. Что такое контроль версий и почему победил Git
  2. Установка Git на Windows, macOS и Linux и первая настройка
  3. Терминал и как спрашивать у Git: выживание в командной строке
  4. Ментальная модель Git: три состояния, коммит-снимок и граф
  5. Первый репозиторий: init, add, commit, status
  6. Просмотр изменений: status, diff и индексация по частям
  7. История проекта: git log, диапазоны и поиск
  8. .gitignore: что не должно попасть в репозиторий
  9. .gitattributes: окончания строк, бинарники и атрибуты
  10. Ветки как указатели: branch, switch и detached HEAD
  11. Слияние веток: fast-forward против трёхстороннего merge
  12. Конфликты слияния: анатомия и уверенное разрешение
  13. Практикум: твой первый день с Git от нуля до push
  14. Удалённые репозитории: remote, fetch, базовый pull и refspec
  15. git push: безопасная отправка и защита от перезаписи
  16. Клонирование: shallow, partial clone, bundle и backfill
  17. GitHub, GitLab и форки: pull request, rulesets и защита веток
  18. Рабочие процессы команды: GitFlow, GitHub Flow, trunk-based
  19. Merge queue и merge trains: безопасное вливание в trunk
  20. Культура code review и Conventional Commits
  21. git reset: три дерева и отмена индексации и коммитов
  22. git revert: безопасная отмена в общей истории
  23. Правка последнего коммита: git commit --amend
  24. git rebase: пересадка истории и золотое правило
  25. Интерактивный rebase: переписать серию коммитов
  26. git cherry-pick: перенос отдельных коммитов
  27. Stacked diffs: цепочка зависимых PR
  28. Массовое переписывание истории: git filter-repo и git replace
  29. git reflog: журнал, который помнит всё
  30. Восстановление утерянных коммитов, веток и файлов
  31. Откат опасных операций: reset --hard, сломанный rebase, плохой merge
  32. git stash: тайник для незавершённой работы
  33. git worktree: несколько рабочих деревьев одного репозитория
  34. git bisect: бинарный поиск регрессии
  35. git blame, mailmap и археология кода
  36. Теги, релизы, archive и bundle
  37. Подмодули: внешние репозитории внутри проекта
  38. Subtree, монорепозитории и sparse-checkout
  39. Git LFS и большие файлы
  40. Вспомогательные команды: clean, mv, rm, restore
  41. Псевдонимы и кастомизация .gitconfig
  42. Хуки Git: автоматизация на клиенте и сервере
  43. Подпись коммитов и тегов: SSH, GPG и Sigstore
  44. Внутреннее устройство I: объектная база Git
  45. Внутреннее устройство II: ссылки, HEAD, notes и reftable (вы здесь)
  46. Внутреннее устройство III: индекс, packfiles, gc и обслуживание
  47. От SHA-1 к SHA-256: переход на новый хеш
  48. Огромные репозитории: partial clone, FSMonitor, Scalar
  49. Git в CI/CD: shallow, кеш, merge_group и безопасность
  50. Куда движется Git: дорожная карта 3.0 и экосистема
  51. Профессиональный setup: собираем рабочее окружение Git
  52. 30+ типичных ошибок Git и как из них выбираться
Зачем нужны git refs, если уже есть SHA

В прошлом уроке мы разобрали объектную модель: коммиты, деревья, блобы - всё адресуется по хешу. Но человек не оперирует строками вида a1b2c3d4. Никто не приходит на стендап со словами "я закоммитил в f3e9c0a". Тебе нужны имена - main, develop, release-2.5, v1.0.0. Вот эти имена и есть git refs (ссылки).

Ссылка - это всего лишь имя для SHA коммита. Не больше и не меньше. Когда ты пишешь

Код: Выделить всё

git log main
, Git берёт имя main, находит, на какой SHA оно указывает, и идёт от него по родителям. Когда ты делаешь коммит на ветке main, Git создаёт новый объект-коммит и переписывает имя main так, чтобы оно указывало на новый SHA. Ветка не "содержит" коммиты. Ветка - это движущийся указатель на один коммит, вершину линии истории. Всё остальное (вся история) достраивается обходом родителей.

Эта простота - ключ к пониманию половины "магии" Git. Reset, rebase, merge, fast-forward - всё это в основе своей перезапись пары байт в файле со ссылкой. Боль, которую решает урок: перестать бояться веток, HEAD и detached-состояний, научиться чинить репозиторий руками через update-ref, понять refspec (откуда вообще берётся origin/main) и быть готовым к reftable - новому хранилищу ссылок, которое в Git 3.0 станет дефолтом.

Изображение

git refs на диске: ветка - это файл с SHA

Самый честный способ понять ссылки - посмотреть на них в файловой системе. Возьмём обычный репозиторий и заглянем в .git/refs.

Код: Выделить всё

$ ls -F .git/refs
heads/  remotes/  tags/

$ ls .git/refs/heads
develop  main

$ cat .git/refs/heads/main
a1c4f2e9b7d3056812ff3490ab12cd34ef567890
Вот и весь секрет. Ветка main - это текстовый файл .git/refs/heads/main, внутри которого лежит ровно одна строка: 40 hex-символов (SHA-1) и перевод строки. Сделай коммит - и эти 40 символов поменяются. Локальные ветки живут в refs/heads. Теги - в refs/tags. А состояние удалённых веток, как мы их видим, - в refs/remotes:

Код: Выделить всё

$ cat .git/refs/remotes/origin/main
a1c4f2e9b7d3056812ff3490ab12cd34ef567890
Полное имя ссылки - это путь от корня .git: refs/heads/main, refs/tags/v1.0, refs/remotes/origin/main. Когда ты пишешь просто main, Git разрешает короткое имя по фиксированному порядку приоритетов (так называемые gitrevisions rules): сначала пробует точное имя, потом refs/main, refs/tags/main, refs/heads/main, refs/remotes/main, refs/remotes/main/HEAD. Поэтому если у тебя есть и ветка, и тег с именем v2, будет неоднозначность - и Git предупредит. Не делай так.

git head: HEAD как симоволическая ссылка

Если ветки - файлы с SHA, то git head устроен хитрее. HEAD отвечает на вопрос "где я сейчас нахожусь". Это не обычная ссылка с SHA, а симоволическая ссылка (symref) - ссылка, указывающая на другую ссылку.

Код: Выделить всё

$ cat .git/HEAD
ref: refs/heads/main
Видишь префикс "ref: "? Это и есть симоволическая ссылка. HEAD говорит: "я указываю не на коммит напрямую, а на ветку main". Поэтому когда ты коммитишь, Git смотрит HEAD -> видит main -> и двигает main. А HEAD автоматически продолжает указывать на main и едет вместе с ней. Так работает обычная жизнь на ветке.

Теперь сделай

Код: Выделить всё

git switch a1c4f2e
(или checkout по SHA), и получишь знаменитое detached HEAD. В этом режиме HEAD перестаёт быть симолической и начинает хранить SHA напрямую:

Код: Выделить всё

$ git switch --detach a1c4f2e9
HEAD is now at a1c4f2e9 fix login race

$ cat .git/HEAD
a1c4f2e9b7d3056812ff3490ab12cd34ef567890
Вот почему detached HEAD опасен: коммиты, которые ты тут сделаешь, не двигают никакую ветку. Они висят только на HEAD. Переключишься обратно на main - и эти коммиты станут недостижимы (до них доберётся git gc). Зная механику, ты не боишься: сделал работу в detached - сразу

Код: Выделить всё

git switch -c new-branch
, и создалась настоящая ссылка-файл, держащая твои коммиты.

Низкоуровневая работа: update-ref, symbolic-ref, show-ref, rev-parse

Редактировать файлы в .git/refs руками - плохая идея (особенно когда ссылки упакованы, об этом ниже). Для этого есть фарфоровые и водопроводные команды. Главная из них - git update-ref.

Код: Выделить всё

$ git update-ref refs/heads/hotfix a1c4f2e9
$ git update-ref --create-reflog refs/heads/exp HEAD
update-ref атомарно записывает SHA в ссылку и (если включено) добавляет запись в reflog. Это безопаснее, чем echo в файл. У неё есть мощный режим --stdin для пакетных атомарных транзакций (создать/обновить/удалить десяток ссылок разом - либо все, либо ни одной). Удалить ссылку:

Код: Выделить всё

git update-ref -d refs/heads/old
. Опция -d с проверкой старого значения (

Код: Выделить всё

git update-ref -d refs/heads/x <old-sha>
) защищает от гонок - не удалит, если ссылка уже сдвинулась.

git symbolic-ref читает и пишет симолические ссылки, тот самый HEAD:

Код: Выделить всё

$ git symbolic-ref HEAD
refs/heads/main

$ git symbolic-ref HEAD refs/heads/develop
Вторая команда переключает HEAD на develop, НЕ трогая рабочую директорию (в отличие от switch). Так чинят репозитории, у которых сломался HEAD.

git show-ref и git rev-parse - твои разведчики. show-ref печатает все ссылки с их SHA, rev-parse превращает любое имя/выражение в голый SHA:

Код: Выделить всё

$ git show-ref
a1c4f2e9b7d3... refs/heads/main
b2d5a3f0c8e1... refs/heads/develop
a1c4f2e9b7d3... refs/remotes/origin/main
9f8e7d6c5b4a... refs/tags/v1.0

$ git rev-parse main
a1c4f2e9b7d3056812ff3490ab12cd34ef567890

$ git rev-parse --abbrev-ref HEAD
main
rev-parse - это калькулятор имён. Он понимает синтаксис навигации, который надо знать назубок:
  • main^ - первый родитель main. main^2 - второй родитель (актуально для merge-коммитов).
  • main~3 - три коммита назад по первому родителю (main^^^).
  • HEAD@{2} - где HEAD был 2 шага назад по reflog (не путать с ~).
  • main@{yesterday}, main@{2.hours.ago} - состояние ссылки по времени из reflog.
  • @ - короткая запись для HEAD.
  • main@{upstream} или main@{u} - upstream-ветка для main (обычно origin/main).
Разница ^/~ против @{} принципиальна: ^ и ~ ходят по графу коммитов (по родителям), а @{} ходит по reflog (по истории движения самой ссылки во времени). Первое - про структуру истории, второе - про "что я делал с веткой".

packed-refs: что происходит при тысячах ссылок

Файл-на-ссылку - отлично для десятка веток. Но в большом репозитории с тысячами тегов и веток это десятки тысяч мелких файлов. Открытие, чтение и обход такого количества inode тормозит. Git это решает упаковкой - packed-refs.

Код: Выделить всё

$ git pack-refs --all
$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled sorted
a1c4f2e9b7d3056812ff3490ab12cd34ef567890 refs/heads/main
9f8e7d6c5b4a3021... refs/tags/v1.0
^c0ffee1234567890...
Вместо россыпи файлов - один отсортированный текстовый файл packed-refs со всеми ссылками. Строка со знаком ^ - это "peeled" значение: для аннотированного тега тут лежит SHA коммита, на который тег в итоге разыменовывается (сам тег - отдельный объект). Это кеш, чтобы не разворачивать тег каждый раз.

Важный момент - двухуровневый поиск. После упаковки ссылки в .git/refs могут исчезнуть как файлы, но продолжают существовать через packed-refs. Когда ты делаешь новый коммит на упакованной ветке, Git создаёт свежий loose-файл refs/heads/main, и он перекрывает запись из packed-refs. То есть loose-версия всегда главнее упакованной. Отсюда классические грабли: руками удалил файл .git/refs/heads/feature, а ветка не пропала - потому что её копия осталась в packed-refs. Поэтому удаляй ссылки только через

Код: Выделить всё

git update-ref -d
или git branch -d, они чистят оба места.

Псевдо-ссылки и спец-рефы: ORIG_HEAD, MERGE_HEAD, stash, notes, replace

Кроме веток есть служебные ссылки, которые Git ставит сам. Они лежат прямо в .git (не в refs) и не упаковываются:
  • ORIG_HEAD - куда HEAD указывал ДО опасной операции (reset, merge, rebase). Твоя кнопка "отмена":

    Код: Выделить всё

    git reset --hard ORIG_HEAD
    откатывает неудачный rebase.
  • MERGE_HEAD - существует только во время незавершённого merge, хранит SHA вливаемой ветки. По нему Git знает, что идёт слияние.
  • FETCH_HEAD - что было только что получено последним git fetch (может содержать несколько строк - по ветке на каждую).
  • CHERRY_PICK_HEAD, REVERT_HEAD - аналогично для прерванных cherry-pick и revert.
А в пространстве refs живут специальные неветочные ссылки, каждая со своим смыслом:
  • refs/stash - вершина стека твоих stash. Сами заначки - это коммиты, а стек ведётся через reflog этой ссылки (stash@{0}, stash@{1}).
  • refs/notes/* - сюда git notes пишет заметки. По умолчанию refs/notes/commits.
  • refs/replace/* - сюда git replace кладёт подмены объектов.
  • refs/remotes/origin/HEAD - симолическая ссылка на дефолтную ветку удалённого (обычно origin/main).
Про git notes стоит сказать отдельно - это недооценённая фича. Заметка позволяет прицепить текст к существующему коммиту, НЕ меняя его SHA. Сам коммит иммутабелен (его хеш зависит от содержимого), а заметка хранится сбоку:

Код: Выделить всё

$ git notes add -m "проверено QA, тикет JIRA-4821" a1c4f2e9
$ git log -1
commit a1c4f2e9b7d3056812ff3490ab12cd34ef567890
Author: ...
    fix login race

Notes:
    проверено QA, тикет JIRA-4821
Механика красивая: refs/notes/commits - это обычная ветка, чьё дерево содержит "файлы", имена которых равны SHA аннотируемых коммитов, а содержимое - текст заметки. Каждое изменение заметки создаёт новый коммит в этой ноте-ветке. Так CI вешает на коммиты результаты тестов, ссылки на сборки, метки ревью - не загрязняя сообщение коммита и не переписывая историю. По умолчанию notes не пушатся - их надо гнать явным refspec (

Код: Выделить всё

git push origin refs/notes/commits
).

git replace из refs/replace/* делает другое - подменяет один объект другим на лету при чтении, не трогая историю физически. Полезно для "склейки" обрезанной истории (graft) или починки битого старого коммита. Git ведёт себя так, будто на месте объекта X лежит объект Y.

refspec: откуда берётся refs/remotes при fetch и push

Теперь главный вопрос: как git fetch узнаёт, какие удалённые ссылки куда складывать локально? Ответ - refspec. Он лежит в .git/config:

Код: Выделить всё

[remote "origin"]
    url = git@cyberlake.ru:project.git
    fetch = +refs/heads/*:refs/remotes/origin/*
Refspec читается как источник:приёмник. Левая часть refs/heads/* - все ветки на сервере. Правая refs/remotes/origin/* - куда их писать у тебя. Звёздочки сопоставляются. Знак + впереди означает "разрешить не-fast-forward обновление" (перезаписать локальную копию, даже если она разошлась - для remote-tracking веток это нормально, ты их сам не редактируешь).

То есть origin/main - не магия, а просто результат записи refspec: серверная refs/heads/main приехала в твою refs/remotes/origin/main. При push refspec работает в обратную сторону:

Код: Выделить всё

git push origin main
разворачивается в refs/heads/main:refs/heads/main (моя локальная ветка -> серверная ветка). Можно явно:

Код: Выделить всё

git push origin HEAD:refs/heads/feature-x
. А удаление ветки на сервере - это push с пустым источником:

Код: Выделить всё

git push origin :refs/heads/old
(ничего -> в old, то есть стереть).

reftable: новый бэкенд хранения ссылок

Файлы и packed-refs - это бэкенд "files", он с нами с 2005 года. У него есть фундаментальные проблемы на масштабе: миллионы ссылок упираются в файловую систему, обновление ссылки не вполне атомарно (особенно при гонках loose против packed), а на регистронезависимых ФС (macOS, Windows) ветки Feature и feature конфликтуют. Ответ сообщества - reftable, бинарный блочный формат хранения ссылок, портированный из проекта JGit/Gerrit.

Что важно знать на 2026 год:
  • reftable стал production-ready с Git 2.51 (вышел летом 2025).
  • Он даёт примерно 22-кратное ускорение fetch и 18-кратное ускорение push на репозиториях с 10000+ ссылок за счёт бинарного поиска по блокам вместо обхода тысяч файлов.
  • Обновления ссылок атомарны и журналируемы, регистрозависимость гарантирована на всех ОС.
  • В Git 3.0 (ожидается к концу 2026) reftable станет дефолтным бэкендом для новых репозиториев.
Создать новый репозиторий сразу на reftable:

Код: Выделить всё

$ git init --ref-format=reftable repo
$ ls repo/.git/refs
reftable/
$ ls repo/.git/reftable
0x000000000001-0x000000000001-xxxxxxxx.ref  tables.list
Никаких текстовых файлов с SHA больше нет - ссылки лежат в бинарных .ref-таблицах, а tables.list задаёт их порядок (новые таблицы накатываются поверх, потом компактятся, как в LSM-деревьях). Поставить reftable дефолтом для всех будущих git init/clone:

Код: Выделить всё

$ git config --global init.defaultRefFormat reftable
Существующий репозиторий мигрируется без потери истории командой git refs migrate (она появилась в Git 2.46):

Код: Выделить всё

$ git refs migrate --ref-format=reftable --dry-run
$ git refs migrate --ref-format=reftable
$ git rev-parse --show-ref-format
reftable
Сначала всегда прогоняй --dry-run: он выполняет миграцию в отдельную временную директорию, чтобы ты убедился, что всё ок, не трогая боевой .git. Учти ограничения ранних версий: миграция могла спотыкаться на worktrees - проверяй на своей версии Git. Откатиться назад так же просто:

Код: Выделить всё

git refs migrate --ref-format=files
. Команда git rev-parse --show-ref-format всегда подскажет, какой бэкенд активен сейчас.

Типичные грабли
  • Руками редактировал .git/refs. Файл поправил, а ветка не сдвинулась или, наоборот, "воскресла" - потому что есть копия в packed-refs, которая её перекрывает или дублирует. Всегда update-ref / branch, не текстовый редактор.
  • Удалил ссылку, а коммиты потерялись. Если коммит держался только этой веткой (или detached HEAD) - он стал недостижим. Спасение - reflog:

    Код: Выделить всё

    git reflog
    помнит старые SHA минимум 90 дней,

    Код: Выделить всё

    git reset --hard HEAD@{1}
    возвращает.
  • Неоднозначное имя. Ветка и тег с одинаковым именем -> Git берёт по приоритету (тег раньше ветки) и предупреждает. Уточняй полным путём refs/heads/x.
  • Notes не доехали до коллег. Заметки не пушатся и не клонируются по умолчанию. Настрой refspec push = +refs/notes/*:refs/notes/* или пушь явно.
  • Reset/rebase "снёс" работу. До паники - ORIG_HEAD.

    Код: Выделить всё

    git reset --hard ORIG_HEAD
    откатывает последнюю разрушительную операцию.
Мини-лаба: пощупай ссылки руками

Делай по шагам в пустой папке, каждый раз сверяя файлы в .git.

Код: Выделить всё

$ git init lab && cd lab
$ git config init.defaultBranch main
$ git commit --allow-empty -m c1
$ git commit --allow-empty -m c2
# 1. ветка - это файл с SHA:
$ cat .git/refs/heads/main
$ git rev-parse main
# 2. HEAD - симолическая ссылка:
$ cat .git/HEAD
$ git symbolic-ref HEAD
# 3. создай ветку через update-ref, без git branch:
$ git update-ref refs/heads/exp HEAD~1
$ git show-ref
# 4. упакуй ссылки и посмотри, как пропали loose-файлы:
$ git pack-refs --all
$ ls .git/refs/heads ; cat .git/packed-refs
# 5. заметка к коммиту:
$ git notes add -m "hello from notes" HEAD
$ git log -1 ; cat .git/refs/notes/commits
# 6. навигация:
$ git rev-parse HEAD^ HEAD~1 @
$ git reflog
# 7. мигрируй на reftable и проверь формат:
$ git refs migrate --ref-format=reftable
$ git rev-parse --show-ref-format
$ ls .git/reftable
К шагу 5: обрати внимание, refs/notes/commits после pack-refs тоже мог уехать в packed-refs - ищи там, если loose-файла нет. К шагу 7: после миграции каталога .git/refs с текстовыми файлами не станет - всё в бинарных таблицах, и это нормально.

Контрольные вопросы
  • Чем симолическая ссылка (HEAD в норме) отличается от обычной, и что меняется при detached HEAD?
  • Ты удалил файл .git/refs/heads/feature, но ветка осталась видна в git branch. Почему и как удалить правильно?
  • Refspec +refs/heads/*:refs/remotes/origin/* - что означает каждая часть и зачем плюс впереди?
  • Что конкретно даёт reftable на репозитории с 10000+ ссылок и какой командой перевести существующий репозиторий на него?
Итог

git refs - это просто имена для SHA: ветка - текстовый файл с одним хешем, HEAD - симолическая ссылка на текущую ветку. Навигация ^/~ ходит по графу коммитов, @{} - по reflog. packed-refs упаковывает тысячи ссылок в один файл (но loose перекрывает упакованное). Спец-рефы (ORIG_HEAD, MERGE_HEAD, refs/stash, refs/notes, refs/replace) - служебный инструментарий Git, а refspec - правило записи серверных веток в refs/remotes. И главное на 2026 - reftable: production-ready с 2.51, 18-22x на больших наборах ссылок, дефолт в Git 3.0. Знаешь, где лежат ссылки и как они обновляются, - значит чинишь репозиторий руками, а не молишься на git pull.
👍1 ❤️1 🔥 😄 🤔2
Аватара пользователя
k8s777
Сообщения: 1
Зарегистрирован: 07 июн 2026, 21:32

Re: Внутреннее устройство II: ссылки, HEAD, notes и reftable

Сообщение k8s777 »

Блин, я всегда думал что ветка это как папка с коммитами внутри. А оно просто файл на 40 символов. Полегчало даже.
👍1 ❤️ 🔥1 😄 🤔
Аватара пользователя
nodak
Сообщения: 1
Зарегистрирован: 12 май 2026, 21:17

Re: Внутреннее устройство II: ссылки, HEAD, notes и reftable

Сообщение nodak »

Сделал git refs migrate на нашем монорепо с кучей тегов, fetch реально стал летать. Спасибо что про dry-run предупредил, прогнал сначала его.
👍 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Внутреннее устройство I: объектная база Git
Следующая глава →
Внутреннее устройство III: индекс, packfiles, gc и обслуживание

Все главы курса «Git профессионально: от первого коммита до внутреннего устройства»

Поделиться темой: ✈ Telegram VK
Похожие запросы: Внутреннее устройство Git

Вернуться в «Git профессионально: от первого коммита до внутреннего устройства»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя