Сценарий знаком каждому. Ты делаешь
Код: Выделить всё
git reset --hardТак вот, главное правило этого урока, и запомни его раньше всех команд: не паникуй и ничего больше не трогай. Git - не текстовый редактор, где Ctrl+Z имеет глубину в пару шагов. Под капотом это база данных с журналом всех движений, и почти всё, что ты сделал, ещё лежит на диске. Большинство катастроф, которые новички считают фатальными, на деле откатываются одной-двумя командами. Опасны не сами операции - опасны хаотичные движения после них. Человек, который снёс reset-ом коммит, а потом в панике начал делать новые коммиты, checkout-ы и ещё один reset, своими руками затаптывает следы, которые Git бережно сохранил.
Разберём механику восстановления так, чтобы ты понимал не заклинание, а почему оно работает. Тогда в реальный момент паники ты будешь действовать, а не гуглить дрожащими руками.

Reflog - чёрный ящик твоего репозитория
Чтобы
Код: Выделить всё
git reset hard отменитьКод: Выделить всё
.git/logs/HEADКод: Выделить всё
.git/logs/refs/heads/<ветка>Ключевая идея: коммит в Git живёт, пока на него есть хоть какая-то ссылка. Ветка - ссылка. HEAD - ссылка. И reflog тоже держит ссылки. Поэтому даже если ни одна ветка больше не указывает на твой коммит, на него ещё месяца три указывает запись в reflog, и сборщик мусора его не тронет. Дефолт - 90 дней для достижимых записей и 30 дней для недостижимых (
Код: Выделить всё
gc.reflogExpireКод: Выделить всё
gc.reflogExpireUnreachableСмотрим журнал:
Код: Выделить всё
$ git reflog
8f3a1c2 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~3
a91be07 HEAD@{1}: commit: add cache layer
4d2f8aa HEAD@{2}: commit: wire up redis client
1c7e0b9 HEAD@{3}: commit: draft config
8f3a1c2 (HEAD -> main) HEAD@{4}: commit: base scaffolding
Код: Выделить всё
HEAD@{0}Код: Выделить всё
HEAD@{1}Код: Выделить всё
a91be07 "add cache layer"Код: Выделить всё
HEAD@{N}Возврат - один reset обратно:
Код: Выделить всё
$ git reset --hard a91be07
HEAD is now at a91be07 add cache layer
Код: Выделить всё
git reset --hard HEAD@{1}Важная оговорка про
Код: Выделить всё
reset --hardКод: Выделить всё
reset --hardORIG_HEAD, MERGE_HEAD, FETCH_HEAD - спец-указатели на крайний случай
Git заранее знает, что reset, merge и rebase опасны, поэтому перед такими операциями он сам сохраняет точку возврата. Это
Код: Выделить всё
git orig_headКод: Выделить всё
.git/ORIG_HEADРазберём три брата, чтобы не путать:
- ORIG_HEAD - где был HEAD до последней крупной операции (reset/merge/rebase/pull). Твоя кнопка "отмена" для одного шага назад.
- MERGE_HEAD - существует только во время незавершённого merge. Указывает на вершину ветки, которую ты вливаешь. Видишь его в - значит, ты в середине слияния (например, застрял в конфликте).
Код: Выделить всё
.git - FETCH_HEAD - что в последний раз притащил с удалёнки. На него ссылается
Код: Выделить всё
git fetch, который скрыто и делаетКод: Выделить всё
git merge FETCH_HEAD.Код: Выделить всё
git pull
Код: Выделить всё
$ git merge feature/payments
# понял, что слил не то и не туда
$ git reset --hard ORIG_HEAD
HEAD is now at 8f3a1c2 base scaffolding
Откатить rebase: ORIG_HEAD, --abort и reflog
Rebase - самая частая причина запросов "
Код: Выделить всё
откатить rebase gitСитуация А: rebase ещё идёт и встал на конфликте или ты понял, что всё пошло не так. Пока процесс не завершён, существует папка состояния (например,
Код: Выделить всё
.git/rebase-merge/Код: Выделить всё
$ git rebase --abort
Код: Выделить всё
git merge --abortКод: Выделить всё
git cherry-pick --abortСитуация Б: rebase уже завершился, и только теперь ты видишь, что история - каша.
Код: Выделить всё
--abortКод: Выделить всё
$ git reset --hard ORIG_HEAD
Код: Выделить всё
$ git reflog
2b9f4e1 (HEAD -> main) HEAD@{0}: rebase (finish): returning to refs/heads/main
2b9f4e1 HEAD@{1}: rebase (pick): add cache layer
7c01aa3 HEAD@{2}: rebase (pick): wire up redis client
e44d9c0 HEAD@{3}: rebase (start): checkout main~5
a91be07 HEAD@{4}: commit: add cache layer
Код: Выделить всё
rebase (start)Код: Выделить всё
rebase (finish)Код: Выделить всё
HEAD@{4}Код: Выделить всё
rebase (start)Код: Выделить всё
$ git reset --hard HEAD@{4}
Восстановиться после force-push
Теперь страшное -
Код: Выделить всё
восстановить после force pushКод: Выделить всё
git push --forceСпокойно, восстановить почти всегда реально, и вот откуда брать данные.
- Твой локальный reflog. Force-push не трогает локальный репозиторий - там вся старая история цела. Находишь нужный коммит в (или у тебя ещё жив
Код: Выделить всё
git reflogна старое значение), сбрасываешь ветку на него и пушишь обратно.Код: Выделить всё
refs/remotes/origin/<ветка> - Чужой клон. Если коллега не делал fetch после твоего форс-пуша, у него на машине лежит старое . Он делает
Код: Выделить всё
origin/main... стоп, не делает - сначала смотритКод: Выделить всё
git fetchилиКод: Выделить всё
git reflog --all, достаёт хеш потерянной вершины и отдаёт тебе. Чужая машина - тоже резервная копия истории.Код: Выделить всё
git log origin/main - Сервер. На GitHub/GitLab осиротевшие коммиты живут какое-то время и достаются через reflog сервера, события API или интерфейс ("Activity"/восстановление ветки). На своём bare-репозитории - тот же , если на сервере включён
Код: Выделить всё
git reflog.Код: Выделить всё
core.logAllRefUpdates
Код: Выделить всё
--forceКод: Выделить всё
--force-with-leaseКод: Выделить всё
$ git push --force-with-lease origin main
Код: Выделить всё
--forceКод: Выделить всё
--force-with-leaseКод: Выделить всё
$ git push --force-with-lease origin main
! [rejected] main -> main (stale info)
error: failed to push some refs to 'origin'
Код: Выделить всё
stale infoУдалённый stash и затёртый файл - тоже спасаемо
Случайно дропнул stash.
Код: Выделить всё
git stash dropКод: Выделить всё
git stash popКод: Выделить всё
$ git fsck --no-reflogs | grep commit
dangling commit 3e7af19c0b4a8d2e5f6071c9d8a4b3c2e1f09a8b
$ git stash store -m "rescued" 3e7af19c
$ git stash list
stash@{0}: rescued
Код: Выделить всё
git fsckКод: Выделить всё
git stash storeЗатёр файл в рабочем каталоге - например, неудачным checkout или reset. Если версия файла была в любом коммите, восстановить точечно:
Код: Выделить всё
$ git restore --source=HEAD@{2} -- config/redis.yml
Код: Выделить всё
git restoreПлохой merge: revert -m или reset до ORIG_HEAD
Влил ветку, а merge оказался плохим - принёс баги или вообще не то. Тут развилка зависит от того, ушёл ли merge на общий сервер.
Merge ещё локальный, никто не видел. Просто перепиши историю, как будто слияния не было:
Код: Выделить всё
$ git reset --hard ORIG_HEAD
Код: Выделить всё
git merge --abortMerge уже опубликован, коллеги его подтянули. Тут
Код: Выделить всё
resetКод: Выделить всё
revertКод: Выделить всё
-mКод: Выделить всё
$ git revert -m 1 9c4f0a2
Код: Выделить всё
-m 1Код: Выделить всё
git show 9c4f0a2Код: Выделить всё
Merge: <parent1> <parent2>Большие грабли с revert merge: revert убирает изменения фичи, но в графе слияние остаётся. Если потом захочешь влить ту же фичу заново, Git решит, что она уже была, и не принесёт коммиты. Лечится revert-ом самого revert-а перед повторным слиянием. Но это уже тема отдельного разговора - просто знай, что revert merge оставляет такой шлейф.
Свод: если случилось X - делай Y
Вот та самая таблица "
Код: Выделить всё
отменить опасную операцию gitКод: Выделить всё
КАТАСТРОФА -> ЛЕЧЕНИЕ
reset --hard снёс коммиты -> git reflog; git reset --hard HEAD@{1}
(или git reset --hard ORIG_HEAD)
rebase идёт и сломался -> git rebase --abort
rebase завершился, история каша -> git reset --hard ORIG_HEAD
(или HEAD@{N} до 'rebase (start)')
merge в процессе (конфликт) -> git merge --abort
плохой merge, локальный -> git reset --hard ORIG_HEAD
плохой merge, уже запушен -> git revert -m 1 <хеш merge>
cherry-pick встал на конфликте -> git cherry-pick --abort
ошибочный force-push -> reflog/чужой клон -> reset -> push
--force-with-lease; профилактика: lease
дропнут stash -> git fsck --no-reflogs; git stash store
затёрт файл -> git restore --source=<rev> -- путь
Мини-лаба: устрой катастрофу и спасись
Лучший способ перестать бояться - сломать всё в песочнице и починить руками.
- Создай полигон: , сделай 4 коммита подряд (трогай любой файл,
Код: Выделить всё
git init lab && cd lab...Код: Выделить всё
git commit -am "c1").Код: Выделить всё
"c4" - Снеси два коммита: . Убедись через
Код: Выделить всё
git reset --hard HEAD~2, что их нет.Код: Выделить всё
git log --oneline - Спасись: , найди строку с "c4", верни
Код: Выделить всё
git reflog. Проверь log - всё на месте.Код: Выделить всё
git reset --hard HEAD@{1} - Сломай merge: создай ветку , коммить туда правку того же файла, вернись
Код: Выделить всё
git switch -c feat, правь ту же строку, коммить. СлейКод: Выделить всё
git switch main- получишь конфликт. Выйди:Код: Выделить всё
git merge feat.Код: Выделить всё
git merge --abort - Откати завершённый merge: разреши конфликт и доведи merge до конца, потом - слияния как не бывало.
Код: Выделить всё
git reset --hard ORIG_HEAD - Спаси stash: какую-нибудь правку, потом
Код: Выделить всё
git stash. Достань её обратно черезКод: Выделить всё
git stash dropиКод: Выделить всё
git fsck --no-reflogs | grep commit.Код: Выделить всё
git stash store
Контрольные вопросы
- Чем отличается выход из rebase командой от отката завершённого rebase через
Код: Выделить всё
--abort? Когда работает только один из вариантов?Код: Выделить всё
reset --hard ORIG_HEAD - Что именно хранит ORIG_HEAD и почему на него нельзя полагаться, если после ошибочной операции ты сделал ещё одну крупную операцию?
- Почему голый опасен, а
Код: Выделить всё
--forceнет? Что значит ответ сервераКод: Выделить всё
--force-with-lease?Код: Выделить всё
stale info - Ты случайно сделал и потерял незакоммиченные правки в файле, которого не было ни в одном коммите. Вернёт ли их reflog и почему?
Код: Выделить всё
reset --hard
Опасные операции Git опасны ровно до тех пор, пока ты не знаешь про reflog и ORIG_HEAD. Закоммиченное практически невозможно потерять: Git ведёт журнал всех движений HEAD и веток, и любой "потерянный" коммит достаётся одной строкой из
Код: Выделить всё
git reflogКод: Выделить всё
--abortКод: Выделить всё
reset --hard ORIG_HEADКод: Выделить всё
revert -mКод: Выделить всё
--force-with-lease