← Вернуться в блог

Восстановление потерянных commit через git reflog (скорее всего, вы ничего не потеряли)

tutorial git

«Я всё потерял» (спойлер: скорее всего, нет)

Выдохните. Если пропавшая работа хоть раз была закоммичена -- пусть даже один раз, пусть даже в не той ветке, пусть даже в коммите, который вы потом «удалили», -- она почти наверняка прямо сейчас лежит в вашем репозитории. Git гораздо лучше умеет хранить, чем терять, и ближайшие десять минут чтения, скорее всего, закончатся тем, что ваш код снова будет на экране.

Вот успокаивающая правда о том, как Git устроен внутри: Git почти никогда не удаляет ничего сразу. Каждый ваш коммит хранится как объект в каталоге .git и идентифицируется своим SHA. Когда вы выполняете git reset --hard, удаляете ветку или переписываете историю через rebase, Git не стирает эти объекты коммитов. Он лишь перемещает указатели -- имена веток и HEAD, -- чтобы те больше на них не указывали. Коммиты становятся «недостижимыми»: к ним больше не ведёт ни одна ветка и ни один тег, но сами объекты остаются на диске неделями, прежде чем сборщик мусора вообще задумается их трогать.

Так что проблема не в том, что ваши коммиты пропали. Проблема в том, что у вас больше нет для них имени. Вам нужен SHA. И Git ведёт журнал, который всё это время тихо записывал каждый SHA, через который проходил ваш репозиторий: reflog.

Что такое reflog на самом деле

Reflog (reference log) -- это локальный, привязанный к конкретной машине журнал того, куда со временем указывали HEAD и вершина каждой ветки. Каждый раз, когда HEAD перемещается -- из-за commit, checkout, merge, rebase, reset или amend, -- Git добавляет строку в этот журнал. Это чёрный ящик вашего репозитория.

Чтобы его увидеть, выполните:

git reflog

Вывод выглядит так:

e4f5a6b HEAD@{0}: reset: moving to HEAD~3
a1b2c3d HEAD@{1}: commit: add payment validation
9f8e7d6 HEAD@{2}: commit: refactor checkout flow
3c4d5e6 HEAD@{3}: checkout: moving from main to feature/payments

Читайте сверху вниз: «самое свежее -- первым». Каждая строка показывает SHA, на который указывал HEAD, позиционную ссылку вида HEAD@{1}, операцию, переместившую HEAD, и краткое описание. Синтаксис HEAD@{n} означает «где HEAD был n перемещений назад»: HEAD@{0} -- это где вы сейчас, HEAD@{1} -- на одну операцию раньше, и так далее. Эти ссылки можно использовать в любой команде, принимающей коммит, -- именно поэтому спасательные операции получаются такими прямолинейными.

В примере выше история ясна: вы сделали два коммита (HEAD@{2} и HEAD@{1}), затем reset переместил HEAD на три коммита назад. Эти два коммита не потеряны -- вот же они, в a1b2c3d.

HEAD -- не единственная ссылка с журналом. У каждой ветки есть свой:

git reflog show feature/payments

Эта команда показывает все позиции, которые занимала вершина feature/payments, -- удобно, когда нужна история одной ветки без шума от каждого checkout, который вы когда-либо делали.

Сценарии спасения, шаг за шагом

1. Вы зашли слишком далеко с git reset --hard

Вы хотели отменить один коммит, а снесли три, или сбросились совсем не туда. Reflog записал, где вы были непосредственно перед reset, и эта позиция -- HEAD@{1}:

git reset --hard HEAD@{1}

Это и есть всё исправление. Сам reset был лишь очередным перемещением HEAD, поэтому возврат HEAD в предыдущую позицию восстанавливает всё. Если после неудачного reset вы успели сделать что-то ещё, сначала выполните git reflog, найдите строку сразу перед записью о reset и сбросьтесь на её SHA. Более широкий обзор стратегий отмены -- в статье как отменить коммит в Git.

2. Вы удалили ветку

