git revert: безопасная отмена в общей истории

Рейтинг: 67.6% · 8 голосов
Самый подробный курс по Git - от первого коммита до объектной базы и packfiles. Терминал и первый репозиторий, ветки и слияние, конфликты, удалённая работа и форджи (GitHub, GitLab), rebase и переписывание истории, спасение кода через reflog и fsck, stash и worktree, bisect и blame, подмодули и монорепо, хуки и подпись коммитов, внутреннее устройство и масштаб. Для новичков и профи. Актуально на 2026 (Git 2.54, дорожная карта 3.0).
Ответить
Аватара пользователя
Pavel_Git
Сообщения: 52
Зарегистрирован: 11 май 2026, 05:31

git revert: безопасная отмена в общей истории

Сообщение Pavel_Git »

Оглавление курса (52)
  1. Что такое контроль версий и почему победил Git
  2. Установка Git на Windows, macOS и Linux и первая настройка
  3. Терминал и как спрашивать у Git: выживание в командной строке
  4. Ментальная модель Git: три состояния, коммит-снимок и граф
  5. Первый репозиторий: init, add, commit, status
  6. Просмотр изменений: status, diff и индексация по частям
  7. История проекта: git log, диапазоны и поиск
  8. .gitignore: что не должно попасть в репозиторий
  9. .gitattributes: окончания строк, бинарники и атрибуты
  10. Ветки как указатели: branch, switch и detached HEAD
  11. Слияние веток: fast-forward против трёхстороннего merge
  12. Конфликты слияния: анатомия и уверенное разрешение
  13. Практикум: твой первый день с Git от нуля до push
  14. Удалённые репозитории: remote, fetch, базовый pull и refspec
  15. git push: безопасная отправка и защита от перезаписи
  16. Клонирование: shallow, partial clone, bundle и backfill
  17. GitHub, GitLab и форки: pull request, rulesets и защита веток
  18. Рабочие процессы команды: GitFlow, GitHub Flow, trunk-based
  19. Merge queue и merge trains: безопасное вливание в trunk
  20. Культура code review и Conventional Commits
  21. git reset: три дерева и отмена индексации и коммитов
  22. git revert: безопасная отмена в общей истории (вы здесь)
  23. Правка последнего коммита: git commit --amend
  24. git rebase: пересадка истории и золотое правило
  25. Интерактивный rebase: переписать серию коммитов
  26. git cherry-pick: перенос отдельных коммитов
  27. Stacked diffs: цепочка зависимых PR
  28. Массовое переписывание истории: git filter-repo и git replace
  29. git reflog: журнал, который помнит всё
  30. Восстановление утерянных коммитов, веток и файлов
  31. Откат опасных операций: reset --hard, сломанный rebase, плохой merge
  32. git stash: тайник для незавершённой работы
  33. git worktree: несколько рабочих деревьев одного репозитория
  34. git bisect: бинарный поиск регрессии
  35. git blame, mailmap и археология кода
  36. Теги, релизы, archive и bundle
  37. Подмодули: внешние репозитории внутри проекта
  38. Subtree, монорепозитории и sparse-checkout
  39. Git LFS и большие файлы
  40. Вспомогательные команды: clean, mv, rm, restore
  41. Псевдонимы и кастомизация .gitconfig
  42. Хуки Git: автоматизация на клиенте и сервере
  43. Подпись коммитов и тегов: SSH, GPG и Sigstore
  44. Внутреннее устройство I: объектная база Git
  45. Внутреннее устройство II: ссылки, HEAD, notes и reftable
  46. Внутреннее устройство III: индекс, packfiles, gc и обслуживание
  47. От SHA-1 к SHA-256: переход на новый хеш
  48. Огромные репозитории: partial clone, FSMonitor, Scalar
  49. Git в CI/CD: shallow, кеш, merge_group и безопасность
  50. Куда движется Git: дорожная карта 3.0 и экосистема
  51. Профессиональный setup: собираем рабочее окружение Git
  52. 30+ типичных ошибок Git и как из них выбираться
Боль: плохой коммит уже в main, и все запулили

Картина из жизни. Кто-то влил в main коммит, который роняет прод: или баг, или случайно закоммитили .env с секретами, или удалили миграцию. Пока ты это заметил, десять человек уже сделали git pull. И тут у новичка дрожит рука потянуться к git reset --hard или git rebase, чтобы "стереть как будто не было". Не делай так. Это самый быстрый способ устроить катастрофу всей команде.

