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, пока не спаслись.