Удаление ветки удаляет указатель, а не коммиты. Если на эту ветку недавно переключались или коммитили в неё на этой машине, её вершина есть в reflog HEAD. Найдите её:

git reflog | grep "feature/payments"

Ищите последний коммит, сделанный в этой ветке, или последнюю запись checkout: moving from feature/payments -- SHA в этой строке и есть вершина ветки. Затем создайте ветку заново, указав её на этот SHA:

git branch feature/payments a1b2c3d

Ветка вернулась -- ровно такая, какой была в момент удаления. Бонус: при удалении ветки Git печатает SHA её вершины (Deleted branch feature/payments (was a1b2c3d)) -- если это сообщение ещё видно в истории терминала, поиск по reflog можно вообще пропустить.

3. Rebase пошёл не так

Rebase переписывает коммиты, и посреди rebase, утыканного конфликтами, может казаться, что ветка изуродована безвозвратно. Это не так: состояние до rebase лежит в reflog. Если rebase ещё идёт, самый чистый выход -- git rebase --abort. Если он уже завершился, а результат вам ненавистен, найдите запись перед его началом:

git reflog
# ищите "rebase (start)" -- запись СРАЗУ ПОД ней
# и есть позиция вашей ветки до rebase
git reset --hard HEAD@{5}

(Замените {5} на ту позицию, которую запись «до rebase» занимает в вашем reflog.) Ваша ветка ровно такая, какой была. Если rebase регулярно загоняет вас в эту ситуацию, наш гид интерактивный rebase без страха рассказывает, как превратить его из риска в рутину.

4. Коммит исчез после --amend

git commit --amend не редактирует коммит -- он создаёт новый и перемещает на него ветку. Оригинал недостижим, но цел, и сразу после amend он находится в HEAD@{1}. Чтобы его посмотреть:

git show HEAD@{1}

Если вы изменили не тот коммит или вам нужен оригинал, сбросьтесь на него или перенесите его через cherry-pick туда, где ему место.

5. Вы закоммитили в detached HEAD, а потом переключились

Вы сделали checkout конкретного коммита или тега, наделали там коммитов в состоянии detached HEAD, а затем переключились на ветку -- и ваши коммиты будто испарились. Git даже предупреждает об этом при переключении и печатает осиротевший SHA. Но в reflog он есть в любом случае:

git reflog
# найдите последний свой коммит перед записью
# "checkout: moving from <sha> to <branch>"
git branch rescued-work e4f5a6b

Теперь ваши «отвязанные» коммиты живут в настоящей ветке, и через merge или rebase их можно отправить туда, куда нужно.

Сначала посмотрите, потом восстанавливайте

Прежде чем направлять что-либо на восстановленный SHA, взгляните на него. Убедитесь, что это именно тот коммит, о котором вы думаете:

git show a1b2c3d

Команда печатает сообщение коммита, автора, дату и полный diff. Если хотите листать reflog с полными деталями коммитов вместо однострочных записей, используйте reflog-режим лога:

git log -g

А вот самая безопасная привычка из всех: вместо того чтобы сбрасывать текущую ветку на восстановленный коммит, сначала создайте на нём временную ветку:

git branch rescue a1b2c3d

Теперь у восстановленной работы есть постоянное, достижимое имя. Ваша текущая ветка не изменилась ни на йоту, сборщик мусора никогда не тронет спасённые коммиты, а вы можете спокойно смотреть, сравнивать и сливать. Ветка ничего не стоит; создавайте её всегда, когда не уверены на 100%.

Ограничения, честно

