← назад к разделу

Главная правда о Git, которую стоит выучить до первой паники: закоммиченное почти невозможно потерять. У каждой ошибки есть штатная отмена, а на крайний случай существует reflog — чёрный ящик вашего репозитория, который помнит даже то, что вы из истории выбросили. Разберём инструменты от лёгких случаев к тяжёлым.

один лишний коммит C3 — два способа его убрать git reset --hard HEAD~1 C3 C1 C2 вне истории main git reflog → d4e5f6 HEAD@{1}: commit: C3git reset --hard d4e5f6 — C3 снова в истории git revert c3d4 C1 C2 C3 C4 отмена C3 main

Один и тот же лишний коммит C3 в двух исходах. Сверху reset --hard двигает закладку main назад на C2: коммит выпадает из истории, но не исчезает — git reflog помнит его идентификатор, и по нему всё возвращается. Снизу revert закладку не двигает: за C3 встаёт новый коммит C4 с обратными правками, и в истории видно и ошибку, и её отмену.

Отменить незакоммиченное: restore

git restore file.txt            # вернуть файл к состоянию из индекса (правки ПРОПАДУТ)
git restore --staged file.txt   # убрать из индекса (правки останутся в файле)
git restore --source=HEAD .     # вернуть рабочие файлы к последнему коммиту

Единственное место в Git, где работа теряется по-настоящему, — здесь: незакоммиченные правки после restore не вернуть. Деталь, на которой спотыкаются: без --source restore берёт содержимое из индекса, а не из коммита, — если файл уже добавлен через add, вы откатитесь к добавленному. Отсюда правило: сомневаетесь — сначала закоммитьте или спрячьте в stash, откатить сможете всегда.

Самая частая мелкая отмена не «всё назад», а «вернуть один файл таким, каким он был три коммита назад»: git restore --source=HEAD~3 путь/к/файлу или --source=a1b2c3, коммит любой. Файл перезапишется в рабочей директории, а история не тронется; дальше его коммитят как обычную правку. Так чинят случайно сломанную конфигурацию, не откатывая соседние изменения.

И вторая по-настоящему необратимая команда, о которой в статье про потери нельзя не сказать: git clean удаляет untracked-файлы, то есть те, что ни разу не попадали в снимок, и вернуть их будет неоткуда, ни reflog, ни restore их не видели. Поэтому сначала всегда сухой прогон git clean -nd, он только перечисляет, и лишь потом git clean -fd.

Спрятать на минутку: stash

Нужно срочно переключиться, а работа не доделана:

git stash                # спрятать незакоммиченное в карман
git stash -u             # то же, но вместе с новыми, ещё не добавленными файлами
git stash list           # что в кармане
git stash pop            # вернуть последнее спрятанное

Флаг -u здесь не факультатив, а заплатка на дырку в кармане. Обычный git stash прячет только то, за чем Git уже следит. Файл, который вы создали пять минут назад и ни разу не добавили через git add, остаётся лежать в рабочей папке и спокойно переезжает с вами на другую ветку — ровно тогда, когда вы были уверены, что убрали со стола всё.

Вторая деталь всплывает при возврате. Если git stash pop упёрся в конфликт, запись из кармана не удаляется: вы разбираете конфликт, работаете дальше, а спрятанное остаётся в списке и при следующем pop всплывает вторым экземпляром. Git об этом честно предупреждает — «The stash entry is kept in case you need it again». После разбора запись убирают руками: git stash drop.

Stash — временный карман, не архив: вернулись к задаче — сразу pop. Полгода коллекционировать stash'и — способ забыть, что в них было.

Откатить коммиты: reset против revert

Два инструмента для разных ситуаций — граница проходит по вопросу «отправлено ли это уже другим?»

reset — переписать локальную историю (коммиты ещё не пушены). Он снимает последний коммит, а флаг решает, что делать с его изменениями. --soft оставляет их в индексе: это «пересобрать коммит иначе». Без флага (--mixed) индекс сбрасывается, а файлы на диске не тронуты. --hard выбрасывает изменения совсем: «этого не было», и с ним как с UPDATE без WHERE, сначала убедитесь, что выбрасываете именно то.

--soft коммит снят --mixed коммит снят индекс сброшен --hard коммит снят индекс сброшен файлы затёрты

Каждый следующий флаг достаёт на слой глубже, и там, где ряд обрывается, reset и останавливается: правки теряются только в самом длинном ряду, у --hard.

git reset --soft HEAD~1   # снять последний коммит, изменения остаются в индексе
git reset HEAD~1          # снять коммит и сбросить индекс; файлы на диске не тронуты (по умолчанию)
git reset --hard HEAD~1   # снять коммит И ВЫБРОСИТЬ изменения

Про средний вариант стоит сказать точнее, чем принято. git reset без флагов (это и есть --mixed) рабочие файлы не трогает вообще: он двигает закладку назад и приводит к новому состоянию индекс. Для только что снятого коммита итог и правда выглядит как «изменения вернулись в рабочие файлы». Но если в индексе уже лежало что-то набранное, вы этот набор потеряете — правки в файлах останутся, а git add придётся делать заново.

revert — отменить публично и честно (коммиты уже на сервере):

