¿Cómo revertir múltiples confirmaciones de git?

984

Tengo un repositorio git que se ve así:

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

Quiero que la cabeza de la rama apunte a A, es decir, quiero que B, C, D y HEAD desaparezcan y quiero que cabeza sea sinónimo de A.

Parece que puedo intentar cambiar la base (no se aplica, ya que he introducido cambios en el medio) o revertir. Pero, ¿cómo revierto las confirmaciones múltiples? ¿Revierto uno a la vez? ¿Es importante el orden?

Cuenta
fuente
3
Si solo desea restablecer el control remoto, ¡puede golpearlo con cualquier cosa! Pero usemos el cuarto commit hace: git push -f HEAD~4:master(suponiendo que la rama remota es master). Sí, puedes empujar cualquier commit así.
u0b34a0f6ae
21
Si la gente se ha retirado, debe realizar una confirmación que revierta los cambios utilizando git revert.
Jakub Narębski
1
Use git show HEAD ~ 4 para asegurarse de que está presionando hacia la derecha en el control remoto
Mâtt Frëëman
1
Posible duplicado de ¿Cómo deshacer los últimos commit (s) en Git?
Jim cayó
55
"¿Es importante la orden?" Sí, si las confirmaciones afectan las mismas líneas en los mismos archivos. Luego, debe comenzar a revertir la confirmación más reciente y volver a trabajar.
avandeursen

Respuestas:

1336

Expandiendo lo que escribí en un comentario

La regla general es que no debe reescribir (cambiar) el historial que ha publicado, porque alguien podría haber basado su trabajo en él. Si reescribe (cambia) el historial, tendrás problemas para fusionar sus cambios y actualizarlos.

Entonces, la solución es crear una nueva confirmación que revierta los cambios de los que desea deshacerse. Puede hacer esto usando el comando git revert .

Tienes la siguiente situación:

A <- B <- C <- D <- maestro <- CABEZA

(las flechas aquí se refieren a la dirección del puntero: la referencia "principal" en el caso de commits, el commit superior en el caso de branch head (branch ref) y el nombre de branch en el caso de HEAD reference).

Lo que necesita crear es lo siguiente:

A <- B <- C <- D <- [(BCD) ^ - 1] <- maestro <- CABEZA

donde "[(BCD) ^ - 1]" significa la confirmación que revierte los cambios en las confirmaciones B, C, D. Las matemáticas nos dicen que (BCD) ^ - 1 = D ^ -1 C ^ -1 B ^ -1, entonces Puede obtener la situación requerida utilizando los siguientes comandos:

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

La solución alternativa sería retirar el contenido de la confirmación A y confirmar este estado:

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

Entonces tendría la siguiente situación:

A <- B <- C <- D <- A '<- maestro <- CABEZA

La confirmación A 'tiene el mismo contenido que la confirmación A, pero es una confirmación diferente (mensaje de confirmación, padres, fecha de confirmación).

La solución de Jeff Ferland, modificada por Charles Bailey, se basa en la misma idea, pero usa git reset :

$ git reset --hard A
$ git reset --soft @{1}  # (or ORIG_HEAD), which is D
$ git commit
Jakub Narębski
fuente
40
Si agregó archivos en B, C o D. git checkout -f A -- .no los eliminará, deberá hacerlo manualmente. Apliqué esta estrategia ahora, gracias Jakub
oma
18
Esas soluciones no son equivalentes. El primero no elimina los archivos recién creados.
m33lky
10
@Jerry: git checkout foopuede significar la rama de pago foo(cambiar a rama) o el archivo de pago foo (del índice). --se utiliza para desambiguar, por ejemplo, git checkout -- foosiempre se trata del archivo
Jakub Narębski
87
Además de gran respuesta. Esta abreviatura funciona para mígit revert --no-commit D C B
welldan97
99
@ welldan97: Gracias por un comentario. Al escribir esta respuesta git revertno aceptaba múltiples confirmaciones; Es una adición bastante nueva.
Jakub Narębski
248

Manera limpia que encontré útil

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

Este comando revierte los últimos 3 commits con solo un commit.

