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

Источник: «The "current branch" in 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 нам её покажет.

Несколько примечаний по этому поводу:

Сценарий 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)

Несколько примечаний по этому поводу:

Сценарий 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 даже не работают.

Несколько примечаний по этому поводу:

Все результаты

Вот таблица со всеми результатами:

.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" означают одно и то же.

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

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

Я попытаюсь подобрать другие определения термина "текущая ветвь"/"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