Перейти к содержанию
Learning Platform
Глоссарий Troubleshooting
Урок 21.02 · 25 мин
Средний
fsckdanglinglost-foundorphan-commitsunreachablegcfor-each-ref

Восстановление потерянных коммитов: fsck, dangling, lost-found

В уроке 01 мы научились возвращать commits через git reflog. Это работает, пока reflog содержит запись. Что делать, если:

  • Reflog уже gc-нут (старше 90 дней)?
  • Ты случайно удалил .git/logs/ (всё ещё бывает)?
  • Кто-то сделал git reflog expire --expire=now --all (manual cleanup)?
  • Это новый clone репо, где reflog ещё не накопился?

Для этих случаев есть git fsck. Он проверяет физическое состояние objects в .git/objects/, находит orphan commits — те, на которые нет refs, но которые ещё не удалены garbage collector-ом.

В этом уроке:

  1. Что такое dangling и unreachable objects.
  2. git fsck --lost-found — поиск orphans.
  3. Восстановление: git branch <name> <sha>.
  4. Когда fsck не помогает (после gc).
  5. DE-specific сценарии.

Анатомия orphan commits

Сначала — теория. В .git/objects/ хранятся все Git objects: blobs (файлы), trees (директории), commits. Хеш-имена.

.git/objects/
├── ab/
│   └── c1234567890abcdef1234567890abcdef1234  # blob или commit или tree
├── de/
│   └── adbeef1234567890deadbeef1234567890dead
└── pack/
    └── pack-abcdef.pack    # упакованные objects

Когда ты делаешь git commit, Git создаёт новый object с SHA. Ref-ы (refs/heads/main, refs/heads/feature) — это указатели на конкретные commits:

.git/refs/heads/main          # содержит SHA текущего HEAD на main
.git/refs/heads/feat-pipeline # SHA HEAD на feat-pipeline

Reachable object = есть путь от какого-то ref до этого object (через chain of commits и trees). Unreachable = ни один ref не ведёт к этому объекту.

Что делает «unreachable»:

  1. git reset --hard HEAD~5 — старый HEAD-5 коммит становится unreachable (ref сдвинулся).
  2. git branch -D feat/x — все commits, эксклюзивные этой ветки, становятся unreachable.
  3. git rebase — старые pre-rebase commits становятся unreachable.
  4. Force push — old chain на remote становится unreachable.

Unreachable objects живут в .git/objects/ пока не запустится garbage collector (git gc). gc запускается:

  • Автоматически, после некоторых операций (когда loose objects > threshold).
  • Явно — git gc, git gc --aggressive, git gc --prune=now.

Пока не gc-нут — orphan commits можно восстановить через fsck.


git fsck: file system check

git fsck проверяет integrity репо. Опции, релевантные для recovery:

$ git fsck --help
ФлагЧто делает
--fullГлубокая проверка всех objects
--unreachableПоказать все unreachable objects (включая blob, tree)
--no-reflogsИгнорировать reflog при расчёте reachability (treat reflog entries as unreachable)
--lost-foundЗаписать unreachable commits в .git/lost-found/
--danglingПоказать только dangling objects (default)

Dangling vs unreachable — нюансы:

  • Dangling — object, на который никто не указывает: ни ref, ни другой object.
  • Unreachable — object, до которого нельзя дойти от ref-ов.

Все dangling — unreachable, но не наоборот. Unreachable commit может быть «parent unreachable commit-а» — тогда первый не dangling (на него указывает второй), но всё равно unreachable.

Для recovery практически — ищем dangling commits.

Базовый запуск

$ git fsck
Checking object directories: 100% (256/256), done.
Checking objects: 100% (12345/12345), done.
dangling commit abcdef1234567890abcdef1234567890abcdef12
dangling commit fedcba9876543210fedcba9876543210fedcba98
dangling blob 1234567890abcdef1234567890abcdef12345678

Видишь list: dangling commits + blobs. Каждый commit — потенциально потерянная работа. Каждый blob — обычно остался от старой версии файла (не critical для recovery, blob уже извлечён в commit).

Что в каждом dangling commit

$ git show abcdef1234567890abcdef1234567890abcdef12
commit abcdef1234567890abcdef1234567890abcdef12
Author: user <[email protected]m>
Date:   Mon May 12 14:23:00 2026 +0300

    Add validation logic

diff --git a/dags/etl.py b/dags/etl.py
index ...
+def validate_input(df):
+    if df.is_empty():
+        raise ValueError("Empty DataFrame")

Видишь message, author, дату, diff. Это тот самый потерянный commit. Решаешь — нужен он или нет.

Если да:

# Восстанавливаем как ветку
$ git branch recovered-work abcdef1

$ git log recovered-work --oneline
abcdef1 Add validation logic
xyz9876 (older commit)
...

# Теперь можно cherry-pick, merge, или работать дальше
$ git checkout recovered-work

—lost-found: dump orphans в файлы