Почему? Потому что reset и rebase переписывают историю - меняют хеши коммитов. Локально это норма. Но если коммит уже опубликован и его утянули другие, ты создаёшь две расходящиеся версии истории. Их push упрётся в твою, твой - в их, начнётся война force-push, и кто-то обязательно затрёт чужую работу. Для уже опубликованной истории есть честный, скучный, безопасный инструмент - git revert. Он не врёт и не прячет, он добавляет.

В этом уроке разберём, как git revert отменяет коммит, не ломая ничего у коллег, почему откатить изменения git через revert безопасно, и самое тонкое - как git revert merge коммита работает с выбором родителя и какие там грабли. Опорно это тема главы 7 второй книги, но смотрим глубже и руками.

Изображение

Что на самом деле делает git revert

Ключевая мысль, которую надо вбить в голову раз и навсегда: git revert не удаляет коммит. Он создаёт новый коммит, который вносит обратные изменения. Если исходный коммит добавил строку, revert-коммит её удаляет. Если исходный удалил файл - revert его вернёт. Старый коммит остаётся в истории на своём месте, его хеш не меняется, ничей pull не сломается.

Механически revert берёт diff целевого коммита (разницу между ним и его родителем), инвертирует его и применяет на твоё текущее состояние (рабочую копию плюс индекс), как обычный патч. По сути это git diff C..C^ примённый поверх HEAD. Поэтому отменить коммит git через revert - это операция вперёд по времени, а не назад: история только растёт.

Код: Выделить всё

*   e4a91c2 (HEAD -> main) Revert "Add broken cache layer"
*   9f3bd07 Add unrelated feature
*   1c2d3e4 Add broken cache layer   <- остаётся как был
*   abc1234 Initial setup
Видишь: коммит 1c2d3e4 никуда не делся. Сверху лёг новый e4a91c2, который его нейтрализует. Любой коллега делает git pull, получает один новый коммит сверху - и всё сходится без конфликтов истории.

Золотое правило: public -> revert, local -> reset

Запомни границу, она решает 90 процентов вопросов "как откатить изменения git".
  • Коммит уже опубликован (запушен в общую ветку, его могли утянуть другие) - только git revert. Историю не трогаем, добавляем компенсирующий коммит.
  • Коммит ещё локальный (только у тебя, не пушен или пушен в личную ветку, которую никто не трогает) - можно git reset, git rebase, amend. Переписывай как хочешь, ты никому не мешаешь.
Простой тест: "Мог ли кто-то уже забрать этот коммит себе?" Если да - revert. Если железно нет - выбирай инструмент по вкусу. reset делает историю чище (как будто плохого коммита не было), revert честнее (видно, что был откат), но на публичной ветке выбора нет: чистота не стоит сломанных репозиториев у всей команды.

Практика: отменяем обычный коммит

Берём самый частый случай - один плохой коммит в main, который уже у всех. Находим его хеш и реверчим.

Код: Выделить всё

$ git log --oneline -3
9f3bd07 (HEAD -> main, origin/main) Add unrelated feature
1c2d3e4 Add broken cache layer
abc1234 Initial setup

$ git revert 1c2d3e4
Откроется редактор с заготовкой сообщения. По умолчанию там

Код: Выделить всё

Revert "Add broken cache layer"
и строка

Код: Выделить всё

This reverts commit 1c2d3e4...
- не удаляй её, по ней потом легко найти, что и зачем откатывали. Сохраняем, выходим:

Код: Выделить всё

$ git revert 1c2d3e4
[main e4a91c2] Revert "Add broken cache layer"
 1 file changed, 0 insertions(+), 14 deletions(-)

$ git push
Готово. 14 строк, которые добавлял плохой коммит, удалены новым коммитом. Прод чинится, история цела, коллеги делают pull и получают фикс. Это и есть git revert commit в чистом виде.

Полезные флаги:
  • git revert --no-edit - не открывать редактор, взять сообщение по умолчанию. Удобно в скриптах и когда правишь второпях.
  • git revert --abort - всё пошло не так (конфликт, передумал) - откатить саму операцию revert к исходному состоянию.
  • git revert --continue - после ручного разрешения конфликта продолжить.
Да, revert может конфликтовать. Если строки, которые он хочет убрать, уже изменены последующими коммитами, Git попросит разрешить конфликт руками - ровно как при merge. Разрулил, git add, git revert --continue.

Самое тонкое: git revert merge и выбор родителя -m

