Аварийное восстановление
Когда reset, rebase или удаление ветки будто уничтожают работу, эскалируй: git reflog находит сдвинутые верхушки, git fsck --lost-found поднимает оторванные объекты, git cat-file -p их осматривает. git gc вычищает недостижимые объекты. Потеря лишь одна — незакоммиченная работа.
Ты запускаешь git reset --hard HEAD~5, чтобы отменить грязный merge, бросаешь взгляд на экран и понимаешь, что пять нужных коммитов исчезли с ветки — полдня работы, на вид удалённой. Или ты удаляешь ветку, а потом вспоминаешь, что в ней была единственная копия фикса. Инстинкт — паника, но вот успокаивающая правда: git почти никогда на самом деле не уничтожает закоммиченный объект в тот момент, когда ты его «теряешь». Коммит по-прежнему в хранилище объектов, просто на него больше ничто не указывает — и есть спокойный, упорядоченный способ его вернуть.
К концу этого урока у тебя будет лестница аварийного восстановления, по которой можно подниматься по нарастающей — reflog, затем fsck, затем осмотр и восстановление оторванных объектов, — и ты будешь знать единственную настоящую потерю, с которой git не поможет, и дедлайн, который завершает восстановление.
После этого урока ты сможешь восстанавливать на вид потерянные коммиты по нарастающей — git reflog, затем git fsck --lost-found, затем git cat-file -p для осмотра и восстановления оторванного объекта — и объяснять, как git gc задаёт дедлайн, после которого недостижимые объекты действительно исчезают.
Первая ступень: git reflog — почти каждый «потерянный» коммит после reset, rebase или удаления ветки здесь. Reflog — это локальный журнал всех мест, куда указывали HEAD и верхушки твоих веток. reset --hard, испорченный rebase, даже branch -D — все оставляют старую позицию записанной. Найди её и вернись:
git reflog # список недавних позиций HEAD с SHA
# напр. abc123 HEAD@{1}: reset: moving to HEAD~5
git reset --hard abc123 # вернуть ветку к состоянию до resetЭто покрывает подавляющее большинство самонанесённых происшествий. Коммиты никогда не удалялись — сдвинулся лишь ярлык ветки, — а reflog помнит, где он был. Пробуй это раньше всего более экзотического.
Вторая ступень: git fsck --lost-found — найти оторванные объекты, когда даже reflog не помогает. Reflog локален для клона и истекает (по умолчанию ~90 дней для достижимых, ~30 для недостижимых записей); у свежего клона его нет, а git gc может его подрезать. Когда reflog пуст, попроси Git просканировать всю базу объектов в поисках объектов, на которые ничто не указывает:
git fsck --full --unreachable # список всех недостижимых объектов
git fsck --lost-found # записать оторванные коммиты/блобы в .git/lost-found/--lost-found сбрасывает оторванные объекты-коммиты в .git/lost-found/commit/, а блобы — в .git/lost-found/other/, именуя их по SHA. «Оторванный» коммит — это тот, до которого не дотягивается ни ветка, ни тег, ни reflog, — ровно то, чем становится верхушка удалённой ветки. fsck находит его, даже когда никто не помнит его имени.
Третья ступень: git cat-file -p, чтобы осмотреть кандидата, затем восстановить его. fsck даёт тебе список SHA, но без контекста. Распечатай любой объект, чтобы увидеть, тот ли это, что нужен, прежде чем действовать:
git cat-file -t <sha> # какого типа объект? (commit/blob/tree)
git cat-file -p <sha> # распечатать: сообщение, автор, дерево, родительКак только ты опознал нужный оторванный коммит, дай ему имя снова, чтобы он перестал быть недостижимым:
git branch recovered <sha> # заново закрепить потерянный коммит на веткеДля одного потерянного файла (оторванного блоба) git cat-file -p <blob-sha> > recovered.txt пишет его содержимое прямо обратно на диск. Именование объекта — вот что его спасает: объект с ссылкой достижим, а достижимые объекты никогда не вычищаются.
Дедлайн: git gc — вот что наконец, безвозвратно удаляет недостижимые объекты. Восстановление работает, потому что Git держит недостижимые объекты рядом — но не вечно. Сборка мусора (git gc, запускаемая автоматически многими командами) вычищает объекты, которые одновременно недостижимы и старше грейс-периода (по умолчанию две недели). Явный агрессивный запуск пропускает большую часть страховки:
git gc --prune=now # немедленно удалить ВСЕ недостижимые объектыЭто точка невозврата: после gc --prune=now оторванный коммит, который reflog уже забыл, по-настоящему исчез. Поэтому правило восстановления — действуй до gc. И одна потеря, которую git никогда не отменит: изменения, которые ты никогда не коммитил, — неподготовленные или несохранённые правки — вообще не объекты, так что reflog, fsck и cat-file нечего находить. Коммить рано; объект можно восстановить, несохранённую правку — нет.
▸Почему это работает
Почему git вообще может восстановить «удалённый» коммит? Потому что Git адресуется по содержимому и на уровне объектов работает только на добавление. Запись коммита сохраняет неизменяемый объект, ключом которого служит его SHA; операции вроде reset, rebase и удаления ветки лишь двигают ссылки (указатели веток и HEAD), они не стирают объекты. Коммит, на который не указывает ни одна ссылка, становится недостижимым, но физически по-прежнему присутствует в .git/objects. Reflog и fsck — это просто два способа заново обнаружить SHA объекта, потерявшего свой ярлык. Сборка мусора — единственное, что реально удаляет объекты, — вот почему каждая техника восстановления на самом деле гонка против следующего gc.
Удалённая ветка, восстановленная двумя способами.
Разработчик завершает хитрый фикс на hotfix-auth, затем — думая, что он уже слит, — запускает git branch -D hotfix-auth. А он не был слит. На верхушечный коммит теперь не указывает ни одна ссылка.
Лёгкий путь (reflog ещё хранит его). Он запускает git reflog, замечает def456 HEAD@{6}: checkout: moving from hotfix-auth to main, читает SHA удалённой верхушки и запускает git branch hotfix-auth def456. Ветка вернулась, каждый коммит цел, всего ушло меньше минуты. Большинство восстановлений заканчиваются здесь.
Трудный путь (свежий клон, reflog нет). Допустим, это случилось на CI-машине, которая затем переклонировала репо, стерев reflog. Он запускает git fsck --full --no-reflogs --lost-found, который перечисляет несколько оторванных коммитов в .git/lost-found/commit/. Он делает git cat-file -p для каждого кандидата, читая сообщения коммитов, пока не находит «Fix auth token refresh» с нужным автором и датой. Он заново закрепляет его: git branch recovered-auth <sha>, затем проверяет через git log. Работа вернулась — но он отмечает, что запусти кто-то git gc --prune=now раньше, объект был бы вычищен и невосстановим. Спокойный чеклист устоял: reflog, затем fsck, затем cat-file, затем ветка — и никогда не запускай gc в панике.
▸Частая ошибка
Худший ход во время восстановления — «прибраться», запустив git gc --prune=now или git reflog expire --expire=now --all — ровно те команды, что уничтожают объекты, которые ты пытаешься спасти. Под стрессом люди тянутся к уборке, но восстановление хочет обратного: оставь репо в покое, прекрати запускать разрушительные команды и работай только на чтение (reflog, fsck, cat-file все безопасны), пока не закрепишь объект на ветке. Вторая классическая ошибка — переоценить восстановление и коммитить недостаточно часто: reflog и fsck видят только закоммиченные объекты, так что дисциплина, делающая их мощными, — это вообще коммитить свою работу.
Ты запустил `git reset --hard HEAD~3` и потерял три коммита. Reflog ещё работает. Каков правильный первый шаг?
Когда reset, rebase или branch -D будто уничтожают закоммиченную работу, восстанавливай по нарастающей. git reflog — первая ступень: он журналирует каждое место, куда указывали HEAD и твои ветки, так что обычно ты просто reset --hard к SHA до происшествия. Когда reflog пропал (свежий клон, истёк или подрезан), git fsck --lost-found сканирует всё хранилище объектов на оторванные объекты, на которые ничто не ссылается. Используй git cat-file -p, чтобы осмотреть SHA-кандидат, затем git branch <name> <sha>, чтобы заново его закрепить, — именование объекта делает его достижимым и безопасным. Дедлайн — git gc: агрессивный --prune=now безвозвратно удаляет недостижимые объекты, поэтому всегда восстанавливай до gc и никогда не запускай команды уборки в панике. Единственная потеря, которую git не отменит, — работа, которую ты никогда не коммитил, ведь она никогда не была объектом. На этом весь этот спасательный юнит — и весь трек git — завершён.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.