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

Источник: «How HEAD works in git»
Вроде бы HEAD — это довольно простая тема, но многие разработчики не уверены, что полностью понимают.

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

Я был удивлён, что кто-то так не уверены в своём понимании — думал, что 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 коммита, например:

Все эти вещи (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)

Не совсем понятно, как их интерпретировать, поэтому вот что я расскажу:

Если использовать эти правила для объяснения 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 полностью и использовать другой метод для выяснения того, какая часть кода является таковой.

Мысли о согласованности терминологии

Я думаю, 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