Сценарий, который ловит каждого. Ты сделал git reset --hard, и три часа работы испарились. Или удалил ветку git branch -D feature, а она была не смержена. Или git checkout затёр незакоммиченную правку в файле. Желудок проваливается, в голове крутится "всё пропало". Так вот: в 90 процентах случаев ничего не пропало. Git - это база контентно-адресуемых объектов, и при обычных операциях он не стирает данные сразу. Он лишь перестаёт на них ссылаться. Физически коммит, дерево и блоб лежат в .git/objects ещё две недели (по умолчанию), пока их не подберёт сборщик мусора.
В прошлых уроках мы разбирали reflog - журнал, где Git помнит, куда указывал HEAD и каждая ветка в последние 90 дней. Reflog - первый инструмент спасателя, и в большинстве случаев его хватает. Но бывает, что reflog не помогает: запись истекла, ты на другой машине, reflog отключён в bare-репозитории, или коммит вообще никогда не был в HEAD (например, он создан внутри прерванного rebase). Вот тогда в дело идёт тяжёлая артиллерия - git fsck, dangling-объекты и прямое чтение базы. Этот урок про то, как восстановить коммит git, как восстановить удаленную ветку git и как вытащить любой файл из любого коммита, даже когда reflog молчит. И главное - в каком порядке это делать, чтобы не сделать хуже.