$ git fsck --lost-found
Checking object directories: 100% (256/256), done.
dangling commit abcdef1234567890abcdef1234567890abcdef12
dangling blob 1234567890abcdef1234567890abcdef12345678

$ ls .git/lost-found/
commit/
other/

$ ls .git/lost-found/commit/
abcdef1234567890abcdef1234567890abcdef12
fedcba9876543210fedcba9876543210fedcba98

$ cat .git/lost-found/commit/abcdef1
# содержимое первого dangling commit (диагностический dump)

Каждый dangling commit записан в .git/lost-found/commit/, blobs — в .git/lost-found/other/.

Это удобно когда много orphans — можно посмотреть какие из них что содержат:

$ for sha in .git/lost-found/commit/*; do
    sha=$(basename $sha)
    echo "=== $sha ==="
    git show --stat $sha | head -10
  done

И выбрать те, которые нужно восстановить.


Сценарий 1: lost work после старого reset

DE сделал git reset --hard HEAD~5 две недели назад, забыл. Сегодня говорит «а где же мой DAG для analytics_users?». Reflog уже потенциально gc-нут.

# Reflog проверка
$ git reflog | head -50
# может быть пусто — старое уже expired

# fsck
$ git fsck --full --unreachable | head -20
unreachable commit 1a2b3c4d...
unreachable commit 5e6f7g8h...
unreachable commit ...

# Один за другим смотрим
$ git show 1a2b3c4d | head
commit 1a2b3c4d
Author: dev
Date:   2 weeks ago
    Add analytics_users DAG

# Нашли. Восстанавливаем:
$ git branch recovered-dag 1a2b3c4d
$ git log recovered-dag --oneline
1a2b3c4d (recovered-dag) Add analytics_users DAG
abc1234 ...

Сценарий 2: lost branch reference

Скрипт случайно сделал rm -rf .git/refs/heads/feat-*. Ветки feat/etl, feat/airflow, feat/snowflake пропали:

$ git branch
* main
# ничего больше нет

# Но objects на диске
$ git fsck --full
dangling commit abcdef1
dangling commit fedcba9
dangling commit deadbeef

Для каждого dangling — git show, смотрим diff, узнаём что это была за ветка, восстанавливаем:

$ git show abcdef1 | head -5
commit abcdef1
Author: dev
Date:   ...
    Refactor S3 sync for performance

# Похоже на feat/snowflake
$ git branch feat/snowflake abcdef1

Повторяем для каждого dangling, который distinguishable.

TIP

В большой DE команде обычно есть runbook для disaster recovery. Один из шагов — «список activeных branches на конкретную дату» (берётся из CI / GitHub UI / monitoring). Это полезно, когда восстанавливаешь много веток — знать имена.


git for-each-ref: что есть прямо сейчас

Для diagnostic — посмотреть полный список refs:

$ git for-each-ref --format='%(refname:short) %(objectname:short) %(committerdate:short)'
HEAD                       abcdef1 2026-05-13
main                       abcdef1 2026-05-13
feat/etl-pipeline          fedcba9 2026-05-12
origin/main                abcdef1 2026-05-13
v1.0.0                     1234567 2026-04-15
ОпцияЧто выведет
%(refname:short)Имя без префикса (main, не refs/heads/main)
%(objectname:short)SHA
%(committerdate:short)Дата последнего commit
%(authorname)Автор
%(subject)Commit message

Удобно когда репо большой:

# Все ветки sorted по дате
$ git for-each-ref --sort=-committerdate refs/heads/ \
    --format='%(refname:short) %(committerdate:short)'

main                  2026-05-13
feat/dbt-refactor     2026-05-12
feat/airflow-upgrade  2026-05-10
old/legacy-stuff      2025-12-01

Полезно для cleanup: вижу old/legacy-stuff от декабря — наверное, можно удалить.


Когда fsck не помогает

git fsck находит только то, что есть на диске. Если уже git gc запустился и удалил orphan commits — fsck их не покажет.

Признаки that gc уже сработал:

$ ls .git/objects/
# почти пусто, всё запаковано

$ ls .git/objects/pack/
pack-abcdef.pack
pack-abcdef.idx

Все objects в pack-файле — gc запустился и оптимизировал. Loose objects (.git/objects/ab/c123...) удалены, если они были unreachable.

В этом случае местные опции исчерпаны. Что осталось:

Восстановление с remote

Если репо был запушен — orphan commits могут быть на remote (если не было force-push). Можно сделать новый clone:

$ cd /tmp
$ git clone --mirror [email protected]:org/repo.git
$ cd repo.git
$ git log --all --oneline | grep "validate_input"
abcdef1 Add validate_input function

Затем перенести нужные коммиты в основной clone (cherry-pick).

Restoring from coworker’s clone

У коллеги local clone содержит тот же commit:

# Коллега
$ git log feat/etl --oneline
abcdef1 Add validate_input
xyz9876 ...

$ git bundle create /tmp/recovery.bundle abcdef1~5..abcdef1

# Отдаёт тебе recovery.bundle (USB / cloud / scp)

# Ты
$ git bundle verify /tmp/recovery.bundle
$ git fetch /tmp/recovery.bundle abcdef1:recovered-work

git bundle — формат экспорта части истории в один файл. Используется для offline transfer.

Backup сервиса (Github Codeshow, GitLab Mirror)

В корпоративных setup-ах часто есть auto-backup всех репо. Tech lead имеет доступ.

Последний случай — потеряно

Если ни локально, ни у коллег, ни на сервере коммита нет — потеряно навсегда. Это редкость, но возможно (force-push в personal репо без backup).

Profilactic mesure: для важных feature-веток — git push origin feat-x каждые несколько часов. Push = backup.


Сценарий 3: восстановление после deleted .git

Самое страшное — rm -rf .git/. Весь репо стёрся:

$ ls -la
total 8
drwxr-xr-x   7 user staff  224 May 13 10:00 .
drwxr-xr-x  10 user staff  320 May 13 10:00 ..
-rw-r--r--   1 user staff  123 May 13 10:00 README.md
-rw-r--r--   1 user staff  456 May 13 10:00 dag.py
# .git нет

Все uncommitted работы — в dag.py, README.md — целы. Истории нет.

Recovery options:

  1. OS-level undelete (rare):

    • macOS Time Machine — если включён.
    • Linux ext4 — extundelete (если не trim-нуто).
    • btrfs/zfs snapshots — если есть.
  2. Re-clone:

    $ git clone <remote> /tmp/recovery
    $ cp -r /tmp/recovery/.git .git

    Файлы локально остаются, история восстановлена с remote. Если есть uncommitted работа — git status покажет diff против remote main.

  3. Coworker bundle (как выше).


DE сценарии recovery

dbt manifest corrupted

Не Git-related, но DE-relevant:

$ dbt run
Error: Failed to parse manifest.json

Удалить и пересобрать:

$ rm -rf target/
$ dbt clean
$ dbt compile

Артефакты регенерируются. Это не recovery от Gittarget/ обычно в .gitignore.

Airflow DAG history

Если потерял старую версию DAG-файла:

$ git log --all -p -- dags/etl_users.py | less
# покажет diff каждого изменения этого файла

$ git log --all --oneline -- dags/etl_users.py
abc1234 Update etl_users
fedcba9 Add validation
9876543 Initial DAG

# Достаём старую версию
$ git show fedcba9:dags/etl_users.py > /tmp/old_dag.py

Это работает даже если файл сейчас удалён — Git помнит всю историю всех файлов (если они когда-то были закоммичены).

Lost stash

git stash создаёт записи в refs/stash. git stash drop удаляет. Если случайно drop-нул:

$ git fsck --unreachable | grep commit
unreachable commit abcdef1
unreachable commit fedcba9

$ git show fedcba9
# проверь, выглядит ли это как stash (заголовок "WIP on branch...")

$ git stash apply fedcba9

Stash commit имеет 3 parents (HEAD, index, untracked), unusual structure. git show его опознаваем.


Профилактика: regular check

В critical-DE репо полезно настроить periodic fsck:

# Cron job, раз в неделю
$ git fsck --full --no-reflogs --unreachable > /tmp/fsck-$(date +%Y%m%d).log
$ grep -c "dangling" /tmp/fsck-*.log

Если внезапно количество dangling выросло — кто-то делал rewrites, посмотри.

git maintenance (см. урок 04) делает это automatically.


Попробуй сам

Полный цикл lose -> fsck -> restore:

$ mkdir fsck-demo && cd fsck-demo
$ git init

# Создаём работу
$ for i in {1..5}; do
    echo "version $i" > file.txt
    git add file.txt
    git commit -m "Commit $i"
  done

# Создаём ветку с эксклюзивной работой
$ git checkout -b feat/lost-work
$ echo "important feature" > critical.py
$ git add critical.py
$ git commit -m "Add critical feature"

# Возвращаемся на main и удаляем ветку
$ git checkout main
$ git branch -D feat/lost-work
Deleted branch feat/lost-work (was XYZ).

# Симулируем reflog cleanup (никогда не делай это в реальности!)
$ git reflog expire --expire=now --all
$ git reflog | head
# может быть пусто

# fsck
$ git fsck --lost-found
dangling commit XYZ

# Восстанавливаем
$ git branch recovered-feat XYZ
$ git log recovered-feat --oneline
XYZ Add critical feature
...

$ ls
critical.py file.txt    # critical.py вернулся

Killer takeaway

git fsck — recovery когда reflog не помогает. --lost-found записывает orphan commits в .git/lost-found/. Dangling = no ref points to it; unreachable = no path from any ref. Восстановление: git branch <name> <sha>. После git gc orphan commits удаляются — fsck бессилен. План B: clone с remote, bundle от коллеги. git for-each-ref — diagnostic список refs. Для critical репо — periodic fsck в cron + push regular как backup. Профилактика лучше: не делай force-push в production, branch protection (модуль 19), wip-commit перед рискованной операцией.

grep: поиск по содержимому файлов и вывода команд

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4