git revert a1b2c3    # создаёт НОВЫЙ коммит с обратными изменениями

История не переписывается — она дополняется коммитом-антиподом. Именно так откатывают влитое в main: сломавший прод PR отменяется revert-коммитом за минуту, безопасно и с сохранением летописи. Правило в одну строку: локальное — reset, отправленное — revert.

Отменять можно и несколько коммитов сразу: git revert a1b2c3^..d4e5f6 создаст по обратному коммиту на каждый, а с ключом --no-commit соберёт все обратные правки в индекс, чтобы закоммитить откат одним снимком с одним сообщением. Отмена отмены тоже штатная: git revert <sha revert-коммита> возвращает изменение обратно, и это единственный честный способ вернуть в main откаченную фичу после починки.

Откат слияния: revert merge-коммита

Отдельный случай — revert merge-коммита. У него два родителя, и Git надо сказать, какой считать основной линией: git revert -m 1 <sha> откатывает всё, что принесла влитая ветка, относительно первого родителя (обычно это main). Побочный эффект неочевиден, и объясняется он не памятью Git — Git ничего не запоминает. Дело в родстве коммитов: после слияния коммиты влитой ветки навсегда остались предками main, и revert их оттуда не убрал — он только добавил сверху коммит с обратными правками. Для Git ветка по-прежнему влита, поэтому повторный git merge честно отвечает «Already up to date» и не приносит ничего, а кода в файлах нет: его снял revert. Если фичу починят и захотят влить снова, сначала придётся отменить саму отмену — сделать revert revert-коммита.

reflog: машина времени последней надежды

«Сделал reset --hard не туда», «rebase всё сломал», «удалил ветку с работой» — спокойно. Reflog — журнал всех перемещений HEAD, включая «удалённые» состояния:

git reflog
# a1b2c3 HEAD@{0}: reset: moving to HEAD~3
# d4e5f6 HEAD@{1}: commit: та самая работа   ← вот оно!

git reset --hard d4e5f6      # вернуться в состояние до аварии
# или аккуратнее: git branch rescue d4e5f6 — поставить ветку на найденный коммит

Reflog есть не только у HEAD, а у каждой ветки, и это спасает в двух сценариях, которые пугают сильнее прочих. Первый: удалили ветку с работой. git reflog show feature-export уже не сработает, ветки нет, но её последний коммит остался в git reflog HEAD (вы на ней когда-то стояли): находите строку с checkout: moving from feature-export, берёте хеш и ставите ветку обратно, git branch feature-export <hash>. Второй: коллега сделал force-push в общую ветку, и ваши коммиты пропали с сервера. Ваш клон помнит, где стоял origin/main до последнего fetch: git reflog show origin/main показывает предыдущие положения серверной ветки, и из строки перед force-push восстанавливается всё, что там было, той же командой git branch rescue <hash>; дальше это можно снова запушить. Работает, пока вы не сделали fetch -p слишком много раз и пока не прошли те самые 30 дней.

Когда reflog не помог (записи истекли, клон свежий), остаётся последний рубеж: git fsck --lost-found перечисляет объекты, до которых не дотянуться ни из одной ветки, но которые ещё не вычищены; среди них ищут «dangling commit», смотрят git show <hash> и ставят на найденный ветку. Это уже раскопки, а не штатная процедура, и они напоминают, зачем пушить рано.

У этого чёрного ящика есть границы, и знать их лучше до аварии, а не после. Reflog — журнал вашего клона и только вашего: он ведётся на вашей машине с той секунды, как вы сделали git clone, и чужие коммиты в нём не появятся. В свежем клоне он пуст — спасать там нечего. И записи не вечны: состояния, до которых из веток уже не дойти, Git вычищает примерно через 30 дней, обычные записи — через 90.

Поэтому обещание из начала статьи точнее звучит так: закоммиченное и не брошенное на месяц потерять почти невозможно — на вашей машине. Отсюда два правила разом: коммитьте часто (коммит — страховка, которую нельзя отобрать) и пушьте рано (страховка, которая переживёт сгоревший ноутбук).

Коротко

  • Незакоммиченное — единственное по-настоящему теряемое: restore необратим. Сомневаетесь — коммит или stash.
  • stash — карман на минутку: спрятал → переключился → вернулся → pop. Новые файлы прячет только stash -u; после конфликта в pop запись остаётся в кармане.
  • Локальную историю правит reset (soft — пересобрать, hard — выбросить); отправленную отменяет revert новым коммитом. «Локальное — reset, публичное — revert».
  • reflog помнит потерянные состояния около 30 дней (обычные записи — 90), но только в вашем клоне: после любой аварии — git reflog → вернуться на нужный коммит, и лучше не откладывать.
  • Частые коммиты = неотбираемая страховка.
  • Один файл из старого коммита возвращает git restore --source=<коммит> <файл>; git clean необратим, сначала -nd, потом -fd.
  • revert умеет диапазон и --no-commit (один коммит отката на несколько), а отмена отмены возвращает фичу обратно.
  • Удалённую ветку ставят обратно по хешу из git reflog, затёртую force-push серверную ветку восстанавливают из git reflog show origin/main; последний рубеж git fsck --lost-found.

Что почитать дальше