📱 Подписаться
IT и цифровая трансформация

[Перевод] origin master или origin/master: как Git хранит ветки в обычном словаре «ключ — значение»

📰 Habr 👁️ 4 просмотров

opium2 часа назадОбъяснить с

origin master или origin/master: как Git хранит ветки в обычном словаре «ключ — значение»

Время на прочтение5 минОхват и читатели2.6KGit*Программирование*Open source*ПереводАвтор оригинала: Alex Kladov (matklad)Недавно я заметно поправил свою ментальную модель Git. Возьмём две команды:

$ git fetch origin master
$ git switch -c my-feature origin/masterПонимаете, почему в одной написано origin master, а в другой origin/master? Я — нет, ещё пару недель назад не понимал!

Я представлял Git как контентно-адресуемую базу данных. Git хранит коммиты, коммит определяется хешем своего содержимого, а содержимое коммита — это в основном:

• снимок состояния кода в определённый момент, без какой-либо памяти о прошлом;
• список (хешей) родительских коммитов.Этого мне хватало, чтобы читать вывод git log и выбираться из любого запоротого ребейза, не клонируя репозиторий заново. (Примерно половину карьеры я как раз клонировал его заново. Ничего стыдного! Разобраться в Git полезно, но, когда только начинаешь, это не самое важное, чему стоит учиться.)

Теперь я понимаю, что в Git есть не только неизменяемая база с адресацией по содержимому, куда можно лишь дописывать, но и самое обычное изменяемое хранилище «ключ — значение».

У Git есть изменяемый словарь: ключи в нём — строки, значения — объекты из той самой контентно-адресуемой базы. Ключи по соглашению записываются как пути в файловой системе, и содержимое словаря обычно можно посмотреть, просто заглянув в каталог .git/refs:

$ eza -T .git/refs
.git/refs
├── heads
│ ├── make
│ ├── master
│ ├── my-feature
│ └── pbd-adt
├── origin
├── remotes
│ └── origin
│ ├── context-switches
│ ├── gh-pages
│ ├── HEAD
│ ├── make
│ └── master
└── tags

$ cat .git/refs/heads/master b59148228e52f7c615ead7fdd4e91001994ad50f

$ git show-ref refs/heads/master
b59148228e52f7c615ead7fdd4e91001994ad50f refs/heads/masterЗапутывает в этой инфраструктуре ссылок (refs) вот что:

• На ней держится множество разных видимых пользователю возможностей Git, но сами ссылки — деталь реализации.
• Абстракция немного протекает: в повседневной работе ссылки почти не видны.
• В командах Git полно сокращённых записей ссылок и аргументов по умолчанию, поэтому далеко не очевидно, что конкретный аргумент — это ссылка.
• И, как обычно, Git любит давать одной вещи несколько имён и называть одним именем разные вещи.Ветки, теги и git notes — всё это просто ссылки!

Структура становится куда понятнее, если развернуть все сокращения. Исходная команда

$ git fetch origin masterпревращается в

$ git fetch \
https://github.com/matklad/matklad.github.io \
refs/heads/master:refs/remotes/origin/masterПервый аргумент fetch (https://...) — адрес удалённого репозитория. Git «звонит» по этому адресу и перекачивает с той машины часть данных по сети.

Второй аргумент — пара строковых ключей (ссылок) вида источник:цель. Источник — ключ в удалённом репозитории, цель — имя локального ключа. В целом fetch просит Git прочитать значение из удалённого репозитория и сохранить его у себя под другим именем.

Чтобы не набирать каждый раз URL репозитория, Git даёт им символические имена, и origin — общепринятое имя для основного удалённого репозитория:

$ git fetch origin \
refs/heads/master:refs/remotes/origin/masterrefs/heads/master — полное имя ветки в удалённом репозитории. То есть ветка my-feature — это всего лишь ссылка refs/heads/my-feature. Её могли бы назвать refs/branch/my-feature, но не назвали :)

Точных правил сокращения я не знаю, но в целом Git позволяет писать только хвост имени ссылки:

