- Андрей Куманяев/
- Git: руководства и команды/
- Git refs: почему ветка занимает 41 байт и как устроены ссылки/
Git refs: почему ветка занимает 41 байт и как устроены ссылки
Вы когда-нибудь задавались вопросом, что на самом деле скрывается за словом «ветка» в Git? Это не какая-то магическая сущность, хранящаяся в базе данных. Это просто файл. Обычный текстовый файл, размером ровно 41 байт. Давайте заглянем в папку .git/refs/ и увидим это своими глазами — там лежат все ветки вашего репозитория, и каждая из них это просто файл с названием ветки, содержащий одну строку текста.
Почему ветка занимает ровно 41 байт? #
Ответ прост и элегантен. Ветка в Git — это указатель на коммит. А коммит в Git идентифицируется SHA-1 хешем, который представляет собой 40 шестнадцатеричных символов. Когда вы создаёте ветку, Git создаёт файл в папке .git/refs/heads/ с названием ветки, и этот файл содержит 40 символов хеша плюс символ новой строки \n. Итого: 40 + 1 = 41 байт.
Давайте это проверим. Если у вас есть ветка main, откройте терминал и выполните:
cat .git/refs/heads/main
Вы увидите примерно следующее:
1a2b3c4d5e6f7g8h9i0j1a2b3c4d5e6f7g8h9i0j
А теперь проверим размер файла:
wc -c .git/refs/heads/main
# 41
Вот она, магия! Ровно 41 байт. Это не прихоть Git, это простая математика: 40 символов хеша (по одному байту на символ) плюс один байт символа новой строки в конце файла. Git просто хранит текстовую запись о том, на какой коммит указывает каждая ветка.
Можно даже посмотреть это в шестнадцатеричном формате:
hexdump -C .git/refs/heads/main | head -3
# 00000000 31 61 32 62 33 63 34 64 35 65 36 66 37 67 38 68 |1a2b3c4d5e6f7g8h|
# 00000010 39 69 30 6a 31 61 32 62 33 63 34 64 35 65 36 66 |9i0j1a2b3c4d5e6f|
# 00000020 37 67 38 68 39 69 30 6a 0a |7g8h9i0j.|
Последний байт 0a — это символ новой строки. Вот и все устройство ветки.
Структура папки refs в .git #
Git организует ссылки (refs) в иерархическую структуру внутри папки .git/refs/. Когда вы заглядываете туда, вы видите несколько основных категорий ссылок.
Самая важная категория — это refs/heads/. Здесь находятся все локальные ветки вашего репозитория. Когда вы создаёте новую ветку feature/login, Git создаёт файл .git/refs/heads/feature/login, содержащий хеш текущего коммита. Обратите внимание на слэш в названии ветки — Git создаст соответствующую вложенность папок. Это удобно для организации: ветки с префиксом feature/ будут в папке feature/, ветки с префиксом bugfix/ — в папке bugfix/, и так далее.
Вторая категория — refs/tags/. Здесь находятся все аннотированные теги вашего репозитория. Если вы создали тег v1.0.0 командой git tag -a v1.0.0 -m "Release 1.0.0", Git создаст файл .git/refs/tags/v1.0.0. Важный момент: лёгкие теги (lightweight tags), созданные без флага -a, тоже содержат ссылку на коммит, но хранятся по-другому. Есть также папка refs/tags/ для упакованных тегов.
Третья категория — refs/remotes/. Если у вас есть удалённый репозиторий (например, origin), то ссылки на его ветки хранятся в refs/remotes/origin/. Когда вы выполняете git fetch, Git обновляет файлы в этой папке, чтобы отразить текущее состояние ветвей на удалённом сервере. Например, если на origin есть ветка main, то её локальное отражение хранится в .git/refs/remotes/origin/main.
Есть также менее заметная папка refs/stash/, где Git хранит информацию о сохранённых (stashed) изменениях. Это не совсем ветки, но ссылки на снимки (snapshots) вашего рабочего состояния.
Давайте посмотрим всю структуру:
find .git/refs -type f
Вы увидите примерно такое дерево:
.git/refs/
├── heads/
│ ├── main
│ ├── develop
│ ├── feature/
│ │ ├── login
│ │ └── dashboard
│ └── bugfix/
│ └── typo-fix
├── tags/
│ ├── v1.0.0
│ ├── v1.0.1
│ └── v1.1.0
└── remotes/
└── origin/
├── main
└── develop
Каждый файл в этой структуре — это ссылка на коммит, и каждый из них содержит 40 символов хеша плюс новая строка.
HEAD — особая символическая ссылка #
Но есть один специальный файл, который устроен не совсем так же. Это файл .git/HEAD. Он тоже является ссылкой, но не прямой ссылкой на коммит, а символической ссылкой (symbolic reference) на другую ссылку.
Откройте этот файл:
cat .git/HEAD
Вы увидите что-то вроде:
ref: refs/heads/main
Это означает, что HEAD в настоящий момент указывает на ветку main. Когда вы делаете коммит, Git использует HEAD, чтобы понять, на какую ветку нужно переместить указатель. Git разрешает символическую ссылку и узнаёт, что нужно обновить файл .git/refs/heads/main.
Когда вы переключаетесь на другую ветку:
git checkout develop
Git обновляет содержимое файла .git/HEAD на:
ref: refs/heads/develop
Иногда HEAD находится в так называемом «detached» состоянии (отсоединённая HEAD). Это происходит, когда вы переключаетесь непосредственно на коммит, а не на ветку:
git checkout abc1234def5678
В этом случае содержимое .git/HEAD не будет ссылкой, а будет прямым хешем:
abc1234def5678
Вы можете управлять HEAD программно командой git symbolic-ref. Давайте посмотрим текущую ветку:
git symbolic-ref HEAD
# refs/heads/main
Или просто:
git symbolic-ref --short HEAD
# main
Можно даже изменить HEAD вручную (хотя это редко нужно):
git symbolic-ref HEAD refs/heads/develop
Эта команда переместит HEAD на ветку develop, но не изменит ваше рабочее дерево. Это может быть полезно в скриптах.
Ошибка: “fatal: refusing to point HEAD outside of refs/” #
Когда вы работаете с Git, вы можете иногда увидеть странную ошибку:
fatal: refusing to point HEAD outside of refs/
Это происходит, когда кто-то (часто случайно) пытается установить HEAD на какой-то произвольный путь, который находится вне папки refs/. Git специально проверяет, что HEAD всегда указывает либо на ветку в refs/heads/, либо на другую ссылку в папке refs/.
Например, вы можете спровоцировать эту ошибку, если попытаетесь сделать:
git symbolic-ref HEAD ../../../etc/passwd
# fatal: refusing to point HEAD outside of refs/
Git защищает себя, чтобы убедиться, что логика репозитория остаётся консистентной.
packed-refs: как Git упаковывает ссылки #
По умолчанию Git хранит каждую ветку в отдельном файле. Но когда репозиторий становится большим с сотнями или тысячами ветвей, это неэффективно. Git может упаковать все ссылки в один файл: .git/packed-refs.
Это может произойти автоматически, или вы можете явно упаковать ссылки:
git pack-refs --all
Откройте файл .git/packed-refs:
cat .git/packed-refs
Вы увидите что-то вроде:
# pack-refs with: peeled fully-peeled
1a2b3c4d5e6f7g8h9i0j1a2b3c4d5e6f7g8h9i0j refs/heads/main
2b3c4d5e6f7g8h9i0j1a2b3c4d5e6f7g8h9i0j refs/heads/develop
3c4d5e6f7g8h9i0j1a2b3c4d5e6f7g8h9i0j refs/tags/v1.0.0
Это компактный формат: хеш, пробел, и полный путь к ссылке. Когда Git ищет ссылку, он сначала проверяет папку refs/, а если не найдёт, посмотрит в packed-refs. Когда вы создаёте новую ветку, файлы в папке refs/ имеют приоритет над packed-refs.
Распаковать ссылки обратно:
git pack-refs --no-all
Или просто удалить файл .git/packed-refs и пересоздать отдельные файлы.
Команды для работы с refs #
Git предоставляет несколько команд для просмотра и управления ссылками. Это низкоуровневые, мощные инструменты для работы с внутренним устройством репозитория.
Самая простая команда — git show-ref. Она выводит все ссылки в репозитории вместе с их хешами:
git show-ref
# 1a2b3c4d5e6f7g8h9i0j1a2b3c4d5e6f7g8h9i0j refs/heads/main
# 2b3c4d5e6f7g8h9i0j1a2b3c4d5e6f7g8h9i0j refs/heads/develop
# 3c4d5e6f7g8h9i0j1a2b3c4d5e6f7g8h9i0j refs/remotes/origin/main
# 4d5e6f7g8h9i0j1a2b3c4d5e6f7g8h9i0j refs/tags/v1.0.0
Если вам нужны только ветки:
git show-ref --heads
Или только теги:
git show-ref --tags
Можно отфильтровать по части названия:
git show-ref | grep feature
Более мощный инструмент — git for-each-ref. Это команда с форматированием, которая позволяет вам вывести информацию о ссылках в любом формате:
git for-each-ref
По умолчанию выводит трёхколоночный формат: хеш, тип объекта и путь к ссылке. Но вы можете задать свой формат:
git for-each-ref --format='%(refname:short) %(objectname:short) %(committerdate:short)'
Это выведет: название ветки (без refs/heads/), короткий хеш и дату коммита. Очень полезно для анализа истории ветвей.
Отфильтровать по типу ссылки:
git for-each-ref --format='%(refname)' refs/heads/
Отсортировать по дате создания:
git for-each-ref --sort=-committerdate refs/heads/
Минус перед committerdate означает обратную сортировку (новые первыми).
Ещё одна полезная команда — git rev-parse. Она преобразует любое название ветки или ссылку в полный хеш коммита:
git rev-parse main
# 1a2b3c4d5e6f7g8h9i0j1a2b3c4d5e6f7g8h9i0j
git rev-parse HEAD
# 1a2b3c4d5e6f7g8h9i0j1a2b3c4d5e6f7g8h9i0j
git rev-parse origin/main
# 2b3c4d5e6f7g8h9i0j1a2b3c4d5e6f7g8h9i0j
Практические примеры: работа с refs #
Давайте посмотрим на реальные примеры использования этих команд.
Найти самый старый тег в репозитории. Хотим вывести все теги в обратном хронологическом порядке:
git for-each-ref --sort=version:refname --format='%(refname:short)' refs/tags/
Флаг version:refname выполняет правильную сортировку версий (1.0 < 1.10, а не 1.0 < 1.2 как при лексикографической сортировке).
Найти последний коммит в каждой ветке. Выведем все ветки с датой последнего коммита:
git for-each-ref --sort=-committerdate --format='%(refname:short) %(committerdate:short) %(authorname)' refs/heads/
Это вкупе с --count полезно для очистки:
git for-each-ref --sort=-committerdate --format='%(refname:short)' refs/heads/ | head -20
Выведет 20 самых свежих веток.
Найти ветки, которые уже слиты в main. Нужно понять, какие ветки можно удалить:
git branch --merged main
Это не работает с refs напрямую, но под капотом использует именно их.
Получить все коммиты, на которые указывают теги. Хотим вывести все объекты, на которые указывают теги:
git for-each-ref --format='%(objectname)' refs/tags/
Проверить, является ли определённый коммит предком ветки. Полезно в скриптах:
git merge-base --is-ancestor abc1234 main && echo "yes" || echo "no"
FAQ: часто задаваемые вопросы #
Что такое HEAD в контексте GitHub Pull Request? Когда вы создаёте pull request на GitHub, вы указываете два «конца» merge: base и head. Head — это ветка, из которой вы хотите merge (обычно ваша feature-ветка). Base — это ветка, в которую вы хотите merge (обычно main). GitHub использует эту информацию, чтобы показать вам, какие коммиты будут добавлены. Это не совсем то же самое, что HEAD в Git, но название одно и то же.
Чем отличается ссылка от объекта в Git? Объект (object) — это то, что хранится в папке .git/objects/. Это blob, tree, commit или tag. Ссылка (ref) — это способ обращения к объекту по имени. Ветка — это ссылка на commit-объект. Тег — это ссылка на объект (обычно commit или tag). Refs дают человеческие названия объектам, которые иначе были бы идентифицированы только по их SHA-1 хешам.
Может ли refs указывать на произвольное место? Не совсем. Git проверяет, что ссылки указывают на валидные объекты в репозитории. Вы не можете создать ссылку на несуществующий коммит. Но технически ничто не мешает ссылке указывать на любой объект: commit, tree, blob, tag. На практике ветки указывают на commits, теги на commits или на другие теги.
Что произойдёт, если я удалю файл .git/refs/heads/main вручную? Ветка main исчезнет. Но коммиты, на которые указывала эта ветка, не будут удалены — они остаются в .git/objects/. Git вообще хранит данные очень консервативно. Если у вас есть доступ к git reflog, вы сможете восстановить ветку.
Зачем нужна символическая ссылка HEAD? HEAD нужна для того, чтобы Git знал, на какую ветку указывать при создании нового коммита. Если бы HEAD была просто файлом с хешем, то при каждом коммите Git не знал бы, какую ветку обновлять. Символическая ссылка решает эту проблему: она указывает на ветку, и Git может обновить эту ветку через refs/heads/.
Заключение #
Теперь вы знаете, что ветка в Git — это просто файл размером 41 байт. Это может звучать излишне упрощённо, но это ядро всего управления версиями в Git. Refs — это абстракция, которая делает Git удобным для человека. Вместо того чтобы помнить странные 40-символные хеши, вы можете давать ветвям понятные названия. Git просто хранит соответствие между названием и хешем в виде простых текстовых файлов.
Понимание того, как устроены refs, помогает разобраться с более сложными Git-операциями. Когда вы видите странные ошибки, вы можете заглянуть в папку .git/refs/ и увидеть, что там на самом деле. Это знание также полезно при написании скриптов и инструментов, которые работают с Git на низком уровне.
Чтобы углубить своё понимание, прочитайте статьи о том, как устроен Git изнутри, о файле .git/HEAD, о тегах в Git, о git хешах и о git reflog. Каждая из этих статей раскрывает отдельный аспект архитектуры Git.