Вот где спотыкаются даже опытные. Обычный коммит имеет одного родителя, и "обратный diff" очевиден. А merge-коммит имеет двух родителей (а octopus-merge и больше). Какой из них считать "линией, к которой возвращаемся"? Git сам решить не может и без подсказки откажется:

Код: Выделить всё

$ git revert 7c0ffee
error: commit 7c0ffee is a merge but no -m option was given.
fatal: reverting is not possible because you have not given a mainline.
Нужен флаг -m (mainline) с номером родителя - какую ветку оставить как основную линию. Нумерация с единицы, в порядке, в каком они записаны в merge-коммите:
  • -m 1 - первый родитель. Это ветка, в которую вливали (обычно main). Берёшь её за основу, выкидываешь всё, что принесла влитая ветка. Это нужно в 99 процентах случаев.
  • -m 2 - второй родитель, влитая feature-ветка. Используется редко.
Чтобы не гадать, посмотри родителей явно:

Код: Выделить всё

$ git show 7c0ffee --no-patch --format="%h parents: %p"
7c0ffee parents: 9f3bd07 5e1c0de
Первый (9f3bd07) - это кончик main до слияния, второй (5e1c0de) - кончик feature. Хочешь "как будто feature не вливали" - оставляешь main как mainline:

Код: Выделить всё

$ git revert -m 1 7c0ffee
[main 0bad10a] Revert "Merge feature/cache into main"
 3 files changed, 0 insertions(+), 120 deletions(-)
Revert-коммит убирает все изменения, которые feature-ветка принесла относительно main. Прод чист, история линейно растёт.

Подводный камень: как потом снова влить ту же ветку

А вот это - грабли, которые портят людям дни. Ты отменил merge через revert -m 1. Позже команда чинит feature-ветку и хочет влить её снова. Делаешь git merge feature - и Git заявляет "Already up to date" или вливает только новые коммиты, а старые исправления feature не появляются. Почему?

Потому что для Git ветка feature уже смерджена - merge-коммит на месте, он в истории. Git считает её содержимое уже учтённым. Но твой revert-коммит затем выкинул это содержимое. Git этого "выкидывания" при следующем merge не учитывает - он смотрит только на граф предков, а по графу feature давно влита. Итог: повторный merge не вернёт откаченные изменения.

Правильный путь - отменить сам revert (revert of a revert) перед новым слиянием:

Код: Выделить всё

$ git revert 0bad10a
[main 2f0 recover] Revert "Revert "Merge feature/cache into main""
Это возвращает содержимое feature обратно в main. Дальше уже делаешь обычный merge, чтобы подтянуть новые исправления ветки. Звучит как масло масляное ("отменить отмену"), но это штатная и документированная практика - именно так Git предлагает повторно вливать ветку после revert merge. Альтернатива чище: не реверти merge вовсе, а заведи новую ветку от исправного состояния и мерджи её. Но если merge уже отменён через revert, дорога одна - revert этого revert.

Запомни как мантру: git revert merge не "размерживает" ветку, он лишь снимает её эффект. Граф предков остаётся, и Git считает ветку влитой навсегда.

Откат диапазона и пакетная отмена через --no-commit

Иногда плохих коммитов несколько подряд. Реверт умеет диапазоны - синтаксис тот же, что у git log, и порядок отмены Git сам делает обратным (сначала новейший), чтобы патчи легли чисто:

Код: Выделить всё

$ git revert abc1234..9f3bd07
Это отменит каждый коммит в (abc1234, 9f3bd07] отдельным revert-коммитом. Левая граница исключается - сам abc1234 не трогается, только то, что после него. Получишь по одному компенсирующему коммиту на каждый исходный - удобно для аудита, видно что и когда откатывали.

Не хочешь россыпь коммитов, а один общий откат - флаг --no-commit (короткий -n). Он применяет инверсии в индекс и рабочую копию, но коммит не делает - ты сам собираешь один:

Код: Выделить всё

$ git revert --no-commit abc1234..9f3bd07
$ git revert --no-commit 1c2d3e4
$ git status
$ git commit -m "Revert broken cache rollout (3 commits)"
Накопил все нужные отмены, проверил git status, собрал одним осмысленным коммитом с понятным сообщением. После --no-commit, если передумал, не забудь git revert --abort или git reset, иначе изменения зависнут в индексе.

