Отмена merge
Однажды мне потребовалось убрать из общей ветки уже опубликованный merge. Переписывать историю было нельзя: коллеги могли успеть получить коммит. Поэтому я использовал git revert, который добавляет новый коммит с обратными изменениями.
Основная команда
git revert -m 1 <merge-commit>У merge commit несколько родителей. Параметр -m здесь означает mainline, а не message:
-m 1— оставить состояние относительно первого родителя;-m 2— оставить состояние относительно второго родителя.
Для обычного merge feature-ветки в main первым родителем чаще всего является прежняя вершина main, поэтому обычно нужен -m 1. Это нужно проверить, а не угадывать.
Как посмотреть родителей
git show --no-patch --pretty=raw <merge-commit>
git log --graph --oneline --decorate --allВ raw-выводе строки parent идут в том порядке, который понимает -m.
parent 1: ветка, на которой выполняли merge
parent 2: ветка, которую присоединилиЕсли нужно проверить изменения до коммита
git revert --no-commit -m 1 <merge-commit>
git diff --staged
git commit--no-commit применяет обратные изменения в index и рабочее дерево, но оставляет создание коммита мне. Это полезно, если нужно разрешить конфликт или исключить часть изменений.
Конфликт во время revert
Если после merge код успел измениться, Git может не суметь автоматически построить обратный patch:
git status
# разрешить конфликт и добавить файлы
git add <files>
git revert --continueОтказаться от операции можно командой:
git revert --abortЧто произошло в моём случае
Я нашёл конкретный merge commit, проверил порядок родителей, применил revert без автоматического коммита и просмотрел diff. После этого создал отдельный коммит с объяснением причины отката.
Мой вклад состоял не в «возврате ветки к старому hash», а в безопасной отмене одного опубликованного изменения с сохранением общей истории.
Важное последствие
Revert merge сообщает Git, что изменения той ветки уже были объединены, а затем отменены. Если снова сделать merge той же неизменённой ветки, старые коммиты могут не вернуться: они уже являются предками main.
Чтобы вернуть отменённые изменения, обычно:
- делают revert от revert-коммита; или
- добавляют новые коммиты в feature-ветку и снова объединяют её.
Когда не нужен reset
git reset --hard передвигает ссылку ветки и меняет рабочее дерево. Для уже опубликованной общей истории это потребовало бы force push и создало бы расхождение у других разработчиков.
Для локального, ещё не опубликованного merge reset может быть уместен. Для общего main безопаснее новый revert commit.
Вопросы для собеседования
Чем revert отличается от reset?
revert сохраняет историю и добавляет обратный коммит. reset передвигает ссылку и может переписать опубликованную историю.
Зачем -m 1?
Чтобы указать, относительно какого родителя merge нужно вычислить обратные изменения.
Можно ли отменить fast-forward merge через -m?
Нет отдельного merge commit с двумя родителями. Нужно отменять конкретные обычные коммиты или диапазон.