Git revert vs reset: какая команда спасёт вашу историю
Git revert против reset: какая команда безопасно отменяет коммиты, когда reset --hard уничтожает работу и как каждая переписывает общую историю GitHub.
TL;DR: git revert добавляет новый коммит, отменяющий старый, — история сохраняется, это безопасно на ветках, которые другие уже забрали. git reset отодвигает указатель ветки назад — история переписывается, безопасно только для незапушенных коммитов. На общих ветках — revert, для локальной уборки — reset. reset --hard заодно удаляет незакоммиченные изменения из рабочей копии — вот кто съедает настоящую работу.
В чём разница между git revert и git reset?
Оба делают проект таким, будто какой-то прошлый коммит не случался. Расходятся они в том, как:
git revert <sha>вычисляет обратный патч коммита и коммитит его. История растёт на один коммит со смыслом «отмени вот тот». Плохой коммит остаётся в логе, следом — его отмена. SHA всех остальных коммитов нетронуты.git reset <sha>передвигает указатель текущей ветки на<sha>. Коммиты после него отвязываются — они остаются в базе объектов до сборки мусора, но больше недостижимы ни из какой ветки и изgit log.
Один ничего не переписывает, другой делает вид, что ничего не было. Именно это свойство решает, что выбрать в каждой ситуации:
# Демо: одноразовый репозиторий, который можно запустить
git init /tmp/revert-vs-reset && cd /tmp/revert-vs-reset
echo one > file.txt && git add . && git commit -m "one"
echo two > file.txt && git add . && git commit -m "two"
# Revert: история хранит оба коммита плюс третий, отменяющий "two"
git revert --no-edit HEAD
git log --oneline # 3 коммита: revert, two, one
# Reset: указатель ветки уезжает назад, "two" исчезает из лога
git reset --hard HEAD~1
git log --oneline # 1 коммит: one
Прогоните оба и посмотрите git log — асимметрия и есть весь ответ.
Что на самом деле делают git reset --soft, --mixed и --hard?
Три режима управляют тем, где выживут ваши изменения после сдвига указателя:
| Режим | Указатель ветки | Индекс (staging) | Рабочая копия | Ваши правки |
|---|---|---|---|---|
--soft |
уезжает назад | остаются staged | нетронута | полностью целы |
--mixed (по умолчанию) |
уезжает назад | unstaged | нетронута | целы, но unstaged |
--hard |
уезжает назад | сброшен | сброшена | удалены |
Конкретные прочтения таблицы:
git reset --soft HEAD~1— «я закоммитил слишком рано». Всё возвращается в staged, готово к перекоммиту — возможно, вместе с новой работой.git reset HEAD~1(mixed) — «я застейджил не те файлы». Правки выживают в рабочей копии, в индексе пусто. Если заодно хотите прибрать лишние файлы, дополните его git remove untracked files.git reset --hard HEAD~1— «этот коммит и мои незакоммиченные правки — мусор». Git дважды спрашивать не станет; отмены для рабочей копии нет, если вы заранее не сделали stash или коммит.
У revert режимов нет, потому что он ничего не уничтожает — только добавляет компенсирующий коммит.
Когда использовать git revert вместо git reset?
Задайте один вопрос: кто-нибудь ещё уже забрал этот коммит? Если да — reset исключён.
Сброс ветки, на которой кто-то успел построить работу, переписывает общую историю. Их следующий pull столкнёт расходящиеся ветки, а «починкой» обычно становится force-push, превращающий вашу ошибку в проблему всех. Revert добавляет обычный коммит, поэтому простой git pull у всех сливается чисто — потому revert и есть ответ по умолчанию на main и в pull request'ах на GitHub, где кнопка «Revert» существует ровно потому, что переписать историю влитого PR нельзя.
Reset — для окна до публикации: коммит тридцатисекундной давности, ошибка стейджинга, локальная экспериментальная ветка. Если единственная копия работы живёт на вашей машине — перестраивайте историю свободно, ради этого окно до push и существует.
Есть средний случай: вы запушили в свою фичевую ветку, и никто на ней не строится. Force-push после reset там социально допустим, но revert всё равно дешевле по координации. Берегите force-push для веток, которыми владеете в одиночку.
Действительно ли revert безопаснее reset на общих ветках?
Да, причём структурно, а не по традиции:
- Revert порождает обычный коммит: CI по нему пробегает, diff можно ревьюить, а
git logобъяснит вам-в-будущем, почему изменение исчезло. - Reset молча выбрасывает промежуточное состояние. Никто при позднем ревью не увидит, что и когда было отменено, — в логе прямая линия, в которой этого никогда не было.
Два практических острых края:
- Отмена merge-коммита требует
git revert -m 1 <sha>(линия первого родителя). Без-mgit откажется и оставит вас читать ошибку. - Revert — не машина времени: он отменяет diff одного коммита. Если поздние коммиты трогали те же строки, возможны конфликты — это git сообщает, что отмена срослась с дальнейшей работой; полезная информация, а не поломка.
Об отмене именно самого последнего коммита — с сохранением или выбрасыванием правок — разложено в git undo last commit: сохраняем изменения.
Куда вписывается git restore?
git restore (и его пара git switch) приехали в git 2.23, чтобы забрать у reset задачи, которые тот делал плохо из-за перегруза по смыслу:
| Задача | Старый способ | Ясный способ |
|---|---|---|
| Выбросить правки файла в рабочей копии | git checkout -- file / git reset --hard |
git restore file |
| Убрать файл из индекса | git reset HEAD file |
git restore --staged file |
| Передвинуть ветку на другой коммит | git reset <sha> |
git reset <sha> (замены нет — остаётся) |
Современное разделение: restore чинит файлы, reset двигает ветки, revert отменяет опубликованные коммиты. Автодополнение показывает, что ищут «git revert vs reset vs restore» вместе, и не зря — это одна ментальная модель, разрезанная на три команды. Старые формы git checkout работают везде; новые просто не дают отправить целую ветку в шредер, когда вы хотели всего лишь снять файл со стейджинга.
Если цель — скопировать хороший коммит вперёд, а не стереть плохой, это работа cherry-pick: см. git cherry-pick: несколько коммитов, ветки, конфликты.
Краткая шпаргалка по выбору
- Коммит публичный (запушен, другие забрали) →
git revert <sha>. - Коммит только локальный, хотите переделать →
git reset --softили--mixedи перекоммит. - Только локальный, хотите убрать вместе с незакоммиченным бардаком →
git reset --hard, после одного честного взгляда на то, что ещё лежит в рабочей копии. - Испортился один файл →
git restore <file>, ветку не трогаем.
Единственный по-настоящему опасный пункт списка — --hard; всё остальное отыгрывается через git reflog. Необратимые потери в git узки и почти всегда требуют явного набора команды; привычка, которую стоит выстроить, — полсекунды паузы перед --hard, а не отказ от острых инструментов git.