Типичные грабли
  • Реверт на публичной ветке через reset. Самая дорогая ошибка. reset --hard на запушенном коммите плюс force-push затирает чужую работу и разносит историю. На общей ветке - только revert.
  • Забыли -m на merge. Git честно откажется и подскажет. Не лепи -m 1 наугад - сперва git show ...--format="%p" и убедись, какой родитель main.
  • Перепутали -m 1 и -m 2. Реверт "не того" родителя оставит в main мусор вместо чистки. Если уже закоммитил и запушил - реверти этот revert и делай заново с правильным mainline.
  • Ждут, что revert merge даст повторно влить ветку. Нет. Нужен revert of revert (см. выше), иначе исправления феатуры не вернутся.
  • Реверт не убирает секрет из истории. Если в плохом коммите утёк токен, revert уберёт его из текущего состояния, но сам секрет остаётся в старом коммите навсегда и виден в git log -p. Тут revert недостаточно - нужно переписывание истории через git filter-repo (сторонний инструмент) или BFG, force-with-lease и обязательно ротация утёкшего секрета. Считай засветившийся токен скомпрометированным.
Мини-лаба: повтори руками

Собери учебный репозиторий и проиграй оба сценария - обычный коммит и merge.

Код: Выделить всё

mkdir revert-lab && cd revert-lab
git init -b main
echo "ok" > app.txt && git add . && git commit -m "init"

# плохой обычный коммит
echo "BROKEN" >> app.txt && git commit -am "broken change"
git log --oneline
git revert --no-edit HEAD
cat app.txt          # строки BROKEN уже нет
git log --oneline    # init, broken change, Revert "broken change"

# теперь merge-сценарий
git switch -c feature
echo "feature" >> app.txt && git commit -am "feature work"
git switch main
git merge --no-ff feature -m "Merge feature"
git show HEAD --no-patch --format="%h parents: %p"   # увидишь двух родителей
git revert -m 1 --no-edit HEAD                       # откат merge по mainline=main
cat app.txt          # строки feature нет

# повторно вливаем feature правильно: revert of revert
git revert --no-edit HEAD   # отменяем сам revert merge
cat app.txt          # строка feature вернулась
Обрати внимание на последний шаг: обычный git merge feature после отката merge ничего бы не вернул, а revert of revert - вернул. Это и есть ядро тонкости git revert merge.

Контрольные вопросы
  • Коллега запушил баг в main, его уже все запулили. Почему git reset --hard плюс force-push - плохая идея, а git revert - хорошая?
  • Что выведет git log после git revert старого коммита: исходный коммит исчезнет или нет, и сколько коммитов добавится?
  • Откатываешь merge-коммит. Что означает -m 1, как узнать, какой родитель main, и что будет, если ошибиться номером?
  • Ты отменил merge через revert -m 1. Команда дописала feature-ветку. Почему обычный git merge feature не вернёт изменения и как влить её правильно?
Итог

git revert - твой штатный инструмент отмены в общей истории: он не переписывает прошлое, а добавляет коммит-компенсацию, поэтому безопасен для всех, кто уже сделал pull. Золотое правило простое: публичная история - revert, локальная - можно reset или rebase. Для merge-коммита всегда указывай родителя через -m (почти всегда -m 1, это main), а если потом понадобится снова влить ту же ветку - отменяй сам revert, потому что граф предков по-прежнему считает ветку влитой. Диапазоны и --no-commit дают гибкость от поштучного аудита до одного общего отката. И помни: revert чистит текущее состояние, но не стирает прошлое - для утёкших секретов нужны другие инструменты и ротация.
👍3 ❤️3 🔥3 😄 🤔
Аватара пользователя
nucja19
Сообщения: 1
Зарегистрирован: 14 май 2026, 17:52

Re: git revert: безопасная отмена в общей истории

Сообщение nucja19 »

Долго не мог понять почему после revert merge ветка не вливается заново. revert of revert реально спас, спасибо что разжевали с -m 1.
👍2 ❤️2 🔥 😄 🤔
Аватара пользователя
curepipe
Сообщения: 1
Зарегистрирован: 25 май 2026, 19:09

Re: git revert: безопасная отмена в общей истории

Сообщение curepipe »

А если плохих коммитов штук пять подряд в main - проще диапазон revert-нуть или по одному? И --no-commit тут норм заходит?
👍 ❤️1 🔥1 😄 🤔
Ответить
← Предыдущая глава
git reset: три дерева и отмена индексации и коммитов
Следующая глава →
Правка последнего коммита: git commit --amend

Все главы курса «Git профессионально: от первого коммита до внутреннего устройства»

Поделиться темой: ✈ Telegram VK
Похожие запросы: git reset: отменить коммит и измененияgit filter-repo: удалить файл из истории

Вернуться в «Git профессионально: от первого коммита до внутреннего устройства»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя