Как HEAD работает в git
Привет! На днях я провёл опрос в Mastodon, в котором спрашивал людей, насколько они уверены в том, что понимают, как работает HEAD в Git. Результаты (из 1700 голосов) немного удивили:
- 10% "100%"
- 36% "довольно уверенно"
- 39% "с некоторой уверенностью"
- 15% "буквально без понятия"
Я был удивлён, что кто-то так не уверены в своём понимании — думал, что HEAD
— это довольно простая тема.
Обычно, когда кто-то говорит, что тема запутанная, а я думаю, что это не так. Причина в том, что есть какая-то скрытая сложность, которую я не учёл. И после нескольких последующих разговоров выяснилось, что HEAD
на самом деле немного сложнее, чем я предполагал!
На самом деле HEAD
— это несколько разных вещей
Поговорив с множеством разных людей о HEAD
, я понял, что на самом деле HEAD
имеет несколько разных, тесно связанных между собой значений:
- Файл
.git/HEAD
HEAD
, в качествеgit show HEAD
(git называет это "параметром ревизии")- Все способы, которыми git использует
HEAD
в выводе различных команд (<<<<<<<<<<HEAD
,(HEAD -> main)
,detached HEAD state
,On branch main
и т.д.)
Они очень тесно связаны друг с другом, но я не думаю, что эта связь полностью очевидна для тех, кто только начинает работать с git.
файл .git/HEAD
В Git есть очень важный файл, называющийся .git/HEAD
. Этот файл работает следующим образом: он содержит либо:
- Имя ветви (например,
ref: refs/heads/main
) - Идентификатор коммита (например,
96fa6899ea34697257e84865fefc56beb42d6390
)
Этот файл определяет, какой является ваша "текущая ветвь" в Git. Например, когда вы запускаете git status
и видите следующее:
$ git status
On branch main
это означает, что файл .git/HEAD
содержит ссылку: refs/heads/main
.
Если .git/HEAD
содержит идентификатор коммита, а не ветви, git называет это "detached HEAD state"/"отсоединённое состояние HEAD". К этому мы вернёмся позже.
Иногда говорят, что HEAD содержит имя ссылки или ID коммита, но я почти уверен, что ссылка должна быть ветвью. Технически вы можете заставить
.git/HEAD
содержать имя ссылки, которая не является ветвью, вручную отредактировав.git/HEAD
, но не думаю, что это можно сделать с помощью обычной команды git. Было бы интересно узнать, существует ли способ сделать.git/HEAD
ссылкой не на ветвь с помощью обычной git-команды, и если да, то зачем вам это нужно!
HEAD
, в качестве git show HEAD
Очень часто в командах git используется HEAD
для обозначения ID коммита, например:
git diff HEAD
git rebase -i HEAD^^^^
git diff main..HEAD
git reset --hard HEAD@{2}
Все эти вещи (HEAD
, HEAD^^^
, HEAD@{2}
) называются "параметрами ревизии". Они документированы в man gitrevisions
, и Git будет пытаться преобразовать их в ID коммита.
Честно говоря, я никогда раньше не слышал термина "параметр ревизии", но именно этот термин поможет найти документацию по этой концепции
HEAD
, в качестве git show HEAD
имеет довольно простое значение: он разрешается в текущий коммит, который вы проверили! Git разрешает HEAD
одним из двух способов:
- Если
.git/HEAD
содержит имя ветви, то это будет последний коммит в этой ветви (например, прочитанный из.git/refs/heads/main
). - если
.git/HEAD
содержит ID коммита, это будет ID этого коммита
Далее: все форматы сообщений
Теперь мы поговорили о файле .git/HEAD
и "параметре ревизии" HEAD
, как в git show HEAD
. Осталось разобраться со всеми различными способами, которыми git использует HEAD
в своих сообщениях.
git status
: "on branch main" или "HEAD detached"
Когда вы запускаете git status
, первая строка всегда будет выглядеть как одна из этих двух:
on branch main
. Это означает, что.git/HEAD
содержит ветвь.HEAD detached at 90c81c72
. Это означает, что.git/HEAD
содержит ID коммита.
Ранее я обещал объяснить, что значит "HEAD detached", так что давайте сделаем это сейчас.
detached HEAD state
"HEAD is detached" или "detached HEAD state" означают, что у вас нет текущей ветви.
Отсутствие текущей ветви немного опасно, потому что если вы сделаете новые коммиты, они не будут прикреплены ни к какой ветви — они будут сиротами! Осиротевшие коммиты — это проблема по двум причинам:
- коммиты сложнее найти (вы не можете запустить
git log somebranch
, чтобы найти их) - осиротевшие коммиты со временем будут удалены сборщиком мусора git'а
Я тщательно избегаю создания коммитов в "detached HEAD state", хотя некоторые предпочитают работать именно так. Выйти из "detached HEAD state" довольно просто, вы можете либо:
- Вернутся к ветви (
git checkout main
) - Создать новую ветвь на этом коммите (
git checkout -b newbranch
). - Если вы находитесь в "detached HEAD state", потому что находитесь в середине ребейза, завершите или прервите ребейз (
git rebase --abort
).
Итак, вернёмся к другим командам git, которые содержат HEAD
в своих сообщениях!
git log
: (HEAD -> main)
Когда запускаете git log
и смотрите на первую строку, то можете увидеть одну из следующих трёх вещей:
commit 96fa6899ea (HEAD -> main)
commit 96fa6899ea (HEAD, main)
commit 96fa6899ea (HEAD)
Не совсем понятно, как их интерпретировать, поэтому вот что я расскажу:
- Внутри
(...)
, git перечисляет все ссылки, которые указывают на этот коммит, например(HEAD -> main, origin/main, origin/HEAD)
означает, чтоHEAD
,main
,origin/main
иorigin/HEAD
все указывают на этот коммит (прямо или косвенно). HEAD -> main
означает, что ваша текущая ветвь —main
.- Если в этой строке написано
HEAD
, а неHEAD ->
, это означает, что вы находитесь в "detached HEAD state" (у вас нет текущей ветви).
Если использовать эти правила для объяснения 3 примеров, приведённых выше, то получится следующее:
commit 96fa6899ea (HEAD -> main)
означает:.git/HEAD
содержитref: refs/heads/main
.git/refs/heads/main
содержит96fa6899ea
commit 96fa6899ea (HEAD, main)
означает:.git/HEAD
содержит96fa6899ea (HEAD is “detached”)
.git/refs/heads/main
также содержит96fa6899ea
commit 96fa6899ea (HEAD)
означает:.git/HEAD
содержит96fa6899ea (HEAD is “detached”)
.git/refs/heads/main
либо содержит другой ID коммита, либо не существует
Конфликты слияния: <<<<<<< HEAD
просто запутывает.
Когда вы разрешаете конфликт слияния, то можете увидеть что-то вроде этого:
<<<<<<< HEAD
def parse(input):
return input.split("\n")
=======
def parse(text):
return text.split("\n\n")
>>>>>>> somebranch
Я нахожу HEAD
в этом контексте чрезвычайно запутанным и в основном просто игнорирую его. Вот почему.
- Когда вы выполняете слияние,
HEAD
в конфликте слияния будет тем же, чем былHEAD
, когда вы запускалиgit merge
. Просто. - Когда вы делаете ребейз,
HEAD
в конфликте слияния — это нечто совершенно иное: это другой коммит, поверх которого вы делаете ребейз. Так что это совершенно не то, чем былHEAD
, когда вы запускалиgit rebase
. Так происходит потому, что ребейз работает, сначала проверяя другой коммит, а затем многократно выбирая (cherry-pick
) коммиты поверх него.
Аналогично, при слиянии и ребейзе меняются местами значения слов "наш" и "их".
Тот факт, что значение HEAD
меняется в зависимости от того, делаю ли я ребейз или слияние, слишком запутан для меня, и я нахожу, что гораздо проще просто игнорировать HEAD
полностью и использовать другой метод для выяснения того, какая часть кода является таковой.
Мысли о согласованности терминологии
Я думаю, HEAD был бы более интуитивным, если бы терминология git'а, связанная с HEAD, была более внутренне последовательной.
Например, git говорит об "detached HEAD state", но никогда о "attached HEAD state"/"присоединённом состоянии HEAD" — документация git вообще никогда не использует термин "attached"/"присоединён" для обозначения HEAD
. И git говорит о нахождении "на" ветви, но никогда не говорит о "не на" ветви.
Поэтому очень трудно догадаться, что on branch main
на самом деле противоположно HEAD detached
. Как пользователь должен догадаться, что HEAD detached
вообще имеет отношение к ветвям, или что "on branch main" имеет отношение к HEAD
?
Вот и всё
Если я вспомню о других способах использования HEAD
в Git'е (особенно о том, как HEAD появляется в выводе Git'а), я могу добавить их в эту статью позже.
Если вы находите HEAD запутанным, надеюсь, это поможет вам!