Цель урока
После урока вы умеете восстановить потерянный коммит через git reflog, восстановить staged-изменения через git fsck --lost-found и точно знаете, какие операции Git безвозвратно удаляют данные. Понимаете, как reset --hard действует на файл в каждом из трёх состояний (working / staging / committed).
| Параметр | Значение |
|---|---|
| Bloom | Применение, частично Анализ |
| SFIA | Уровень 2 |
| Время | 40–60 минут |
| Артефакт | ~/.gitconfig с алиасами safe-reset, recover-staged, tree, undo |
| Проверка | Мини-тест + восстановление потерянного коммита за 2 команды |
Теория: что такое «потеря данных» в Git
В Дне 1 вы видели три состояния — working / staging / committed. Сегодня смотрим на них под углом reset’а.
git reset --hard HEAD делает три разные вещи одновременно:
- Сбрасывает index (staging) до состояния HEAD
- Перезаписывает tracked файлы в working дереве до HEAD
- Untracked файлы не трогает — остаются как есть
Отсюда таблица «что выживает»:
| Состояние файла | После reset --hard | Чем спасать |
|---|---|---|
Untracked (не было git add) | Остаётся на диске | Никаких спасений не нужно |
Modified (правки в working, без свежего git add) | Откат к HEAD, правки потеряны | Local History IDE, swap-файл, ничего |
Staged (был git add, но нет git commit) | Откат, но blob остался в object store | git fsck --lost-found |
Committed (был git commit) | Коммит «потерян», HEAD смотрит мимо | git reflog (90 дней) |
Эта таблица объясняет 90% случаев «я потерял код». Запомните её.
«Щёлкнуло» дня: Git защищает данные после git add. До git add вы полагаетесь на IDE / редактор / бэкап ФС. После git add у Git минимум один шанс вас спасти.
Практика 1: reflog восстанавливает закоммиченное
Шаг 1. Собираем демо-репозиторий
mkdir -p /tmp/demo-reflog && cd /tmp/demo-reflog
git init -q
git config user.name Student
git config user.email student@example.com
echo "console.log('v1')" > app.js
git add . && git commit -q -m "v1"
echo "console.log('v2')" > app.js
git add . && git commit -q -m "v2"
echo "console.log('v3')" > app.js
git add . && git commit -q -m "v3"
git log --oneline
Три коммита: v1, v2, v3.
Шаг 2. «Катастрофа»
git reset --hard HEAD~2
git log --oneline
Вывод:
<sha> v1
v2 и v3 «исчезли». В git log их нет.
Шаг 3. Восстановление через reflog
git reflog --date=iso
Вывод:
<sha1> HEAD@{0} reset: moving to HEAD~2
<sha3> HEAD@{1} commit: v3
<sha2> HEAD@{2} commit: v2
<sha1> HEAD@{3} commit (initial): v1
Reflog — это лог всех движений HEAD: коммиты, reset, checkout, rebase. Git держит его локально ~90 дней по умолчанию.
Возвращаемся на v3:
git reset --hard HEAD@{1}
git log --oneline
Вывод: v3, v2, v1 — всё на месте.
Вывод: reflog спасает всё, что было закоммичено. Даже после reset --hard коммиты живы ~90 дней в object store.
Практика 2: fsck восстанавливает staged, но не закоммиченное
Шаг 1. Собираем
mkdir -p /tmp/demo-fsck && cd /tmp/demo-fsck
git init -q
git config user.name Student
git config user.email student@example.com
echo "v1" > file.txt
git add . && git commit -q -m "v1"
echo "v2-важная-правка" > file.txt
git add file.txt # staged, но НЕ git commit
Шаг 2. Катастрофа
git reset --hard HEAD
cat file.txt # → v1, правка исчезла
Reflog здесь не поможет — коммита не было, HEAD не двигался к v2. Но git add уже создал blob в object store. Ищем:
Шаг 3. Восстановление через fsck
git fsck --lost-found
Вывод:
Checking object directories: 100%, done.
dangling blob 7a2b1c3d4e5f...
Смотрим содержимое blob’а:
git cat-file -p $(git fsck --lost-found 2>/dev/null | awk '/dangling blob/{print $3}' | head -1)
Вывод: v2-важная-правка.
Возвращаем в файл:
BLOB=$(git fsck --lost-found 2>/dev/null | awk '/dangling blob/{print $3}' | head -1)
git cat-file -p "$BLOB" > file.txt
cat file.txt # → v2-важная-правка
Вывод: fsck спасает всё, что попало в index через git add, даже если следом был reset --hard. Blob живёт ~14 дней до git gc.
Практика 3: когда не спасёт никто
Сценарий: 3 часа кода, ни одного git add
mkdir -p /tmp/demo-lost && cd /tmp/demo-lost
git init -q
git config user.name Student
git config user.email student@example.com
echo "initial" > file.txt
git add . && git commit -q -m "initial"
# три часа работы — только редактировали, ни разу git add
echo "3 часа моего кода" > file.txt
# случайный reset --hard
git reset --hard HEAD
cat file.txt # → initial
Пробуем:
git reflog # покажет только initial commit + reset operation
git fsck --lost-found # dangling? ничего, blob не создавался
Ни reflog, ни fsck не помогут. Blob никогда не существовал.
Что остаётся:
- Local History в IDE (VSCode: Timeline, JetBrains: Local History, Vim: undo-persistence)
- Swap-файл редактора (
.swp,.vim/undo/) - Резервные копии файловой системы (Time Machine, rsync, ZFS snapshots)
Это главный урок Дня 2: Git защищает данные после git add. До git add вы вне его зоны ответственности.
Артефакт: защитные алиасы в ~/.gitconfig
Добавьте в ~/.gitconfig:
git config --global alias.tree "log --all --graph --decorate --oneline"
git config --global alias.undo "reset --soft HEAD~1"
Два защитных алиаса сохраняем в файл (длинные):
cat >> ~/.gitconfig << 'EOF'
[alias]
# reset --hard с подтверждением
safe-reset = "!f() { \
echo 'reset --hard уничтожит modified и staged изменения.'; \
echo 'Сначала: git stash push -u (включая untracked).'; \
read -p 'Продолжить? (yes/NO): ' c; \
[ \"$c\" = yes ] && git reset --hard \"$@\"; \
}; f"
# восстановить все dangling blobs в .recovered/
recover-staged = "!f() { \
mkdir -p .recovered; \
for b in $(git fsck --lost-found 2>/dev/null | awk '/dangling blob/{print $3}'); do \
git cat-file -p $b > .recovered/$b.txt; \
done; \
echo \"Восстановлено в .recovered/\"; \
ls .recovered; \
}; f"
EOF
Использование:
git tree # граф всех веток и коммитов
git undo # откатить последний коммит (staged остаётся)
git safe-reset HEAD # reset --hard с подтверждением
git recover-staged # вытащить всё из dangling blobs
git undo = reset --soft HEAD~1 — возвращает staging к состоянию «до коммита», файлы остаются. Это то, что вам нужно в 90% случаев «я закоммитил слишком рано».
git safe-reset добавляет паузу перед reset --hard. Секунды размышлений vs часы восстановления.
Частые ошибки
| Ошибка | Почему больно | Как не делать |
|---|---|---|
git reset --hard на чужой ветке после pull | Теряете локальные коммиты без шанса увидеть их в reflog (reflog только для вашего репо) | Перед reset — git status, git stash push -u |
git clean -fd следом за reset | Удаляет untracked файлы — те самые, которые reset –hard пощадил. Ничто не спасёт | Сначала git stash -u, потом clean |
git gc --prune=now сразу после катастрофы | Выкидывает dangling blobs до того, как вы их нашли | Не трогайте gc, пока не решена проблема |
«Я делал правки, Git их съел» без git add | Правки не были в Git — нечего восстанавливать | IDE Local History / swap / ФС-бэкапы |
📝 Документирование
Создайте ~/notes/day-02.md и ответьте:
- Три состояния под углом reset’а: что выживает, что нет — одной таблицей.
- Разница reflog vs fsck: по одному предложению на каждый.
- Ваш случай из практики: теряли ли код? В каком состоянии был файл, как спаслись (или не спаслись)?
- Какой алиас вы поставите первым в свой
~/.gitconfigи почему?
Мини-тест
- Вы сделали 2 коммита, потом
git reset --hard HEAD~2. Какой командой вернуть оба коммита? - Вы сделали
git add file.txt, но неgit commit. Потомgit reset --hard. Где искать содержимоеfile.txt? - Вы час редактировали
.env, ни разу не делалиgit add. Потомgit reset --hard. Что покажет reflog? Что покажет fsck? - Вы сделали
git reset --soft HEAD~1. Пропали ли правки из staging? А из working?
Ответы — в конце поста.
Что дальше
- День 3 → ветки и merge: когда fast-forward, когда
--no-ff, как читатьgit log --graph - Challenge →
docker run devitway/git-challenge: в одной из 10 задач ждёт dangling blob без подсказок — найдёте за 2 команды - Системно с нуля → «Курс молодого бойца» DevIT Academy, Git разбираем в Неделе 2
Ответы на мини-тест
git reflog→ найти нужныйHEAD@{N}→git reset --hard HEAD@{N}. Reflog покажет всю историю движений HEAD за последние ~90 дней.git fsck --lost-found→ скопировать SHA изdangling blob <sha>→git cat-file -p <sha>. Blob живёт в object store ~14 дней доgit gc.Reflog покажет только операцию
reset: moving to HEADи initial commit — содержимого.envв reflog нет. Fsck не покажет ничего — blob никогда не создавался, потому что не былоgit add. Данные потеряны навсегда. Искать: Local History IDE, swap-файл редактора, ФС-бэкап.git reset --soft HEAD~1откатывает только HEAD, не трогая index и working. Правки остаются в staging (git statusпокажет их как «to be committed»). Working тоже не меняется. Это безопасный способ «отменить последний коммит, чтобы переделать сообщение или перегруппировать файлы».