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; незакоммиченные правки — нет.