Tampoco reescribe la historia.

bucear profundo
fuente
16
Esta es la simple y mejor respuesta
Julien Deniau
3
@JohnLittle organiza los cambios. git commita partir de ahí en realidad hará el commit.
x1a4
14
Esto no funcionará si algunos commits son commits de fusión.
MegaManX
55
¿Qué hacen los dos puntos al final?
cardamomo
55
@cardamom Esos especifican un rango. HEAD~3..es lo mismo queHEAD~3..HEAD
Toine H
238

Para hacerlo, solo tiene que usar el comando revertir , especificando el rango de confirmaciones que desea revertir.

Teniendo en cuenta su ejemplo, tendría que hacer esto (suponiendo que esté en la rama 'maestro'):

git revert master~3..master

Esto creará una nueva confirmación en su local con la confirmación inversa de B, C y D (lo que significa que deshacerá los cambios introducidos por estas confirmaciones):

A <- B <- C <- D <- BCD' <- HEAD
Víctor
fuente
129129
git revert --no-commit HEAD~2..es una forma un poco más idiomática de hacerlo. Si está en la rama maestra, no es necesario especificarla nuevamente. La --no-commitopción le permite a git intentar revertir todas las confirmaciones a la vez, en lugar de llenar el historial con varios revert commit ...mensajes (suponiendo que eso sea lo que desea).
kubi
66
@Victor Arreglé tu rango de confirmación. El comienzo de la gama es exclusivo, lo que significa que no está incluido. Entonces, si desea revertir los últimos 3 commits, debe comenzar el rango desde el padre del 3er commit, es decir master~3.
2
@kubi ¿no hay forma de incluir los SHA en un mensaje de confirmación, utilizando una única confirmación (su método pero sin tener que ingresar manualmente las confirmaciones que se revertieron)?
Chris S
@ChrisS Mi primer pensamiento sería no usar --no-commit(para obtener una confirmación por separado para cada reversión), y luego juntarlos todos en una nueva versión interactiva. El mensaje de confirmación combinado contendrá todos los SHA, y usted puede organizarlos como quiera usando su editor de mensajes de confirmación favorito.
Radon Rosborough
71

Similar a la respuesta de Jakub, esto le permite seleccionar fácilmente confirmaciones consecutivas para revertir.

# revert all commits from B to HEAD, inclusively
$ git revert --no-commit B..HEAD  
$ git commit -m 'message'
Konyak
fuente
99
Su solución funcionó bien para mí, pero con una ligera modificación. Si tenemos este caso Z -> A -> B -> C -> D -> HEAD y si quisiera volver al estado A, entonces extrañamente tendría que ejecutar git revert --no-commit Z .. CABEZA
Bogdan
3
De acuerdo con @Bogdan, el rango de reversión es así: SHA_TO_REVERT_TO..HEAD
Vadym Tyemirov
11
El rango está mal. Debería ser B^..HEAD, de lo contrario B está excluido.
tessus
3
De acuerdo con @tessus, así que lo correcto sería: git revert --no-commit B^..HEADogit revert --no-commit A..HEAD
Yoho
64
git reset --hard a
git reset --mixed d
git commit

Eso actuará como una reversión para todos ellos a la vez. Dar un buen mensaje de compromiso.

Jeff Ferland
fuente
44
Si quiere HEADlucir así A, probablemente quiera que el índice coincida, por git reset --soft Dlo que probablemente sea más apropiado.
CB Bailey
2
- el reinicio suave no mueve el índice, por lo que cuando se compromete, parecería que el compromiso vino directamente de un en lugar de D. Eso haría que la rama se dividiera. --mixed deja los cambios, pero mueve el puntero de índice, por lo que D se convertirá en la confirmación principal.
Jeff Ferland el
55
Sí, creo que git reset: mantener es exactamente lo que tengo arriba. Salió en la versión 1.7.1, lanzada en abril de 2010, por lo que la respuesta no estaba disponible en ese momento.
Jeff Ferland
git checkout Aentonces git commitarriba no funcionó para mí, pero esta respuesta sí.
SimplGy
¿Por qué se git reset --mixed Drequiere? Específicamente por qué reset? ¿Es porque, sin reiniciar a D, que HEAD apuntaría a A, haciendo que B, C y D se "cuelguen" y recojan basura, que no es lo que él quiere? Pero entonces ¿por qué --mixed? Usted ya respondió " --softrestablecer no mueve el índice ..." Entonces, al mover el índice, esto significa que el índice contendrá los cambios de D, mientras que el directorio de trabajo contendrá los cambios de A, de esta manera a git statuso git diff(que compara el índice [D] con El Directorio de trabajo [A]) mostrará la sustancia; ese usuario va de D a A?
The Red Pea
39

