Текущая ветвь/current branch в git
Четыре возможных определения для "текущей ветви"
- Это то, что находится в файле
.git/HEAD
. Так его определяет глоссарий git. - Это то, что
git status
сообщает в первой строке. - Это то, что вы в последний раз проверяли/загружали с помощью
git checkout
илиgit switch
. - Это то, что находится в подсказке git оболочки. Я использую fish_git_prompt, поэтому буду говорить именно о нём.
Изначально я думал, что все эти четыре определения более или менее одинаковы. Но пообщавшись с людьми на Mastodon, я понял, что они отличаются друг от друга больше, чем казалось.
Итак, давайте поговорим о нескольких сценариях использования git и о том, как каждое из этих определений работает в каждом из них. Для всех этих экспериментов использовался git версии 2.39.2
(Apple Git-143).
Сценарий 1: сразу после git checkout main
Самая обычная ситуация: вы проверяете ветвь.
.git/HEAD
содержитref: refs/heads/main
git status
сообщаетOn branch main
- Последнее, что я проверял:
main
- Подсказка git в моей оболочке сообщает:
(main)
В данном случае все четыре определения совпадают: они все main
. Достаточно просто.
Сценарий 2: сразу после git checkout 775b2b399
Теперь представим, что я проверяю определённый ID коммита (так что мы находимся в "detached HEAD state").
.git/HEAD
содержит775b2b399fb8b13ee3341e819f2aa024a37fa92
git status
сообщаетHEAD detached at 775b2b39
- Последнее, что я проверял:
775b2b399
- Подсказка git в моей оболочке сообщает:
((775b2b39))
Опять, все они в основном совпадают — некоторые из них обрезали ID коммита, а некоторые нет, но это всё. Давайте двигаться дальше.
Сценарий 3: сразу после git checkout v1.0.13
Что, если мы проверили тег, а не ID ветви или коммита?
.git/HEAD
содержитca182053c7710a286d72102f4576cf32e0dafcfb
git status
сообщаетHEAD detached at v1.0.13
- Последнее, что я проверял:
v1.0.13
- Подсказка git в моей оболочке сообщает:
((v1.0.13))
Теперь всё начинает становиться немного странным! Значения .git/HEAD
расходятся с тремя другими значениями: git status
, сообщение git и то, что я проверил, все одинаковы (v1.0.13
), но .git/HEAD
содержит ID коммита.
Причина в том, что git пытается помочь нам: ID коммитов довольно непрозрачны, поэтому если есть метка, соответствующая текущему коммиту, git status
нам её покажет.
Несколько примечаний по этому поводу:
Если мы проверяем коммит по его ID (
git checkout ca182053c7710a286d72
), а не по его тегу, то то, что отображается вgit status
и в моей подсказке shell, абсолютно одинаково — git на самом деле не "знает", что мы проверили тег.Похоже, что можно найти теги, соответствующие HEAD, выполнив
git describe HEAD --tags --exact-match
(вот код для fish git prompt)Вы можете увидеть, как
git-prompt.sh
добавил поддержку описания коммита по тегу таким образом в коммите 27c578885 в 2008 году.Я не знаю, есть ли разница в том, аннотирован тег или нет.
Если есть 2 тега с одним и тем же ID коммита, это становится немного странным. Например, если я добавлю тег
v1.0.12
к этому коммиту, чтобы он был и сv1.0.12
, и сv1.0.13
, вы увидите, что подсказка git изменится, а затем подсказка иgit status
разойдутся во мнениях о том, какой тег отображать:bork@grapefruit ~/w/int-exposed ((v1.0.12))> git status
HEAD detached at v1.0.13(моя подсказка показывает
v1.0.12
, аgit status
показываетv1.0.13
)
Сценарий 4: в середине ребейза
Теперь: что если я проверю ветку main
, выполню ребейз, но в середине ребейза возникнет конфликт слияния? Вот ситуация:
.git/HEAD
содержитc694cf8aabe2148b2299a988406f3395c0461742
(ID коммита, на который я делаю ребейз, в данном случаеorigin/main
)git status
сообщаетinteractive rebase in progress; onto c694cf8
- Последнее, что я проверял:
main
- Подсказка git в моей оболочке сообщает:
(main|REBASE-i 1/1)
Несколько примечаний по этому поводу:
- Думаю, что в каком-то смысле "текущая ветвь" здесь
main
— это то, что я недавно проверил, это то, к чему мы вернёмся после завершения ребейза, и это то, к чему мы вернёмся, если я выполнюgit rebase --abort
- В другом смысле, мы находимся в detached HEAD state в
c694cf8aabe2
. Но это не имеет обычных последствий пребывания в "detached HEAD state" — если вы сделаете коммит, он не будет осиротевшим! Вместо этого, при условии, что вы закончите ребейз, он будет поглощён ребейзом и помещён куда-то в середину вашей ветки. - Похоже, что во время ребейза старая "текущая ветвь" (
main
) сохраняется в.git/rebase-merge/head-name
. Хотя я не совсем в этом уверен.
Сценарий 5: сразу после git init
А как насчёт того, чтобы создать пустой репозиторий с помощью git init
?
.git/HEAD
содержитref: refs/heads/main
git status
сообщаетOn branch main
(и "No commits yet")- Последнее, что я проверял, в общем, ничего
- Подсказка git в моей оболочке сообщает:
(main)
Итак, здесь всё в основном совпадает, за исключением того, что мы никогда не запускали git checkout
или git switch
. В основном Git автоматически переключается на ту ветвь, которая была настроена в init.defaultBranch
.
Сценарий 6: голый git-репозиторий
Что если мы клонируем голый репозиторий с помощью git clone --bare https://github.com/rbspy/rbspy
?
.git/HEAD
содержитref: refs/heads/main
git status
сообщаетfatal: this operation must be run in a work tree
- Последнее, что я проверял, в общем, ничего,
git checkout
даже не работает в голых репозиториях - Подсказка git в моей оболочке сообщает:
(BARE:main)
Итак, №1 и №4 совпадают (они оба согласны с тем, что текущая ветвь — "main"), но git status
и git checkout
даже не работают.
Несколько примечаний по этому поводу:
- Я думаю, что
HEAD
в голом репозитории в основном влияет только на одну вещь: это ветвь, которая будет проверена, когда вы клонируете репозиторий. Она также используется при запускеgit log
. - Если вы действительно хотите, то можете обновить
HEAD
в голом репозитории на другую ветвь с помощьюgit symbolic-ref HEAD refs/heads/whatever
. Однако мне никогда не приходилось этого делать, и это кажется странным, потому чтоgit symbolic ref
не проверяет, является ли ветвь, на которую вы указываетеHEAD
, действительно существующей. Не уверен, что есть лучший способ.
Все результаты
Вот таблица со всеми результатами:
.git/HEAD | git status | checked out | prompt | |
---|---|---|---|---|
1. checkout main | ref: refs/heads/main | On branch main | main | (main) |
2. checkout 775b2b | 775b2b399... | HEAD detached at 775b2b39 | 775b2b399 | ((775b2b39)) |
3. checkout v1.0.13 | ca182053c... | HEAD detached at v1.0.13 | v1.0.13 | ((v1.0.13)) |
4. во время rebase | c694cf8aa... | interactive rebase in progress; onto c694cf8 | main | (main|REBASE-i 1/1) |
5. после git init | ref: refs/heads/main | On branch main | n/a | (main) |
6. bare repository | ref: refs/heads/main | fatal: this operation must be run in a work tree | n/a | (BARE:main) |
"Текущая ветвь" кажется не совсем удачным определением
Изначально, когда я говорил о git, мне хотелось согласиться с глоссарием git и сказать, что HEAD
и "текущая ветвь"/"current branch" означают одно и то же.
Но это уже не кажется таким незыблемым, как я думал раньше! Некоторые размышления:
.git/HEAD
, безусловно, имеет наиболее последовательный формат — это всегда либо ветвь, либо ID коммита. Все остальные гораздо более запутанны.- Я испытываю гораздо больше симпатии, чем раньше, к определению
текущая ветвь — это та, которую вы последний раз проверяли
. Git проделывает огромную работу, чтобы запомнить, какую ветвь вы проверили в последний раз (даже если в данный момент вы делаете bisect, merge или что-то ещё, что временно перемещает HEAD из этой ветви), и игнорировать это кажется странным. git status
даёт много полезного контекста — эти 5 сообщений о статусе говорят гораздо больше, чем просто о том, какое значениеHEAD
установлено в данный моментon branch main
HEAD detached at 775b2b39
HEAD detached at v1.0.13
interactive rebase in progress; onto c694cf8
on branch main, no commits yet
Ещё несколько определений "текущей ветви"
Я попытаюсь подобрать другие определения термина "текущая ветвь"/"current branch", которые я слышал от людей на Mastodon, и написать несколько заметок о них.
Ветвь, которая будет обновлена, если я выполню коммит
- В большинстве случаев это то же самое, что и
.git/HEAD
- Возможно, если вы находитесь в середине ребейза, это отличается от
HEAD
, потому что в конечном итоге этот новый коммит окажется в ветке в.git/rebase-merge/head-name
- В большинстве случаев это то же самое, что и
Ветвь, с которой работает большинство операций git.
- Это примерно то же самое, что и в
.git/HEAD
, за исключением того, что некоторые операции (например,git status
) будут вести себя по-другому в некоторых ситуациях. Например,git status
не сообщит текущую ветвь, если вы находитесь в голом репозитории.
- Это примерно то же самое, что и в
На осиротевших коммитах
Я заметил одну вещь, которая не была учтена во всём этом: является ли текущий коммит осиротевшим или нет — сообщение git status
(HEAD detached from c694cf8
) одинаково независимо от того, осиротел ваш текущий коммит или нет.
Я полагаю, это связано с тем, что выяснение того, является ли данный коммит осиротевшим, может занять много времени в большом хранилище: вы можете узнать, является ли текущий коммит осиротевшим, с помощью git branch --contains HEAD
, и эта команда занимает около 500 мс в хранилище с 70 000 коммитов.
Git предупредит вас, что коммит осиротел ("Warning: you are leaving 1 commit behind, not connected to any of your branches..."), когда вы переключитесь на другую ветвь.
Вот и всё!
У меня нет ничего особенно умного, чтобы сказать по этому поводу. Чем больше думаю о git, тем больше понимаю, почему люди запутались.