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