reset вглубь: soft, mixed, hard и чем отличается от revert

Разбираемся, что на самом деле двигает git reset, почему у него три режима и чем он принципиально отличается от revert и restore.

git reset перемещает указатель текущей ветки на выбранный коммит и, в зависимости от режима, по-разному синхронизирует с ним индекс и рабочую директорию.

Базовую тройку «restore / reset / revert» мы уже встречали. Здесь — глубокий разбор именно reset: что значат --soft, --mixed и --hard на уровне внутренней механики git, и как осознанно выбирать между reset, revert и restore.

Зачем понимать reset глубоко

reset — единственная по-настоящему опасная из команд отмены: одним флагом она может либо аккуратно «разобрать» коммит обратно в правки, либо безвозвратно стереть вашу работу. Разница — в одном слове. Чтобы не бояться команды и не терять код, нужно понимать модель трёх деревьев.

Три дерева Git

«Дерево» здесь — это снимок набора файлов. Git постоянно жонглирует тремя такими снимками:

ДеревоЧто этоРоль
HEADпоследний коммит текущей ветки«как было в прошлом коммите»
Индекс (staging)то, что попадёт в следующий коммит«черновик будущего коммита»
Рабочая директорияреальные файлы на диске«с чем вы работаете прямо сейчас»

Обычный цикл — это перетекание изменений между деревьями: правите файлы (рабочая директория), git add копирует их в индекс, git commit превращает индекс в новый HEAD. reset запускает этот конвейер в обратную сторону, а флаг определяет, до какого дерева докатить откат.

Три режима reset

Все три сначала делают одно и то же: двигают указатель ветки (HEAD) на заданный коммит. Дальше начинаются различия.

--soft: трогаем только HEAD

git reset --soft HEAD~1

Указатель ветки сдвинулся на коммит назад, но индекс и рабочая директория остались как были. Изменения отменённого коммита теперь лежат в индексе, готовые к новому коммиту. Идеально, чтобы переделать последний коммит: разбить его, дописать, переформулировать сообщение.

--mixed: HEAD + индекс (режим по умолчанию)

git reset HEAD~1        # --mixed подразумевается

Двигает HEAD и сбрасывает индекс под него, но файлы на диске не трогает. Изменения отменённого коммита оказываются в рабочей директории как незастейдженные правки. Этот же режим без указания коммита (git reset file.py) — стандартный способ убрать файл из staging.

--hard: HEAD + индекс + рабочая директория

git reset --hard HEAD~1

Откатывает все три дерева. Файлы на диске перезаписываются под целевой коммит — все несохранённые правки и изменения отменённого коммита исчезают безвозвратно. Это самая опасная команда урока. Используйте её, только когда точно решили выбросить текущее состояние.

РежимHEADИндексРабочая директорияГде окажутся изменения
--softсдвинутне тронутне тронутав индексе (staged)
--mixedсдвинутсброшенне тронутав рабочей директории (unstaged)
--hardсдвинутсброшенперезаписанаудалены

reset до файла, а не до коммита

У reset есть второй облик — с указанием пути. Он не двигает HEAD, а лишь обновляет запись о файле в индексе из указанного коммита (по умолчанию HEAD). Это и есть «убрать из staging»:

git reset HEAD config.yaml   # вынуть config.yaml из индекса, правки в файле сохранить

В современном git ту же задачу решает более понятная команда git restore --staged config.yaml — она специально создана, чтобы не перегружать reset.

reset против revert против restore

Три команды, три уровня воздействия:

КомандаНа что влияетПереписывает историю?Когда применять
restoreсодержимое файлов / индекснетоткатить правки в файле, вынуть из staging
resetуказатель ветки (+ деревья)дапеределать локальные, незапушенные коммиты
revertдобавляет новый коммитнетбезопасно отменить опубликованный коммит

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

Как это работает под капотом

Коммит в git — это неизменяемый объект, ссылающийся на снимок дерева и на родителя. Ветка (main) — это просто текстовый файл с хешем одного коммита (загляните в .git/refs/heads/). reset по сути переписывает этот файл, заставляя ветку указывать на другой коммит. Сами «отменённые» коммиты никуда не удаляются — они остаются в базе объектов и доступны через git reflog, пока их не подберёт сборщик мусора. Именно поэтому даже после --hard часто можно спастись.

Спасение после ошибочного reset

Сделали reset --hard и потеряли коммиты? reflog хранит журнал всех перемещений HEAD:

git reflog
# ...
# 9f8e7d6 HEAD@{1}: commit: Важная фича, которую я снёс
git reset --hard HEAD@{1}   # вернуть HEAD на тот коммит

Пока с момента ошибки не прошло слишком много времени (по умолчанию недели), потерянный коммит почти наверняка ещё там.

Частые ошибки

  • Использовали --hard с незакоммиченными правками. Несохранённая работа в рабочей директории при --hard теряется без следов в reflog. Перед опасным reset сделайте git stash.
  • Сделали reset запушенной ветки, потом force-push. Это переписывает общую историю — классический способ сломать репозиторий всей команде. Для опубликованного только revert.
  • Путают reset HEAD~1 (откат коммита) и reset file (вынуть из staging). Без аргумента-пути reset двигает ветку; с путём — только правит индекс, HEAD стоит на месте.
  • Считают --hard необратимым приговором. Потерянные коммиты почти всегда возвращаются через reflog; безвозвратно гибнут лишь незакоммиченные изменения.

Итоги

  • Git оперирует тремя деревьями: HEAD (прошлый коммит), индекс (будущий коммит), рабочая директория (диск).
  • Все режимы reset двигают HEAD; --soft на этом останавливается, --mixed ещё сбрасывает индекс, --hard вдобавок перезаписывает файлы.
  • --soft кладёт изменения в индекс, --mixed — в рабочую директорию, --hard — уничтожает их.
  • reset — для незапушенного, revert — для опубликованного, restore — для содержимого файлов.
  • После --hard потерянные коммиты обычно спасает git reflog; незакоммиченные правки — нет.
Проверьте себя
1. Какие три дерева различает Git при работе reset?
Amain, develop и feature
BHEAD, индекс (staging) и рабочая директория
Clocal, remote и upstream
Dcommit, branch и tag
2. Куда попадут изменения отменённого коммита после git reset --soft HEAD~1?
AБудут удалены безвозвратно
BОстанутся в индексе (staged), готовые к новому коммиту
CОкажутся в рабочей директории как незастейдженные
DПревратятся в обратный коммит
3. Нужно отменить коммит, который уже запушен и которым пользуются коллеги. Что выбрать?
Agit reset --hard
Bgit reset --soft
Cgit revert
Dgit restore
4. Случайно выполнили git reset --hard и снесли нужные коммиты. Что поможет?
AНичего, коммиты удалены навсегда
Bgit reflog покажет прошлые позиции HEAD, на одну из которых можно вернуться
Cgit revert восстановит их
DТолько повторный git clone с сервера