Revert — безопасная отмена
git revert отменяет коммит, добавляя НОВЫЙ коммит-инверсию, оставляя историю нетронутой — поэтому он безопасен на общих, запушенных ветках, где reset небезопасен. Откат мержа требует -m 1 для выбора магистрального родителя, а --no-commit пакетирует несколько откатов.
На main только что прилетел плохой коммит, и десять коллег уже его подтянули. Ты хочешь его отменить — но если ты отмотаешь историю через git reset, репозиторий каждого теперь расходится с твоим, и следующий, кто запушит, заново внесёт плохой коммит. Есть отмена, придуманная ровно для этой ситуации: git revert. Вместо того чтобы стирать прошлое, она пишет новый коммит, который гасит старый.
К концу этого урока ты сможешь выбирать между revert и reset исходя из того, общая ли история, и корректно откатывать обычные коммиты и даже мерж-коммиты.
После этого урока ты сможешь объяснить, как git revert создаёт инвертирующий коммит, сохраняющий историю, обосновать, почему это делает его безопасным на общих ветках, противопоставить его git reset и откатить мерж-коммит с помощью -m 1.
git revert отменяет коммит, добавляя новый коммит, а не удаляя старый. По данному коммиту git вычисляет точно противоположное изменение — каждая добавленная строка становится удалённой и наоборот — и записывает это как свежий коммит поверх твоей ветки:
git revert <commit>Исходный коммит остаётся в истории, полностью виден в git log. Под ним теперь сидит «инвертирующий» коммит, чистый эффект которого его отменяет. Итоговое состояние твоих файлов соответствует «как будто этого коммита не было», но запись и об изменении, и о его отмене сохраняется.
Поскольку он только добавляет коммиты, revert никогда не переписывает общую историю — именно это делает его безопасным. git reset двигает указатель ветки назад и отбрасывает коммиты; если ты уже запушил, у каждого, кто подтянул, теперь есть коммиты, которые твоя ветка больше не признаёт, и согласование этого требует форс-пушей и координации. revert всегда двигает ветку только вперёд новым коммитом, поэтому каждый другой клон нормально перематывается вперёд при следующем pull. Вот и всё правило:
revert -> безопасен на публичных / запушенных ветках (история сохранена)
reset -> только для локальной, незапушенной истории (история переписана)Откат мерж-коммита требует -m, чтобы выбрать, какая сторона «магистральная». У обычного коммита один родитель, поэтому его инверсия однозначна. У мерж-коммита два родителя, и git не может угадать, какую линию истории ты хочешь сохранить. Ты говоришь ему через -m (номер родителя, считая с 1):
git revert -m 1 <merge-commit>-m 1 означает «считай родителя 1 — ветку, в которую ты вливал, обычно main, — магистралью и отмени всё, что принесла влитая ветка». Опусти -m на мерже — и git откажется с ошибкой, а не станет угадывать. Выбор не того родителя откатит противоположный набор изменений, так что сначала подтверди через git show <merge-commit>.
▸Граничные случаи
У отката мержа есть тонкая ловушка: он отменяет код, который мерж внёс, но git всё равно помнит, что ветка была влита. Если потом ты заново вольёшь ту же фичевую ветку, чтобы «вернуть работу», git не увидит ничего нового для слияния, и код не вернётся. Стандартное решение — откатить откат (git revert <the-revert-commit>), когда ты готов восстановить фичу, а не вливать заново. Это известный острый угол, который стоит запомнить.
--no-commit позволяет собрать несколько откатов в один коммит. По умолчанию каждый revert делает свой коммит. Чтобы отменить серию коммитов и записать совокупную отмену как один коммит, подготовь их без коммита, затем закоммить один раз:
git revert --no-commit <commit-a> <commit-b> <commit-c>
git commit -m "Revert the failed rollout (3 commits)"Каждый revert с --no-commit применяет свою инверсию к индексу и рабочему дереву, но останавливается перед коммитом, так что они накапливаются. Это сохраняет историю читаемой, когда несколько коммитов составили одну логическую ошибку.
Отменить ломающий деплой коммит на общем main.
Коммит 9f1c2ab («Switch to new auth client») уехал в main, был подтянут командой и теперь роняет прод. Ты должен отменить его, не переписывая историю, которая есть у всех остальных.
git log --oneline -3
# 9f1c2ab Switch to new auth client <- виновник
git revert 9f1c2ab
# git открывает редактор с заранее заполненным сообщением:
# Revert "Switch to new auth client"
# This reverts commit 9f1c2ab...
git pushPush чисто перематывается вперёд для всех — их следующий git pull просто добавляет твой revert-коммит сверху. Прод починен, а git log по-прежнему показывает и исходное изменение, и его откат, так что история того, что пошло не так и как это отменили, цела. Если бы ты вместо этого запустил git reset --hard 9f1c2ab^ и форс-пушнул, ветка каждого коллеги разошлась бы, и следующий неосторожный push мог бы воскресить баг.
▸Почему это работает
Почему переписывание общей истории так вредно, механически? Идентичность каждого коммита (его SHA) включает его родителя. Отбрось или перепиши коммит — и каждый коммит после него получает новый SHA. У любого, кто основал работу на старых SHA, теперь ветка, не имеющая общей вершины с твоей; git трактует это как расхождение и выдаёт запутанные конфликты слияния или дублированные коммиты. Revert обходит всё это, никогда не меняя существующий SHA, — он только добавляет новые.
Плохой коммит уже запушен на общую ветку, которую коллеги подтянули. Какая отмена безопасна и почему?
git revert отменяет коммит, дописывая новый инвертирующий коммит, оставляя исходный — и всю окружающую историю — нетронутыми. Поскольку он двигает ветку только вперёд, он безопасен на общих, запушенных ветках, где git reset (который переписывает историю) вызвал бы расхождение у всех. Правило: revert для публичной истории, reset для только-локальной истории. Откат мерж-коммита требует -m 1, чтобы назвать магистрального родителя, а --no-commit пакетирует несколько откатов в один. Дальше ты изучишь git reflog — страховочную сетку, восстанавливающую коммиты даже после reset, о котором ты пожалел.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.