Как отменить несколько коммитов git?


984

У меня есть git-репозиторий, который выглядит так:

A -> B -> C -> D -> HEAD

Я хочу, чтобы глава ветви указывал на A, т.е. я хочу, чтобы B, C, D и HEAD исчезали, и я хочу, чтобы head был синонимом A.

Похоже, я могу либо попытаться перебазировать (неприменимо, так как я поместил изменения между ними), либо вернуться. Но как мне отменить несколько коммитов? Вернуть ли мне по одному? Важен ли порядок?


3
Если вы просто хотите сбросить настройки пульта, вы можете забить его чем угодно! Но давайте воспользуемся четвертым коммитом назад: git push -f HEAD~4:master(при условии, что удаленная ветвь - master). Да, вы можете нажать любой коммит как этот.
u0b34a0f6ae

21
Если люди потянули, вы должны сделать коммит, который отменяет изменения, используя git revert.
Якуб Наребски

1
Используйте git show HEAD ~ 4, чтобы убедиться, что вы
нажимаете направо


5
"Порядок важен?" Да, если коммиты влияют на те же строки в тех же файлах. Затем вы должны начать возвращать самый последний коммит и вернуться обратно.
avandeursen

Ответы:


1337

Расширяя то, что я написал в комментарии

Общее правило заключается в том, что вы не должны переписывать (изменять) историю, которую вы опубликовали, потому что кто-то мог основывать свою работу на этом. Если вы переписываете (изменяете) историю, у вас возникают проблемы со слиянием их изменений и их обновлением.

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

У вас следующая ситуация:

A <- B <- C <- D <- master <- HEAD

(здесь стрелки указывают направление указателя: ссылка «родитель» в случае фиксации, верхняя фиксация в случае заголовка ветви (ссылка на ветвь) и имя ветви в случае ссылки на HEAD).

Вам нужно создать следующее:

A <- B <- C <- D <- [(BCD) ^ - 1] <- master <- HEAD

где «[(BCD) ^ - 1]» означает коммит, который отменяет изменения в коммитах B, C, D. Математика говорит нам, что (BCD) ^ - 1 = D ^ -1 C ^ -1 B ^ -1, поэтому Вы можете получить необходимую ситуацию, используя следующие команды:

$ git revert --no-commit D
$ git revert --no-commit C
$ git revert --no-commit B
$ git commit -m "the commit message"

Альтернативным решением будет извлечение содержимого коммита A и принятие этого состояния:

$ git checkout -f A -- .
$ git commit -a

Тогда у вас будет следующая ситуация:

A <- B <- C <- D <- A '<- master <- HEAD

Коммит А 'имеет то же содержимое, что и коммит А, но это другой коммит (сообщение о коммите, родители, дата коммита).

Решение Джеффа Ферланд, модифицированное Чарльз Бейли основывается на ту же идею, но использует GIT сброс :

$ git reset --hard A
$ git reset --soft @{1}  # (or ORIG_HEAD), which is D
$ git commit

40
Если вы добавили файлы в B, C или D. git checkout -f A -- .Они не будут удалены, вам придется сделать это вручную. Я применил эту стратегию сейчас, спасибо Jakub
Oma

18
Эти решения не эквивалентны. Первый не удаляет вновь созданные файлы.
m33lky

10
@Jerry: git checkout fooможет означать ветку извлечения foo(переключиться на ветку) или файл извлечения foo (из индекса). --используется для устранения неоднозначности, например git checkout -- foo, всегда о файле.
Якуб Наребски

87
В дополнение к отличному ответу. Это сокращение работает для меняgit revert --no-commit D C B
welldan97

9
@ welldan97: Спасибо за комментарий. При написании этого ответа git revertне принимал несколько коммитов; это довольно новое дополнение.
Якуб Наребски

248

Чистый путь, который я нашел полезным

git revert --no-commit HEAD~3..

Эта команда отменяет последние 3 коммита только с одним коммитом.

Также не переписывает историю.


16
Это простой и лучший ответ
Жюльен

3
@JohnLittle это ставит изменения. git commitоттуда будет фактически делать коммит.
x1a4

14
Это не будет работать, если некоторые коммиты являются коммитами слияния.
MegaManX

5
Что делают две точки в конце?
Кардамон

5
@cardamom Те определяют диапазон. HEAD~3..такой же, какHEAD~3..HEAD
Toine H

238

Для этого вам просто нужно использовать команду revert , указав диапазон коммитов, которые вы хотите вернуть.

Принимая во внимание ваш пример, вы должны сделать это (при условии, что вы находитесь на ветке 'master'):

git revert master~3..master

Это создаст новый коммит в вашем local с обратным коммитом B, C и D (это означает, что он отменит изменения, внесенные этими коммитами):

A <- B <- C <- D <- BCD' <- HEAD

129
git revert --no-commit HEAD~2..это немного более идиоматический способ сделать это. Если вы находитесь в основной ветке, нет необходимости указывать мастер снова. --no-commitОпция позволяет GIT попытаться вернуть все коммиты сразу, вместо того , захламление истории с множеством revert commit ...сообщений (при условии , что это то , что вы хотите).
Куби

6
@Victor Я установил диапазон коммитов. Начало диапазона является эксклюзивным, то есть оно не включено. Так что если вы хотите отменить последние 3 фиксаций, вам нужно , чтобы начать диапазон от родителя 3 - го совершения, то есть master~3.

2
@kubi Нет ли способа включить SHA в сообщение фиксации, используя один коммит (ваш метод, но без необходимости вручную вводить коммиты, которые были отменены)?
Крис С

@ChrisS Моей первой мыслью было бы не использовать --no-commit(таким образом, вы получаете отдельный коммит для каждого возврата), а затем раздавить их все вместе в интерактивной перебазировке. Сообщение комбинированной фиксации будет содержать все SHA, и вы можете расположить их так, как вам нравится, с помощью вашего любимого редактора сообщений фиксации.
Радон Росборо

71

Подобно ответу Якуба, это позволяет вам легко выбирать последовательные коммиты для возврата.

# revert all commits from B to HEAD, inclusively
$ git revert --no-commit B..HEAD  
$ git commit -m 'message'

9
Ваше решение работало хорошо для меня, но с небольшой модификацией. Если у нас есть этот случай Z -> A -> B -> C -> D -> HEAD и если я захочу вернуться в состояние A, то странным образом мне придется выполнить git revert --no-commit Z .. ГОЛОВА
Богдан

3
Согласитесь с @Bogdan, диапазон возврата такой: SHA_TO_REVERT_TO..HEAD
Вадим Тюмиров

11
Диапазон неверен. Должно быть B^..HEAD, иначе Б исключен.
Тесс

3
Согласитесь с @tessus, поэтому правильнее всего будет: git revert --no-commit B^..HEADилиgit revert --no-commit A..HEAD
Йохо

64
git reset --hard a
git reset --mixed d
git commit

Это будет действовать как возврат для всех из них одновременно. Дайте хорошее сообщение коммита.


4
Если он хочет HEADвыглядеть так, Aон, вероятно, хочет, чтобы индекс соответствовал, поэтому git reset --soft D, вероятно, более уместен.
CB Bailey

2
- Сброс программного обеспечения не перемещает индекс, поэтому, когда он фиксирует коммит, он будет выглядеть так, как будто коммит был получен непосредственно из а, а не из D. Это разделит ветвь. --mixed оставляет изменения, но перемещает указатель индекса, так что D станет родительским коммитом.
Джефф Ферланд

5
Да, я думаю, что git reset --keep - это именно то, что я имею выше. Он вышел в версии 1.7.1, выпущенной в апреле 2010 года, поэтому ответа не было в то время.
Джефф Ферланд

git checkout Aтогда git commitвыше не работал для меня, но этот ответ сделал.
SimplGy

Почему git reset --mixed Dтребуется? Конкретно почему reset? Это потому, что без сброса на D HEAD будет указывать на A, в результате чего B, C и D будут «зависать» и собирать мусор - а это не то, что он хочет? Но тогда почему --mixed? Вы уже ответили: « --softсброс не перемещает индекс ...». Таким образом, перемещение индекса означает, что индекс будет содержать изменения D, в то время как рабочий каталог будет содержать изменения A - таким образом, a git statusили git diff(что сравнивает индекс [D] с Рабочий каталог [A]) покажет вещество; что пользователь идет от D обратно к A?
Красный горох

39

Сначала убедитесь, что ваша рабочая копия не изменена. Затем:

git diff HEAD commit_sha_you_want_to_revert_to | git apply

а потом просто коммит. Не забудьте документально подтвердить причину возврата.


1
Работал на меня! Возникла проблема с изменениями в ветви функций, внесенными в ветку разработки (некоторые исправления ошибок), так что ветви функций пришлось перезаписывать все изменения, сделанные в разработке (включая удаление некоторых файлов).
сайлсер

1
Не будет работать с бинарными файлами:error: cannot apply binary patch to 'some/image.png' without full index line error: some/image.png: patch does not apply
GabLeRoux

2
Это гораздо более гибкое решение, чем принятый ответ. Спасибо!
Брайан Кунг

2
re: бинарные файлы используют --binary option: git diff --binary HEAD commit_sha_you_want_to_revert_to | git apply
weinerk

2
Это будет работать, даже если вы хотите отменить диапазон коммитов, который содержит коммиты слияния. При использовании git revert A..Zвы получитеerror: commit X is a merge but no -m option was given.
Juliusz Gonera

35

Я так расстроен, что на этот вопрос нельзя просто ответить. Любой другой вопрос касается того, как правильно вернуться и сохранить историю. Этот вопрос говорит: «Я хочу, чтобы руководитель ветви указывал на A, т.е. я хочу, чтобы B, C, D и HEAD исчезали, и я хочу, чтобы head был синонимом A.»

git checkout <branch_name>
git reset --hard <commit Hash for A>
git push -f

Я многому научился, читая пост Якуба, но какой-то парень в компании (с доступом к push в нашу «тестирующую» ветку без Pull-Request) выдвинул, как 5 плохих коммитов, пытаясь исправить и исправить ошибку, которую он совершил 5 коммитов назад. Мало того, но были приняты один или два Запроса на подтягивание, которые теперь были плохими. Так что забудьте, я нашел последний хороший коммит (abc1234) и просто запустил базовый скрипт:

git checkout testing
git reset --hard abc1234
git push -f

Я сказал остальным 5 парням, работающим в этом репо, что они лучше запомнят их изменения за последние несколько часов и Wipe / Re-Branch после последнего тестирования. Конец истории.


2
Я не публиковал коммиты, так что это был ответ, который мне был нужен. Спасибо, @Suamere.
Том Баррон

Лучший способ сделать это - git push --force-with-leaseпереписать историю только в том случае, если никто не подтвердил ветку после или в пределах диапазона коммитов для испарения. Если другие люди использовали ветку, то ее историю никогда не следует переписывать, а фиксация должна быть просто явно изменена.
frandroid

1
@frandroid "история никогда не должна быть переписана", только ситская сделка в абсолютах. Вопрос этой темы и смысл моего ответа именно в том, что для конкретного сценария всю историю следует стереть.
Суамере

@ Суамер Конечно, это вопрос. Но, как говорится в вашем ответе о том, что вы должны были сказать другим парням, есть вероятность неприятностей. Исходя из личного опыта, push -f может испортить вашу кодовую базу, если другие люди сделали то, что вы пытаетесь стереть. --force-with-lease достигает того же результата, за исключением того, что он спасает вашу задницу, если вы собираетесь испортить репо. Зачем рисковать? Если --force-with-lease не удается, вы можете увидеть, какой коммит мешает, правильно оценить, откорректировать и повторить попытку.
frandroid

