Текущая ветвь/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/maingit statusсообщаетOn branch main- Последнее, что я проверял:
main - Подсказка git в моей оболочке сообщает:
(main)
В данном случае все четыре определения совпадают: они все main. Достаточно просто.
Сценарий 2: сразу после git checkout 775b2b399
Теперь представим, что я проверяю определённый ID коммита (так что мы находимся в "detached HEAD state").
.git/HEADсодержит775b2b399fb8b13ee3341e819f2aa024a37fa92git statusсообщаетHEAD detached at 775b2b39- Последнее, что я проверял:
775b2b399 - Подсказка git в моей оболочке сообщает:
((775b2b39))
Опять, все они в основном совпадают — некоторые из них обрезали ID коммита, а некоторые нет, но это всё. Давайте двигаться дальше.
Сценарий 3: сразу после git checkout v1.0.13
Что, если мы проверили тег, а не ID ветви или коммита?
.git/HEADсодержитca182053c7710a286d72102f4576cf32e0dafcfbgit 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/maingit 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/maingit 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 mainHEAD detached at 775b2b39HEAD detached at v1.0.13interactive rebase in progress; onto c694cf8on 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, тем больше понимаю, почему люди запутались.