В прошлом уроке мы разобрали объектную модель: коммиты, деревья, блобы - всё адресуется по хешу. Но человек не оперирует строками вида a1b2c3d4. Никто не приходит на стендап со словами "я закоммитил в f3e9c0a". Тебе нужны имена - main, develop, release-2.5, v1.0.0. Вот эти имена и есть git refs (ссылки).
Ссылка - это всего лишь имя для SHA коммита. Не больше и не меньше. Когда ты пишешь
Код: Выделить всё
git log mainЭта простота - ключ к пониманию половины "магии" 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
Код: Выделить всё
$ cat .git/refs/remotes/origin/main
a1c4f2e9b7d3056812ff3490ab12cd34ef567890
git head: HEAD как симоволическая ссылка
Если ветки - файлы с SHA, то git head устроен хитрее. HEAD отвечает на вопрос "где я сейчас нахожусь". Это не обычная ссылка с SHA, а симоволическая ссылка (symref) - ссылка, указывающая на другую ссылку.
Код: Выделить всё
$ cat .git/HEAD
ref: refs/heads/main
Теперь сделай
Код: Выделить всё
git switch a1c4f2eКод: Выделить всё
$ git switch --detach a1c4f2e9
HEAD is now at a1c4f2e9 fix login race
$ cat .git/HEAD
a1c4f2e9b7d3056812ff3490ab12cd34ef567890
Код: Выделить всё
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
Код: Выделить всё
git update-ref -d refs/heads/oldКод: Выделить всё
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
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
- 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).
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...
Важный момент - двухуровневый поиск. После упаковки ссылки в .git/refs могут исчезнуть как файлы, но продолжают существовать через packed-refs. Когда ты делаешь новый коммит на упакованной ветке, Git создаёт свежий loose-файл refs/heads/main, и он перекрывает запись из packed-refs. То есть loose-версия всегда главнее упакованной. Отсюда классические грабли: руками удалил файл .git/refs/heads/feature, а ветка не пропала - потому что её копия осталась в packed-refs. Поэтому удаляй ссылки только через
Код: Выделить всё
git update-ref -dПсевдо-ссылки и спец-рефы: ORIG_HEAD, MERGE_HEAD, stash, notes, replace
Кроме веток есть служебные ссылки, которые Git ставит сам. Они лежат прямо в .git (не в refs) и не упаковываются:
- ORIG_HEAD - куда HEAD указывал ДО опасной операции (reset, merge, rebase). Твоя кнопка "отмена": откатывает неудачный rebase.
Код: Выделить всё
git reset --hard ORIG_HEAD - MERGE_HEAD - существует только во время незавершённого merge, хранит SHA вливаемой ветки. По нему Git знает, что идёт слияние.
- FETCH_HEAD - что было только что получено последним git fetch (может содержать несколько строк - по ветке на каждую).
- CHERRY_PICK_HEAD, REVERT_HEAD - аналогично для прерванных cherry-pick и revert.
- 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 add -m "проверено QA, тикет JIRA-4821" a1c4f2e9
$ git log -1
commit a1c4f2e9b7d3056812ff3490ab12cd34ef567890
Author: ...
fix login race
Notes:
проверено QA, тикет JIRA-4821
Код: Выделить всё
git push origin refs/notes/commitsgit 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/*
То есть origin/main - не магия, а просто результат записи refspec: серверная refs/heads/main приехала в твою refs/remotes/origin/main. При push refspec работает в обратную сторону:
Код: Выделить всё
git push origin mainКод: Выделить всё
git push origin HEAD:refs/heads/feature-xКод: Выделить всё
git push origin :refs/heads/oldreftable: новый бэкенд хранения ссылок
Файлы и 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 станет дефолтным бэкендом для новых репозиториев.
Код: Выделить всё
$ git init --ref-format=reftable repo
$ ls repo/.git/refs
reftable/
$ ls repo/.git/reftable
0x000000000001-0x000000000001-xxxxxxxx.ref tables.list
Код: Выделить всё
$ git config --global init.defaultRefFormat reftable
Код: Выделить всё
$ git refs migrate --ref-format=reftable --dry-run
$ git refs migrate --ref-format=reftable
$ git rev-parse --show-ref-format
reftable
Код: Выделить всё
git refs migrate --ref-format=filesТипичные грабли
- Руками редактировал .git/refs. Файл поправил, а ветка не сдвинулась или, наоборот, "воскресла" - потому что есть копия в packed-refs, которая её перекрывает или дублирует. Всегда update-ref / branch, не текстовый редактор.
- Удалил ссылку, а коммиты потерялись. Если коммит держался только этой веткой (или detached HEAD) - он стал недостижим. Спасение - reflog: помнит старые SHA минимум 90 дней,
Код: Выделить всё
git reflogвозвращает.Код: Выделить всё
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
Контрольные вопросы
- Чем симолическая ссылка (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.