Текущая ветвь/current branch в git

Привет! Недавно написал статью о HEAD в git, но задумался, что означает термин "текущая ветвь" или "current branch" в git, и это немного страннее, чем я думал.

Четыре возможных определения для "текущей ветви"

  1. Это то, что находится в файле .git/HEAD. Так его определяет глоссарий git.
  2. Это то, что git status сообщает в первой строке.
  3. Это то, что вы в последний раз проверяли/загружали с помощью git checkout или git switch.
  4. Это то, что находится в подсказке git оболочки. Я использую fish_git_prompt, поэтому буду говорить именно о нём.

Изначально я думал, что все эти четыре определения более или менее одинаковы. Но пообщавшись с людьми на Mastodon, я понял, что они отличаются друг от друга больше, чем казалось.

Итак, давайте поговорим о нескольких сценариях использования git и о том, как каждое из этих определений работает в каждом из них. Для всех этих экспериментов использовался git версии 2.39.2 (Apple Git-143).

Сценарий 1: сразу после git checkout main

Самая обычная ситуация: вы проверяете ветвь.

  1. .git/HEAD содержит ref: refs/heads/main
  2. git status сообщает On branch main
  3. Последнее, что я проверял: main
  4. Подсказка git в моей оболочке сообщает: (main)

В данном случае все четыре определения совпадают: они все main. Достаточно просто.

Сценарий 2: сразу после git checkout 775b2b399

Теперь представим, что я проверяю определённый ID коммита (так что мы находимся в "detached HEAD state").

  1. .git/HEAD содержит 775b2b399fb8b13ee3341e819f2aa024a37fa92
  2. git status сообщает HEAD detached at 775b2b39
  3. Последнее, что я проверял: 775b2b399
  4. Подсказка git в моей оболочке сообщает: ((775b2b39))

Опять, все они в основном совпадают — некоторые из них обрезали ID коммита, а некоторые нет, но это всё. Давайте двигаться дальше.

Сценарий 3: сразу после git checkout v1.0.13

Что, если мы проверили тег, а не ID ветви или коммита?

  1. .git/HEAD содержит ca182053c7710a286d72102f4576cf32e0dafcfb
  2. git status сообщает HEAD detached at v1.0.13
  3. Последнее, что я проверял: v1.0.13
  4. Подсказка 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, выполню ребейз, но в середине ребейза возникнет конфликт слияния? Вот ситуация:

  1. .git/HEAD содержит c694cf8aabe2148b2299a988406f3395c0461742 (ID коммита, на который я делаю ребейз, в данном случае origin/main)
  2. git status сообщает interactive rebase in progress; onto c694cf8
  3. Последнее, что я проверял: main
  4. Подсказка 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?

  1. .git/HEAD содержит ref: refs/heads/main
  2. git status сообщает On branch main (и "No commits yet")
  3. Последнее, что я проверял, в общем, ничего
  4. Подсказка git в моей оболочке сообщает: (main)

Итак, здесь всё в основном совпадает, за исключением того, что мы никогда не запускали git checkout или git switch. В основном Git автоматически переключается на ту ветвь, которая была настроена в init.defaultBranch.

Сценарий 6: голый git-репозиторий

Что если мы клонируем голый репозиторий с помощью git clone --bare https://github.com/rbspy/rbspy?

  1. .git/HEAD содержит ref: refs/heads/main
  2. git status сообщает fatal: this operation must be run in a work tree
  3. Последнее, что я проверял, в общем, ничего, git checkout даже не работает в голых репозиториях
  4. Подсказка 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/HEADgit statuschecked outprompt
1. checkout mainref: refs/heads/mainOn branch mainmain(main)
2. checkout 775b2b775b2b399...HEAD detached at 775b2b39775b2b399((775b2b39))
3. checkout v1.0.13ca182053c...HEAD detached at v1.0.13v1.0.13((v1.0.13))
4. во время rebasec694cf8aa...interactive rebase in progress; onto c694cf8main(main|REBASE-i 1/1)
5. после git initref: refs/heads/mainOn branch mainn/a(main)
6. bare repositoryref: refs/heads/mainfatal: this operation must be run in a work treen/a(BARE:main)

"Текущая ветвь" кажется не совсем удачным определением

Изначально, когда я говорил о git, мне хотелось согласиться с глоссарием git и сказать, что HEAD и "текущая ветвь"/"current branch" означают одно и то же.

Но это уже не кажется таким незыблемым, как я думал раньше! Некоторые размышления:

  • .git/HEAD, безусловно, имеет наиболее последовательный формат — это всегда либо ветвь, либо ID коммита. Все остальные гораздо более запутанны.
  • Я испытываю гораздо больше симпатии, чем раньше, к определению текущая ветвь — это та, которую вы последний раз проверяли. Git проделывает огромную работу, чтобы запомнить, какую ветвь вы проверили в последний раз (даже если в данный момент вы делаете bisect, merge или что-то ещё, что временно перемещает HEAD из этой ветви), и игнорировать это кажется странным.
  • git status даёт много полезного контекста — эти 5 сообщений о статусе говорят гораздо больше, чем просто о том, какое значение HEAD установлено в данный момент
    1. on branch main
    2. HEAD detached at 775b2b39
    3. HEAD detached at v1.0.13
    4. interactive rebase in progress; onto c694cf8
    5. on branch main, no commits yet

Ещё несколько определений "текущей ветви"

Я попытаюсь подобрать другие определения термина "текущая ветвь"/"current branch", которые я слышал от людей на Mastodon, и написать несколько заметок о них.

  1. Ветвь, которая будет обновлена, если я выполню коммит
    • В большинстве случаев это то же самое, что и .git/HEAD
    • Возможно, если вы находитесь в середине ребейза, это отличается от HEAD, потому что в конечном итоге этот новый коммит окажется в ветке в .git/rebase-merge/head-name
  2. Ветвь, с которой работает большинство операций 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, тем больше понимаю, почему люди запутались.

Дополнительные материалы

Предыдущая Статья

Практическое применение Flexbox

Следующая Статья

Утверждение исключений в тестах Laravel 11