Блог 7even

про ruby, rails, sinatra, git и всё на свете

Git Detached HEAD

На неделе с рабочим git-репозиторием случилась занимательная история.

В конце одного рабочего дня случилась обычная и вполне штатная ситуация: коллега запушила изменения в центральный репо, я попытался сделать то же самое, на что git меня справедливо послал куда подальше.

Я всегда стараюсь поддерживать простую историю коммитов, поэтому вместо обычного merge решил сделать rebase своего коммита на коммиты коллеги. И, как стало ясно из последующих событий, видимо совершил свою любимую ошибку - вместо наложения своего коммита поверх чужих, наложил чужие на свой. После этого я запушил результат в центральный репо, собрал вещи и ушел домой.

На следующее утро я пришел в офис и по привычке решил сделать pull перед началом работы, но он почему-то выдал мне коммит далеко не первой свежести, причем с пометкой (forced update). Не помню, зачем, но я снова сделал pull (или fetch) - и каково же было мое удивление, когда второй раз он выдал правильный последний коммит, сделанный мной вчера вечером!

Последующие фетчи и пуллы работали аналогичным образом, через один - то выдается forced update со старым коммитом, то правильный HEAD. Попытки сделать новую локальную ветку, новую локальную копию, даже новый идентичный прежнему репозиторий на сервере - ничего не принесли.

Гугл поведал мне о том, что в git есть такое нехорошее состояние, как detached HEAD - это когда на определенный коммит, или даже целую ветку коммитов, не указывает ничего (как известно, в git ветка является указателем на последний из последовательности коммитов; в данной ситуации на последний из коммитов указывает только HEAD, и только до тех пор, пока мы не сделаем checkout другой ветки). Такая ситуация случается, в частности, при rebase - перемещаемые поверх других коммиты остаются в репозитории в двух местах - в новом, куда их переместил rebase (поверх других коммитов), и в старом, где они были до rebase; и на старое место более не указывает ни один ref (ни HEAD, ни ветки, ни теги).

Такая ситуация обычно не представляет опасности - но только если исходные коммиты до rebase не были запушены в другой репозиторий, используемый другими разработчиками; если это случилось, пушить отrebase-нные коммиты нельзя, во всяком случае если не хочется быстрой смерти от руки коллеги :) Если в общем репозитории появятся 2 версии одного и того же коммита, и каждый разработчик будет работать со своей версией, начнется трэшак.

Так вот, после некоторого изучения ситуации выяснилось, что в серверном репозитории HEAD указывает на последний коммит в master, а тот указывает на правильный последний коммит. Но при этом в .git/refs/heads помимо всех наших веток был еще и файл HEAD, указывающий уже на тот самый коммит, который выдавался fetch-ем через раз с пометкой (forced update).

Не зная внутренностей гита, поначалу я решил, что HEAD там и должен всегда находиться; но потом посмотрел внутренности нескольких других имеющихся репозиториев, и не нашел .git/refs/heads/HEAD ни в одном из них.

После удаления этого файла все чудесным образом заработало. Честно говоря, полное понимание того, что случилось, так до сих пор не пришло; поэтому резервная копия того файла осталась на всякий случай лежать на сервере.

P.S. Пока гуглил, нашел достаточно качественный визуальный справочник по командам git.

Комментарии