Reflog -- замечательная страховка, но у него есть реальные границы, и о них лучше знать до того, как они понадобятся:

  • Он строго локален. Reflog живёт на вашей машине и никогда не передаётся при push, pull или clone. Свежий клон начинается с пустого reflog. Если вы потеряли коммиты на ноутбуке, важен именно reflog на этом ноутбуке -- клон коллеги не поможет, как и копия на сервере.
  • Записи истекают. По умолчанию записи reflog для коммитов, всё ещё достижимых из ветки, истекают через 90 дней, а для недостижимых -- через 30 дней. После истечения сборщик мусора может удалить недостижимые объекты уже по-настоящему. На практике этого времени более чем достаточно -- но это значит, что reflog спасает прошломесячную ошибку, а не прошлогоднюю.
  • Он не восстановит то, что никогда не коммитилось. Незакоммиченные изменения, уничтоженные git reset --hard или git checkout -- <file>, никогда не были объектами Git, поэтому ни один журнал их не записал. Единственное частичное исключение: файлы, которые хотя бы попали в staging через git add, существуют как blob-объекты, и git fsck --lost-found может вытащить эти висячие blob в .git/lost-found/ -- содержимое без имён файлов и без истории, но лучше, чем ничего. Это крайняя мера, а не рабочий процесс.
  • У удалённых stash есть свой запасной выход. Stash, по которому вы сделали drop или clear, не попадает в reflog HEAD, но коммиты stash часто выживают как висячие объекты. git fsck --unreachable | grep commit плюс git show по кандидатам помогут их найти. Если вы активно пользуетесь stash, наш гид по git stash описывает более безопасные сценарии работы.

Профилактика: сделайте будущие паники скучными

У каждого сценария выше было одно условие: работа была закоммичена. Отсюда единственный совет по профилактике, который имеет значение: коммитьте рано и коммитьте часто. Маленькие, черновые, work-in-progress коммиты ничего не стоят -- позже их можно объединить через squash, переформулировать или переставить. Как только что-то закоммичено, оно лежит в базе объектов, и reflog его прикрывает. Пока нет -- вы полагаетесь на удачу.

Для быстрых переключений контекста, когда коммит кажется слишком тяжёлым, используйте stash, вместо того чтобы таскать незакоммиченные изменения между checkout, -- stash тоже настоящий объект, который Git сможет найти снова.

Где здесь GitSquid (и где выигрывает CLI)

Честный ответ: reflog -- одно из тех мест, где правильный инструмент -- командная строка. В GitSquid нет браузера reflog, и когда вам нужен git reflog, откройте терминал и используйте его -- команды из этой статьи и есть путь восстановления.

Вместо этого GitSquid делает безопаснее окружающий рабочий процесс, чтобы вы тянулись к reflog реже. Таймлайн файла (кликните правой кнопкой по любому файлу и выберите Show history) даёт визуальную историю по каждому файлу, и это часто отвечает на вопрос «куда делся тот код?» вообще без всякого восстановления. Разрушительные операции вроде reset и удаления ветки выполняются через явные контекстные меню на графе коммитов, так что вы видите, что именно собираетесь сделать, до того как сделаете. А когда вы восстанавливаете картину «что только что произошло», журнал команд (Cmd/Ctrl+Shift+L) показывает каждую Git-команду, которую GitSquid выполнил в вашем репозитории, с полными аргументами и кодами выхода -- ровно те улики, которые хочется иметь рядом с reflog, когда разбираешь инцидент по кусочкам.

Краткая шпаргалка

Ситуация Команда
Посмотреть, где побывал HEAD git reflog
Посмотреть, где побывала вершина ветки git reflog show <branch>
Отменить неудачный reset --hard git reset --hard HEAD@{1}
Восстановить удалённую ветку git branch <name> <sha>
Отменить завершившийся rebase git reset --hard HEAD@{n} (запись до rebase)
Посмотреть восстановленный коммит git show <sha>
Листать reflog с полными деталями git log -g
Безопасно припарковать спасённое git branch rescue <sha>
Крайняя мера для файлов из staging, которые так и не были закоммичены git fsck --lost-found

Reflog превращает «я всё потерял» в «я на десять минут потерял указатель». Коммитьте часто, заглядывайте в reflog прежде чем паниковать и паркуйте восстановленное на временной ветке, прежде чем трогать что-либо ещё. А если хотите Git-клиент, который делает разрушительные операции явными и ведёт полный журнал каждой выполненной команды, скачайте GitSquid бесплатно -- он не заменит ваш reflog, но поможет нуждаться в нём реже.