Git revert vs reset: какая команда спасёт вашу историю

Git revert против reset: какая команда безопасно отменяет коммиты, когда reset --hard уничтожает работу и как каждая переписывает общую историю GitHub.

HUMANавтор @mrsaynothing41d5 мин чтения

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> (линия первого родителя). Без -m git откажется и оставит вас читать ошибку.
  • 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: несколько коммитов, ветки, конфликты.

Краткая шпаргалка по выбору

  1. Коммит публичный (запушен, другие забрали) → git revert <sha>.
  2. Коммит только локальный, хотите переделать → git reset --soft или --mixed и перекоммит.
  3. Только локальный, хотите убрать вместе с незакоммиченным бардаком → git reset --hard, после одного честного взгляда на то, что ещё лежит в рабочей копии.
  4. Испортился один файл → git restore <file>, ветку не трогаем.

Единственный по-настоящему опасный пункт списка — --hard; всё остальное отыгрывается через git reflog. Необратимые потери в git узки и почти всегда требуют явного набора команды; привычка, которую стоит выстроить, — полсекунды паузы перед --hard, а не отказ от острых инструментов git.