[Перевод] Как в git работает HEAD

ebb5f1afe9d587006ba0ad98ae4bd1bb.png

Недавно я провела в Mastodon опрос о том, насколько мои читатели уверены в том, что они хорошо понимают работу HEAD в Git. Результаты (на основании примерно 1700 голосов) меня немного удивили:

  • 10% — 100%

  • 36% — достаточно сильно уверен

  • 39% — уверен в некоторой степени

  • 15% — представления не имею

Меня удивило, что люди не уверены в своём понимании: я-то считала, что HEAD — это довольно простая тема.

Обычно, когда остальные, в отличие от меня, считают какую-то тему запутанной, причина заключается в какой-то скрытой сложности, которую я не учитываю. И в дальнейших обсуждениях выяснилось, что HEAD действительно чуть сложнее, чем я считала!

На самом деле HEAD имеет несколько значений

Поговорив с кучей людей о HEAD, я осознала, что HEAD на самом деле имеет несколько тесно связанных значений:

  1. Файл .git/HEAD

  2. HEAD в git show HEAD (git называет это «параметром ревизии»)

  3. Все способы, которыми git использует HEAD в выводе различных команд (<<<<<<<<<,  (HEAD -> main),  detached HEAD state,  On branch main и так далее)

Все эти темы крайне тесно связаны друг с другом, но я не думаю, что эта связь очевидна тем, кто только начинает работать с git.

Файл .git/HEAD

В Git есть очень важный файл .git/HEAD. Он может содержать что-то из перечисленного ниже:

  1. Имя ветви (например, ref: refs/heads/main)

  2. 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 одним из двух способов:

  1. Если .git/HEAD содержит имя ветви, он будет последним коммитом в этой ветви (например, при чтении его из .git/refs/heads/main)

  2. Если .git/HEAD содержит ID коммита, это будет ID коммита

Все форматы вывода

Мы рассмотрели файл .git/HEAD и параметр ревизии HEAD, используемый в git show HEAD. Осталось рассмотреть все способы, которыми git использует HEAD в выводе.

git status: «on branch main» или «HEAD detached»

При выполнении git status первая строка будет выглядеть как один из двух вариантов:

  1. on branch main. Это значит, что .git/HEAD содержит ветвь.

  2. HEAD detached at 90c81c72. Это значит, что .git/HEAD содержит ID коммита.

Выше я обещала, что объясню значение «HEAD detached», так что давайте разбираться.

Состояние detached HEAD

«HEAD is detached» или «detached HEAD state» означает, что отсутствует текущая ветвь.

Отсутствие текущей ветви может быть немного опасно, потому что при внесении новых коммитов эти коммиты не будут прикрепляться ни к одной из ветвей — они будут «сиротами»! Коммиты-сироты могут вызвать следующие проблемы:

  1. коммиты будет сложнее найти (нельзя выполнить git log somebranch, чтобы найти их)

  2. коммиты-сироты со временем будут удалены сборщиком мусора git

Лично я очень тщательно стараюсь избегать коммитов в состоянии detached HEAD, хотя некоторые люди предпочитают работать именно так. Выйти из состояния detached HEAD достаточно просто, можно выполнить одно из следующих действий:

  1. Вернуться в ветвь (git checkout main)

  2. Создать новую ветвь в этом коммите (git checkout -b newbranch)

  3. Если вы находитесь в состоянии detached HEAD, потому что идёт процесс rebase, завершите или прекратите rebase (git rebase --abort)

А теперь вернёмся к другим командам git, имеющим в выводе HEAD.

git log:  (HEAD → main)

Выполнив git log и посмотрев на первую строку, можно увидеть один из следующих трёх вариантов:

  1. commit 96fa6899ea (HEAD -> main)

  2. commit 96fa6899ea (HEAD, main)

  3. commit 96fa6899ea (HEAD)

Их интерпретация не совсем очевидна, так что разберём их:

  • внутри (...) git перечисляет все ссылки, которые ссылаются на этот коммит, например, (HEAD -> main, origin/main, origin/HEAD)  означает, что HEAD,  main,  origin/main и origin/HEAD указывают на этот коммит (прямо или косвенно)

  • HEAD -> main  означает, что текущая ветвь — это main

  • Если в этой строке написано HEAD,, а не HEAD ->, это значит, что мы находимся с состоянии detached HEAD (текущая ветвь отсутствует)

Если мы используем эти правила для объяснения трёх приведённых выше примеров, то результат будет таким:

  1. commit 96fa6899ea (HEAD -> main) означает:

  2. commit 96fa6899ea (HEAD, main) означает:

  3. 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 сбивает вас с толку, то надеюсь, что моя статья немного вам помогла!

© Habrahabr.ru