1
@ Суамере Спасибо! Я согласен, что в вопросе четко говорится, что он хочет переписать историю. Я нахожусь в той же ситуации, что и вы, и я предполагаю, что ОП - это то, что кто-то сделал десятки уродливых возвратов и странных коммитов и возвратов по случайности случайно (пока я был в отпуске), и состояние нужно вернуть. В любом случае наряду с хорошим предупреждением о вреде для здоровья это должен быть принятый ответ.
Ли Ричардсон

9

Это расширение одного из решений, представленных в ответе Якуба.

Я столкнулся с ситуацией, когда коммиты, которые мне нужно было откатить, были несколько сложными, причем некоторые из коммитов были коммитами слияния, и мне нужно было избегать переписывания истории. Я не смог использовать сериюgit revert команд, потому что в итоге столкнулся с конфликтами между добавляемыми изменениями реверсии. Я закончил тем, что использовал следующие шаги.

Сначала проверьте содержимое целевого коммита, оставив HEAD на кончике ветки:

$ git checkout -f <target-commit> -- .

(- удостоверяется <target-commit> интерпретируется как коммит, а не файл;. Относится к текущему каталогу.)

Затем определите, какие файлы были добавлены в откаты коммитов, и, следовательно, должны быть удалены:

$ git diff --name-status --cached <target-commit>

Добавленные файлы должны отображаться с буквой «А» в начале строки, и никаких других отличий не должно быть. Теперь, если необходимо удалить какие-либо файлы, подготовьте эти файлы для удаления:

$ git rm <filespec>[ <filespec> ...]

Наконец, совершите реверсию:

$ git commit -m 'revert to <target-commit>'

При желании убедитесь, что мы вернулись в желаемое состояние:

$git diff <target-commit> <current-commit>

Там не должно быть никаких различий.


Вы уверены, что можете gitГОЛОВАТЬ только с верхушкой ветви?
Суамер

2
Это было гораздо лучшее решение для меня, так как в моем случае были коммиты слияния.
Совэмп

3

Простой способ вернуть группу коммитов в общий репозиторий (который используют люди, и вы хотите сохранить историю) - использовать git revertв сочетании с gitrev-list . Последний предоставит вам список коммитов, первый сделает сам возврат.

Есть два способа сделать это. Если вы хотите отменить несколько коммитов за один коммит:

for i in `git rev-list <first-commit-sha>^..<last-commit-sha>`; do git revert -n $i; done

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

Другой вариант - иметь один коммит на одно возвращенное изменение:

for i in `git rev-list <first-commit-sha>^..<last-commit-sha>`; do git revert --no-edit -s $i; done

Например, если у вас есть дерево коммитов вроде

 o---o---o---o---o---o--->    
fff eee ddd ccc bbb aaa

чтобы отменить изменения от eee до bbb , запустите

for i in `git rev-list eee^..bbb`; do git revert --no-edit -s $i; done

просто использовал это. Спасибо!
Ран Бирон

2

Ни один из них не работал для меня, поэтому у меня было три коммита (последние три коммита), поэтому я сделал:

git revert HEAD
git revert HEAD~2
git revert HEAD~4
git rebase -i HEAD~3 # pick, squash, squash

Работал как шарм :)


2
Это приемлемый вариант, только если ваши изменения еще не внесены.
kboom

0

На мой взгляд, очень простой и чистый способ может быть:

вернуться к

git checkout -f A

направить голову мастера на текущее состояние

git symbolic-ref HEAD refs/heads/master

спасти

git commit

1
Можете ли вы объяснить причину понижения голосов?
nulll

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

Это так же, как git checkout master; git reset --hard A? Или, если нет, не могли бы вы объяснить немного больше о том, что это делает?
MM

просто работает, но символическая ссылка HEAD, похоже, не является "безопасной" командой
Sérgio

я не хочу исправлять мастер, я хочу починить ветку
Sérgio

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.