Где Git физически держит мусор до сборки
Чтобы спасать осознанно, надо понимать, куда смотреть. Любой коммит, дерево или блоб - это объект с именем-хешем. Пока на объект кто-то ссылается (ветка, тег, HEAD, reflog, индекс), он "живой". Как только последняя ссылка пропала, объект становится unreachable (недостижимым). Если на него вдобавок не ссылается ни один другой объект - он dangling (висячий). Висячий коммит - типичный итог reset, оборванной ветки или прерванного rebase: верхушка цепочки, на которую больше ничто не указывает.
Недостижимые объекты не исчезают мгновенно. Раньше они валялись россыпью loose-файлов в .git/objects/xx/. С Git 2.5x обслуживание поумнело: одиночные объекты-сироты собираются в cruft pack - отдельный пакет с файлом .mtimes, где у каждого объекта записано время последнего касания. Это аккуратнее, чем тысячи мелких файлов, и позволяет точно отсчитывать "возраст" мусора. Удаляет его только сборка - и тут важная развилка. С Git 2.54 дефолтом ручного обслуживания стала geometric-стратегия (git maintenance), а классический git gc - легаси-путь. Но суть для спасателя одна:
Сроки по умолчанию: недостижимое с reflog-следом держится 90 дней (gc.reflogExpire), совсем недостижимое без ссылок - 14 дней (gc.pruneExpire). Это твоё окно.Пока ты не запустил git gc или git maintenance run с прунингом - твои потерянные коммиты ещё в репозитории. Первое правило спасателя: не запускай сборку мусора, пока не вытащил всё нужное.
git fsck: рентген объектной базы
git fsck (file system check) проверяет связность и целостность всех объектов. Для спасения нас интересует не проверка, а его побочный талант - он показывает то, на что больше никто не ссылается. Запускаем так:
Код: Выделить всё
$ git fsck --full --no-reflogs
Checking object directories: 100% (256/256), done.
Checking objects: 100% (1843/1843), done.
dangling commit 9f3c1a7b8e2d4c6f0a1b2c3d4e5f60718293a4b5
dangling blob 1d8e7f6a5b4c3d2e1f0091a2b3c4d5e6f7081920
dangling tree c4b5a6978d0e1f2a3b4c5d6e7f80910a1b2c3d4e
Когда висячих коммитов много, удобнее, чтобы Git сам разложил их по полочкам - для этого есть git fsck --lost-found. Это и есть классический git lost found из старых мануалов:
Код: Выделить всё
$ git fsck --lost-found
dangling commit 9f3c1a7b8e2d4c6f0a1b2c3d4e5f60718293a4b5
dangling blob 1d8e7f6a5b4c3d2e1f0091a2b3c4d5e6f7081920
$ ls .git/lost-found/commit/
9f3c1a7b8e2d4c6f0a1b2c3d4e5f60718293a4b5
$ ls .git/lost-found/other/
1d8e7f6a5b4c3d2e1f0091a2b3c4d5e6f7081920
Опознаём нужный объект: cat-file и show
fsck выдал десяток хешей - который твой? Не угадывай, читай. Любой объект можно вскрыть напрямую. git cat-file -p печатает содержимое объекта (-p значит pretty - распарсить по типу), git cat-file -t говорит тип:
Код: Выделить всё
$ git cat-file -t 9f3c1a7
commit
$ git cat-file -p 9f3c1a7
tree 8a2b...e1f
parent 4d5e6f7...
author Anton <a@cyberlake.ru> 1749900000 +0300
committer Anton <a@cyberlake.ru> 1749900000 +0300
feat: парсер конфигов, который я случайно снёс ресетом
Код: Выделить всё
$ git show 9f3c1a7:src/config/parser.php
<?php
namespace App\Config;
final class Parser { /* ... тот самый код ... */ }
Восстановление удалённой ветки
Снёс ветку: git branch -D feature. Ветка в Git - это просто файл в .git/refs/heads/ с одним хешем верхушки. Удалив ветку, ты убрал ссылку, но цепочка коммитов цела. Найти верхушку - две дороги.
Дорога первая, быстрая: reflog ветки HEAD ещё помнит, на чём ты стоял. Команда git reflog show ничего не даст по удалённой ветке (её reflog тоже снесён), а вот общий журнал HEAD - да:
Код: Выделить всё
$ git reflog
a1b2c3d (HEAD -> main) HEAD@{0}: checkout: moving from feature to main
9f3c1a7 HEAD@{1}: commit: feat: парсер конфигов
4d5e6f7 HEAD@{2}: checkout: moving from main to feature
Код: Выделить всё
$ git branch feature 9f3c1a7
$ git log --oneline -1 feature
9f3c1a7 feat: парсер конфигов
Восстановление файла: удалённого и закоммиченного
Случай мягче: файл цел в истории, но ты его удалил и закоммитил удаление, либо затёр локально. Тут не нужен fsck - файл достижим, ищем его в нужном коммите.
Современный путь - git restore (стабилен с Git 2.51, учим его вместо checkout). Восстановить файл из конкретного коммита в рабочее дерево:
Код: Выделить всё
$ git restore --source=HEAD~1 -- src/config/parser.php
Код: Выделить всё
$ git log --oneline -- src/config/parser.php
7e8f9a0 chore: убрал старый парсер
9f3c1a7 feat: парсер конфигов
$ git restore --source=9f3c1a7 -- src/config/parser.php
Отдельная боль - восстановление из stash. Сделал git stash, потом git stash drop или git stash clear - и заначка исчезла из списка. Но коммиты stash - тоже объекты, и они становятся dangling:
Код: Выделить всё
$ git fsck --no-reflogs | grep commit
dangling commit b7c8d9e0f1a2b3c4d5e6f7081920a3b4c5d6e7f8
$ git stash apply b7c8d9e0
Восстановление после reset, rebase, checkout
Три классических способа "потерять" работу - и все обратимы.
После git reset --hard. Ресет двигает ветку и затирает рабочее дерево, но старая верхушка остаётся в reflog:
Код: Выделить всё
$ git reset --hard HEAD~3 # ой, снёс три коммита
$ git reflog
d4e5f60 HEAD@{0}: reset: moving to HEAD~3
a1b2c3d HEAD@{1}: commit: третий коммит, который мне нужен
$ git reset --hard a1b2c3d # вернулись точно как было
После checkout/switch, затёршего файл. Если незакоммиченную правку затёрло переключением - её, увы, не было в объектной базе, reflog не спасёт. Но если ты делал git add перед этим, содержимое осело блобом в индексе и всплывёт в git fsck --lost-found как dangling blob. Поэтому привычка git add перед опасными операциями - дешёвая страховка.
Восстановление целого репозитория
Худший случай: снёс или повредил .git целиком. Git распределённый - почти наверняка полная история есть у коллеги или на сервере. Клонируй заново (git clone) - получишь все коммиты. Если уникальные локальные коммиты были только у тебя и .git частично жив, иногда вытягивает git reflog по веткам плюс fsck по уцелевшим объектам. Если .git мёртв, а рабочее дерево цело - это уже не Git-история, а просто файлы: делай git init, main вручную (init.defaultBranch main или git branch -m main - в ванильном Git на 2026 по умолчанию всё ещё создаётся master с подсказкой), и коммить заново. Полноценную историю в этом случае не вернуть - вот зачем нужен push на remote.
Типичные грабли
- Запустил git gc до спасения - и прунинг снёс висячие объекты навсегда. Сначала вытащи, потом убирайся.
- Забыл --no-reflogs в fsck - реально потерянное прячется за reflog-ссылками и не показывается как dangling.
- Путь в git show <commit>:path указал относительно текущего каталога, а не корня репо - получил "does not exist". Путь всегда от корня.
- Восстанавливаешь файл из коммита-удаления, а не из предыдущего - бери коммит ДО удаления (parent).
- Снёс ветку на удалённом сервере и думаешь, что fsck локально поможет - нет, ищи в своём клоне или в reflog того, кто пушил.
- Перепутал git branch <имя> <SHA> (создаёт) с git branch -f (двигает существующую) - в спасении нужен первый.
Повтори по шагам в тестовом репо, ничем не рискуя:
Код: Выделить всё
mkdir rescue && cd rescue && git init -b main
echo "v1" > app.txt && git add . && git commit -m "first"
echo "ценный код" > app.txt && git add . && git commit -m "valuable"
git log --oneline # запомни хеш второго коммита
git reset --hard HEAD~1 # ПОТЕРЯЛИ valuable
cat app.txt # снова v1, паника
git reflog # видим valuable в HEAD@{1}
git reset --hard HEAD@{1} # ВЕРНУЛИ, проверь cat app.txt
git branch -D main 2>/dev/null; git checkout -b tmp # снесём ветку-имитацию
git fsck --lost-found --no-reflogs # найди dangling commit
git show <SHA-из-вывода>:app.txt # прочти файл из сироты
Чек-лист спасателя
- СТОП. Не запускай git gc, git maintenance run, git prune. Заморозь окно.
- Что потеряно - коммит, ветка, файл, stash? От этого зависит инструмент.
- Сначала git reflog - в большинстве случаев верхушка там. Нашёл - git reset --hard <SHA> или git branch <имя> <SHA>.
- Reflog пуст - git fsck --lost-found --no-reflogs, разбирай dangling commit/blob/tree.
- Опознай объект: git cat-file -t/-p, git show <SHA>. Не угадывай по хешу.
- Достань: ветку - git branch <имя> <SHA>; файл - git restore --source=<SHA> -- path или git show <SHA>:path > file; stash - git stash apply <SHA>.
- Репозиторий целиком - клонируй с remote или у коллеги.
- После спасения - закоммить, запушь, и только потом думай об уборке.
Git проектировался так, чтобы данные было трудно потерять навсегда. Контентно-адресуемая база не стирает объекты при reset, rebase или удалении ветки - она лишь убирает ссылки, а сами объекты ждут в .git/objects недели до сборки мусора. Твоя лестница спасения: reflog как первый и самый частый инструмент, git fsck --lost-found как тяжёлая артиллерия для висячих сирот, git cat-file и git show <commit>:path для опознания и извлечения, git branch <имя> <SHA> для воскрешения ветки, git restore --source для файла. Запомни одно правило крепче остальных: в момент паники не запускай сборку мусора - дай себе время вытащить потерянное. А лучшая страховка от всего - регулярный git push: распределённость работает только когда у истории есть копия.