reflog: машина времени и спасение потерянных коммитов

Сделали reset --hard и думаете, что коммиты пропали навсегда? Почти наверняка нет — есть reflog.

reflog — это локальный журнал, в котором git записывает каждое движение HEAD и веток. Он позволяет вернуться к состоянию, на которое уже не указывает ни одна ветка.

Из прошлых уроков мы знаем: коммиты иммутабельны и живут в базе по своему хешу, а ветки и HEAD — это указатели. Когда «опасная» команда сдвигает указатель, сам коммит никуда не девается — он просто теряет имя. reflog хранит прежние положения указателей, и по нему потерянный коммит легко найти и вернуть.

Зачем это знать на практике

Это самый практичный навык во всём курсе: умение чинить катастрофы. git reset --hard на не том коммите, неудачный rebase, случайно снесённая ветка, коммиты в detached HEAD — всё это выглядит как потеря работы, но почти всегда обратимо за пару команд. Один раз спасённый день работы окупает урок полностью.

Как читать reflog

Каждый раз, когда HEAD меняет положение — коммит, переключение веток, reset, rebase, merge — git дописывает строку в reflog. Посмотрим журнал:

git reflog
# 9a8b7c6 HEAD@{0}: reset: moving to HEAD~2
# 1f2e3d4 HEAD@{1}: commit: Добавить валидацию формы
# 5c6d7e8 HEAD@{2}: commit: Свёрстать форму входа
# 4b6f0d8 HEAD@{3}: checkout: moving from main to feature

Слева — хеш, на который HEAD указывал в тот момент. Запись HEAD@{0} — текущее состояние, HEAD@{1} — предыдущее, и так далее. Главное прозрение: даже если git log уже не показывает коммит 1f2e3d4 (его «срезал» reset), reflog помнит, что он был, и хранит его хеш. А раз есть хеш — есть и сам коммит в базе.

Спасение после reset --hard

Классическая авария. Вы хотели откатить один файл, а сделали git reset --hard HEAD~2 и снесли два часа работы из рабочей директории и истории. Лечится так:

# 1. Находим в журнале коммит ДО reset
git reflog
# ...видим: 1f2e3d4 HEAD@{1}: commit: Добавить валидацию формы

# 2. Возвращаем ветку на него
git reset --hard 1f2e3d4
# или эквивалентно по позиции:
git reset --hard HEAD@{1}

HEAD@{1} читается как «то, на что HEAD указывал один шаг назад». После этого ветка снова смотрит на потерянный коммит, рабочая директория восстановлена. Если не уверены — сначала проверьте безопасно: git switch -c rescue 1f2e3d4 создаст ветку на спасённом коммите, ничего не ломая.

Спасение после неудачного rebase

Интерактивный rebase переписывает коммиты — создаёт новые объекты и переставляет ветку на них (помните: rebase не правит, а пересоздаёт). Если результат вас не устроил, до-rebase-цепочка осталась в базе, и reflog знает её вершину. Удобный приём — фильтр по операции:

git reflog --grep-reflog=rebase   # отфильтровать записи rebase
git reflog                         # или просто весь журнал
# ищем последнюю запись ПЕРЕД "rebase (start)" / "rebase (finish)"
git reset --hard HEAD@{5}          # откат на состояние до rebase

Поскольку у каждой ветки свой reflog, есть и адресный синтаксис: main@{1} — предыдущее положение именно ветки main, а не HEAD. А запись по времени main@{yesterday} или main@{2.hours.ago} вернёт ветку к состоянию на тот момент.

Восстановление удалённой ветки

Удалили ветку через git branch -D feature и поняли, что зря? Её коммиты на месте, нужен лишь их хеш. Если он мелькал в выводе при удалении — используйте его. Иначе ищите в reflog (HEAD проходил через эти коммиты) или загляните в «висячие» коммиты:

git fsck --lost-found
# dangling commit 1f2e3d4...
git switch -c feature 1f2e3d4

Как это работает под капотом

Reflog хранится в файлах .git/logs/HEAD и .git/logs/refs/heads/<ветка> — это обычные текстовые журналы «откуда, куда, кто, когда, действие». Важнейшие факты о нём: reflog локальный (его нет на сервере и он не передаётся при clone/push — это история ваших действий, а не репозитория) и у каждой ветки он свой.

Теперь о сроках. «Потерянные» коммиты живут не вечно — их рано или поздно убирает сборка мусора (git gc). Коммиты, до которых нельзя дойти ни по одной ссылке, называют недостижимыми (unreachable). По умолчанию git даёт фору: недостижимые объекты, на которые ещё ссылается reflog, удаляются не раньше чем через 90 дней (настройка gc.reflogExpire), а совсем «висячие», без всякой ссылки, — через 14 дней (gc.pruneExpire). Поэтому окно для спасения обычно щедрое, но не бесконечное: чините потерю в ближайшие дни, а не месяцы.

Частые ошибки

  • Считать, что reset --hard стирает коммиты. Он двигает указатель; коммиты остаются в базе и достижимы через reflog ещё недели.
  • Искать потерянное на сервере. Reflog локальный: после fresh clone его нет. Восстанавливайтесь на той машине, где случилась авария.
  • Запустить git gc --prune=now в панике. Это как раз и удалит недостижимые объекты немедленно, лишив вас спасательной сети. Сначала найдите коммит, потом убирайтесь.
  • Тянуть месяцами. Окно reflog ограничено (по умолчанию 90/14 дней). Восстанавливайте сразу.

Итоги

  • git reflog — журнал движений HEAD/веток; по нему находят коммиты, до которых уже не дойти через git log.
  • После reset --hard или неудачного rebase: найдите хеш в reflog и сделайте git reset --hard HEAD@{N} или заведите спасательную ветку.
  • Удалённую ветку возвращают через её хеш из reflog или через git fsck --lost-found.
  • Reflog локальный и у каждой ветки свой; на сервер он не уходит.
  • Недостижимые объекты убирает git gc — обычно через 90/14 дней; не запускайте --prune=now, пока не спаслись.
Проверьте себя
1. Вы случайно сделали 'git reset --hard HEAD~2' и потеряли два коммита. Что поможет их вернуть?
AНичего, коммиты удалены безвозвратно
Bgit reflog — найти хеш потерянного коммита и сделать reset --hard на него
Cgit pull с сервера — он восстановит локальную историю
Dgit commit --amend вернёт предыдущее состояние
2. Почему reflog нельзя получить с удалённого сервера после свежего git clone?
AПотому что reflog шифруется и не передаётся по сети
BПотому что reflog локальный — это история действий на конкретной машине, он не пушится и не клонируется
CПотому что сервер хранит reflog только 24 часа
DПотому что reflog есть только у платных аккаунтов GitHub
3. Что произойдёт с недостижимыми (unreachable) коммитами, если запустить 'git gc --prune=now'?
AНичего, gc их не трогает никогда
BОни будут немедленно удалены, и спасательная сеть reflog исчезнет
CОни будут перенесены на сервер
DОни автоматически привяжутся к текущей ветке