Primero asegúrese de que su copia de trabajo no esté modificada. Entonces:

git diff HEAD commit_sha_you_want_to_revert_to | git apply

y luego solo comprometerse. No olvide documentar cuál es el motivo de la reversión.

mateusz.fiolka
fuente
1
¡Trabajó para mi! Tuve un problema con los cambios obsoletos de la rama de características realizados en la rama de desarrollo (algunas correcciones de errores), por lo que la rama de características tuvo que sobrescribir todos los cambios realizados en el desarrollo (incluida la eliminación de algunos archivos).
Silentier
1
No funciona con archivos binarios:error: cannot apply binary patch to 'some/image.png' without full index line error: some/image.png: patch does not apply
GabLeRoux
2
Esta es una solución mucho más flexible que la respuesta aceptada. ¡Gracias!
Brian Kung
2
re: uso de archivos binarios - opción binaria: git diff --binary HEAD commit_sha_you_want_to_revert_to | git apply
weinerk
2
Esto funcionará incluso si desea revertir un rango de confirmaciones que contiene confirmaciones de fusión. Al usarlo git revert A..Zobtendríaserror: commit X is a merge but no -m option was given.
Juliusz Gonera
35

Estoy tan frustrado que esta pregunta no puede ser respondida. Cualquier otra pregunta está relacionada con cómo revertir correctamente y preservar la historia. Esta pregunta dice "Quiero que la cabeza de la rama apunte a A, es decir, quiero que B, C, D y HEAD desaparezcan y quiero que cabeza sea sinónimo de A."

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

Aprendí mucho leyendo la publicación de Jakub, pero un tipo de la compañía (con acceso para enviar a nuestra rama de "prueba" sin Pull-Request) presionó como 5 errores cometidos tratando de arreglar y corregir y corregir un error que cometió hace 5 compromisos. No solo eso, sino que se aceptaron una o dos solicitudes de extracción, que ahora eran malas. Así que olvídalo, encontré el último buen commit (abc1234) y simplemente ejecuté el script básico:

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

Les dije a los otros 5 muchachos que trabajan en este repositorio que mejor tomen nota de sus cambios en las últimas horas y que borren / vuelvan a ramificar de las últimas pruebas. El final de la historia.

Suamere
fuente
2
No había publicado los commits, así que esta era la respuesta que necesitaba. Gracias, @Suamere.
Tom Barron
La mejor manera de hacerlo es git push --force-with-lease, que reescribirá el historial solo si nadie más se ha comprometido con la sucursal después o dentro del rango de los compromisos de vaporización. Si otras personas han usado la rama, entonces su historia nunca debe reescribirse, y la confirmación simplemente debe revertirse visiblemente.
frandroid
1
@frandroid "la historia nunca debe reescribirse", solo el trato sith en términos absolutos. La pregunta de este hilo, y el punto de mi respuesta, es exactamente que, para un escenario particular, toda la historia debe borrarse.
Suamere
@Suamere Claro, esa es la pregunta. Pero como su respuesta menciona lo que tenía que decirles a los demás, existe la posibilidad de problemas. Por experiencia personal, push -f puede estropear su base de código si otras personas se han comprometido después de lo que está tratando de borrar. --force-with-lease logra el mismo resultado, excepto que te salva el culo si estabas a punto de arruinar tu repositorio. ¿Por qué arriesgarse? Si --force-with-lease falla, puede ver qué commit se interpone en el camino, evaluar adecuadamente, ajustar e intentar nuevamente.
frandroid
1
@Suamere ¡Gracias! Estoy de acuerdo en que la pregunta establece claramente que quiere reescribir la historia. Estoy en la misma situación que tú y supongo que el OP en el que alguien hizo docenas de reversiones feas y cometidos extraños y reversiones de reversiones por accidente (mientras estaba de vacaciones) y el estado necesita ser revertido. En cualquier caso, junto con una buena advertencia saludable, esta debería ser la respuesta aceptada.
Lee Richardson
9

