Реальный случай. Один разработчик трижды слил ветку task5 c задачей в мастер и отправил изменения в репозиторий в origin/master. Спустя 3 дня другой разработчик начинает готовить релиз, находит эту ветку в мастере и понимает, что с этой веткой task5 в мастере выкатываться нельзя. Эти коммиты нужно убрать, а ветку task5 вернуть на доработку.

Навали git push -f

Все осложняется тем, что task5 лежит в мастере уже 3 дня и другие разработчики могли обновить свои локальные мастера и получить этот злополучный task5 в свой мастер и свои новосозданные ветки, ответвленные от мастера. Это значит, что если просто удалить коммиты и перезаписать историю через git push -f, то любой разработчик, кто уже подсосал изменения вновь их запушит на сервер, сам того не подозревая.

Давай git revert

Плюс как я уже писал выше, есть merge-коммиты, которые также нужно отменить. А merge-коммиты, как известно имеют двух родителей, и такой коммит нельзя просто отменить командой git revert, как обычный коммит. Нужно явно указывать, какого именно родителя мы хотим отменить:

git revert abc123de -m 1
# или
git revert abc123de -m 2

И тонкость здесь в том, что если при сливании ветки разработчик разрешал некоторые конфликты, то для того, чтобы отменить merge, эти конфликты нужно разрешать в обратном порядке. В итоге, спустя несколько часов, мне так и не удалось в помощью git revert вернуть ветку в нужное состояние, все время были какие-то отличия.

Решение

В итоге было найдено решение с cherry-pick. Сперва мы все же перезаписываем мастер:

git reset --hard master~3
git push -f

После этого все ветки всех разработчиков, которые содержат коммиты task5 нужно пересоздать. Каждый разработчик обновляет перезаписанный мастер: git fetch

Находит список коммитов в своей ветке:

git log master..task12 --oneline --date-order --reverse
> e2cbdab commit message...
> 66f27c9 commit message...
> 96b4af3 commit message...
> 5d073df commit message...
> 6bfadbb commit message...

После этого находим те коммиты, которые сделал разработчик в текущей ветке (первые три коммита в нашей ситуации будут из ветки task5), и переносим их в отдельную ветку.

git checkout -b task12_new      # Создаем новую чистую ветку
git cherry-pick 5d073df 6bfadbb # Переносим все коммиты из старой ветки
                                # в той последовательности, в которой были созданы
                                # На этом этапе могут возникнуть конфликты, это вполне нормально,
                                # разрешаем их
git branch -D task12            # После этого можно удалить старую ветку
git push origin :task12         # Отправляем удаление
git branch -m task12_new task12 # И переименовываем ветку обратно

Дополнительно

Получить список веток, которые нужно обновить (нужен список хешей коммитов, от которых избавляемся):

git branch --contains b2cbdab; git branch --contains b6f27c9; git branch --contains d6b4af3

Список коммитов в текущей ветке через пробел:

printf -- "%s " `git log master.. --oneline --pretty=format:"%h" --date-order --reverse`

К недостаткам данного метода можно отнести то, что это должен делать каждый разработчик со всеми своими ветками. Если кто-то этого не сделает, или пропустит ветку, то позже благополучно запушит изменения в мастер.

Скорее всего нечто подобное можно реализовать и через git revert и через git rebase, но кунг-фу автора не хватило, чтобы осилить эти способы. git cherry-pick был последним вариантом, который мы пробовали, и который почти сразу у нас получился.