$ git fetch origin \
master:refs/remotes/origin/masterrefs/remotes/origin/master — имя локальной ссылки, куда мы сохраним результат. Казалось бы, естественнее взять локально то же имя, что и в удалённом репозитории, но это работает, только пока удалённый репозиторий один. Если их два (например, ваш форк и оригинальный репозиторий, от которого вы форкнулись), имена ссылок столкнутся. Поэтому ссылки удалённого репозитория foo складываются в отдельное пространство имён refs/remotes/foo. А origin — просто общепринятое имя для того самого удалённого репозитория в простых конфигурациях.

Опять же, естественнее было бы напрямую повторить у себя структуру удалённых ссылок:

refs/ heads/my-branch -> refs/ remotes/origin/ heads/my-branchно Git выбрасывает лишний компонент heads. Это отображение «убрать heads, добавить remotes/$remote» встроено в Git, и оно сжимает команду до

$ git fetch origin masterПрим. перев.: точнее, отображение задаёт не сам Git, а настройкаremote.origin.fetch, которуюgit cloneиgit remote addзаписывают по умолчанию:+refs/heads/*:refs/remotes/origin/*. Если её поменять,git fetch origin masterобновит другую ссылку.

Стоит задуматься, почему всё устроено именно так. Модель Git рассчитана на работу без сети. Более того, она предполагает явные точки синхронизации. Git не синхронизируется с удалённым репозиторием в фоне, когда есть связь: чтобы байты ушли по проводу, нужно явно выполнить fetch или push. В такой парадигме полезно хранить у себя модель того, в каком состоянии была удалённая сторона, когда мы с ней в последний раз говорили. Теория разума, только для репозиториев!

Надеюсь, это проясняет, чем в Git локальные ветки отличаются от удалённых. Возьмём ветку main. В удалённом репозитории origin она существует как refs/heads/main. Когда вы синхронизируете свой локальный репозиторий с origin, у вас появляется refs/remotes/origin/main — ваше последнее знание о том, в каком состоянии main на origin.

А ещё есть ваша локальная refs/heads/main. Обычно она сначала указывает на тот же коммит, что и refs/remotes/origin/main. Но когда вы делаете коммит, refs/heads/main сдвигается вперёд, а refs/remotes/origin/main остаётся на месте.

Когда вы пытаетесь запушить свой коммит в origin, а ветка main на origin за это время ушла вперёд, вы получите конфликт. Тогда Git сам обновит refs/remotes/origin/main (это ведь всего лишь локальное зеркало удалённого состояния), а вот обновить refs/heads/main и запушить снова — уже ваша забота.

Прим. перев.: здесь автор неточен. Отклонённыйpushничего не скачивает, иrefs/remotes/origin/mainостаётся прежней: мы проверили это в контейнере с Git. Чтобы увидеть новые коммиты сorigin, сначала нуженgit fetch(илиgit pull), и только потом rebase или merge.

Вернёмся к исходному примеру:

$ git fetch origin master
$ git switch -c my-feature origin/masterПервая команда находит в .git/config URL удалённого репозитория origin и отправляет запрос на ту машину. В результате локальная refs/remotes/origin/master начинает указывать на тот же коммит, что и refs/heads/master на удалённой стороне (а сам коммит со всеми предками при этом скачивается к вам).

Вторая команда создаёт ссылку refs/heads/my-feature (то есть ветку), начальная точка которой — refs/remotes/origin/master. Тут абстракция как раз и протекает.

Второй аргумент здесь — (сокращённая) ссылка, поэтому можно написать и так:

$ git switch -c my-feature \
refs/remotes/origin/masterА вот первый аргумент, хотя и создаёт ссылку, сам ссылкой не является. Иначе говоря, если развернуть и его

$ git switch -c refs/heads/my-feature \
refs/remotes/origin/masterполучится

refs/heads/refs/heads/my-featureВот и всё! Не уверен, что это особенно полезно, но, может быть, хотя бы интересно!Теги:• git
• git fetch
• refs
• ветки
• remote
• origin
• контроль версийХабы:• Git
• Программирование
• Open source

Получайте больше инсайтов о систематизации бизнеса

Подписывайтесь на Telegram-канал Business Operations — ежедневные материалы о бизнес-процессах, операционном управлении и повышении эффективности

💬 Подписаться на канал