Esta es una expansión de una de las soluciones proporcionadas en la respuesta de Jakub

Me enfrenté a una situación en la que los commits que necesitaba revertir eran algo complejos, con varios de los commits siendo commits de fusión, y necesitaba evitar reescribir el historial. No pude usar una serie de git revertcomandos porque finalmente encontré conflictos entre los cambios de reversión que se agregaban. Terminé usando los siguientes pasos.

Primero, verifique el contenido de la confirmación de destino mientras deja HEAD en la punta de la rama:

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

(El - se asegura de que <target-commit>se interprete como una confirmación en lugar de un archivo; el. Se refiere al directorio actual).

Luego, determine qué archivos se agregaron en las confirmaciones que se están deshaciendo y, por lo tanto, deben eliminarse:

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

Los archivos que se agregaron deberían aparecer con una "A" al comienzo de la línea, y no debería haber otras diferencias. Ahora, si es necesario eliminar algún archivo, organice estos archivos para su eliminación:

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

Finalmente, comete la reversión:

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

Si lo desea, asegúrese de volver al estado deseado:

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

No debería haber diferencias.

Warren Dew
fuente
¿Estás seguro de que puedes gitCABEZA solo con la punta de la rama?
Suamere
2
Esta fue una solución mucho mejor para mí, ya que en el mío tenía commits de fusión.
sovemp
3

La manera fácil de revertir un grupo de confirmaciones en el repositorio compartido (que la gente usa y desea preservar el historial) es usarlo git revertjunto con git rev-list. El último le proporcionará una lista de confirmaciones, el primero hará la reversión.

Hay dos formas de hacer eso. Si desea revertir varias confirmaciones en una única confirmación, use:

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

esto revertirá un grupo de confirmaciones que necesita, pero deje todos los cambios en su árbol de trabajo, debe confirmarlos como de costumbre.

Otra opción es tener una única confirmación por cambio revertido:

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

Por ejemplo, si tiene un árbol de confirmación como

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

para revertir los cambios de eee a bbb , ejecute

for i in `git rev-list eee^..bbb`; do git revert --no-edit -s $i; done
Ruslan Kabalin
fuente
acabo de usar esto. ¡Gracias!
Ran Biron
2

Ninguno de esos funcionó para mí, así que tenía tres commits para revertir (los últimos tres commits), así que hice:

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

Trabajado como un encanto :)

dorio
fuente
2
Esta es una opción viable solo si sus cambios aún no se han enviado.
kboom
0

En mi opinión, una forma muy fácil y limpia podría ser:

volver a A

git checkout -f A

señalar la cabeza del maestro al estado actual

git symbolic-ref HEAD refs/heads/master

salvar

git commit
nulll
fuente
1
¿Puede explicar la razón de la votación negativa?
nulll
Esto funciona bien ¿Cuál es el punto de dar voto negativo a esta útil respuesta o alguien puede explicar cuál es la mejor práctica?
Levent Divilioglu 01 de
¿Es eso lo mismo que git checkout master; git reset --hard A? O si no, ¿podría explicar un poco más sobre lo que hace?
MM
simplemente funciona, pero simbólico-ref HEAD parece no ser un comando "seguro"
Sérgio
err No quiero arreglar maestro, quiero arreglar una rama
Sérgio
-6

Si desea revertir temporalmente las confirmaciones de una función, puede usar la serie de comandos siguientes.

Así es como funciona

git log --pretty = en línea | grep 'nombre_característica' | cortar -d '' -f1 | xargs -n1 git revert --no-edit

Ajit Singh
fuente