Как HEAD работает в git

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

Привет! На днях я провёл опрос в 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, (HEAD -> main), detached HEAD state, On branch main и т.д.)

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

файл .git/HEAD

В Git есть очень важный файл, называющийся .git/HEAD. Этот файл работает следующим образом: он содержит либо:

  1. Имя ветви (например, ref: refs/heads/main)
  2. Идентификатор коммита (например, 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 одним из двух способов:

  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 state

"HEAD is detached" или "detached HEAD state" означают, что у вас нет текущей ветви.

Отсутствие текущей ветви немного опасно, потому что если вы сделаете новые коммиты, они не будут прикреплены ни к какой ветви — они будут сиротами! Осиротевшие коммиты — это проблема по двум причинам:

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

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

  1. Вернутся к ветви (git checkout main)
  2. Создать новую ветвь на этом коммите (git checkout -b newbranch).
  3. Если вы находитесь в "detached HEAD state", потому что находитесь в середине ребейза, завершите или прервите ребейз (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 state" (у вас нет текущей ветви).

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

  1. commit 96fa6899ea (HEAD -> main) означает:
    • .git/HEAD содержит ref: refs/heads/main
    • .git/refs/heads/main содержит 96fa6899ea
  2. commit 96fa6899ea (HEAD, main) означает:
    • .git/HEAD содержит 96fa6899ea (HEAD is “detached”)
    • .git/refs/heads/main также содержит 96fa6899ea
  3. 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 запутанным, надеюсь, это поможет вам!

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

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

Миграция с MySQL на Postgres с помощью конструктора запросов Laravel

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

Автоматическое перехэширование паролей в Laravel 11