Eliminar confirmación específica

287

Estaba trabajando con un amigo en un proyecto, y él editó un montón de archivos que no deberían haberse editado. De alguna manera fusioné su trabajo con el mío, ya sea cuando lo saqué o cuando traté de elegir los archivos específicos que quería. He estado buscando y jugando durante mucho tiempo, tratando de descubrir cómo eliminar las confirmaciones que contienen las ediciones de esos archivos, parece ser un cambio entre revertir y rebase, y no hay ejemplos directos, y el los documentos suponen que sé más que yo.

Así que aquí hay una versión simplificada de la pregunta:

Dado el siguiente escenario, ¿cómo elimino commit 2?

$ mkdir git_revert_test && cd git_revert_test

$ git init
Initialized empty Git repository in /Users/josh/deleteme/git_revert_test/.git/

$ echo "line 1" > myfile

$ git add -A

$ git commit -m "commit 1"
[master (root-commit) 8230fa3] commit 1
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 myfile

$ echo "line 2" >> myfile

$ git commit -am "commit 2"
[master 342f9bb] commit 2
 1 files changed, 1 insertions(+), 0 deletions(-)

$ echo "line 3" >> myfile

$ git commit -am "commit 3"
[master 1bcb872] commit 3
 1 files changed, 1 insertions(+), 0 deletions(-)

El resultado esperado es

$ cat myfile
line 1
line 3

Aquí hay un ejemplo de cómo he estado tratando de revertir

$ git revert 342f9bb
Automatic revert failed.  After resolving the conflicts,
mark the corrected paths with 'git add <paths>' or 'git rm <paths>'
and commit the result.
Joshua Cheek
fuente
55
Si alguien encuentra esto mientras busca el mismo problema, esto es lo que terminé haciendo: copiar y pegar. Seriamente. Pasé más de 6 horas tratando de hacer que las soluciones sugeridas funcionen, en vano. Al final, me quedé sin tiempo, saqué el original y simplemente copié / pegué unos 20 archivos. Tomó menos de 5 minutos, y las cosas han estado bien desde entonces (incluso cuando esos archivos se fusionan con cambios en otras ramas que ocurrieron antes de este fiasco). Le sugiero que tome este enfoque también. No solo es lo más simple, también sospecho que es lo único que funciona.
Joshua Cheek
Me enfrenté a un problema similar, pero quizás más complejo: tenía una rama con cientos de commits que quería aplastar. Desafortunadamente, los commits de otra rama se fusionaron de nuevo en la rama en un punto intermedio, por lo que era necesario "desunir" antes de que pudiera aplastar. Seguí un camino similar al sugerido por tk a continuación (selección de cereza + uso de la notación de rango), pero produjo conflictos en algunos otros archivos. Al final, copiar y pegar + algunas ediciones manuales fue el camino más fácil y más predecible hacia adelante. Definitivamente vale la pena considerarlo si te pasas demasiado tiempo en esto.
Federico

Respuestas:

73

El algoritmo que utiliza Git al calcular las diferencias para revertir requiere que

  1. las líneas que se revierten no son modificadas por ninguna confirmación posterior.
  2. que no haya otros compromisos "adyacentes" más adelante en la historia.

La definición de "adyacente" se basa en el número predeterminado de líneas de una diferencia de contexto, que es 3. Entonces, si 'myfile' se construyó así:

$ cat >myfile <<EOF
line 1
junk
junk
junk
junk
line 2
junk
junk
junk
junk
line 3
EOF
$ git add myfile
$ git commit -m "initial check-in"
 1 files changed, 11 insertions(+), 0 deletions(-)
 create mode 100644 myfile

$ perl -p -i -e 's/line 2/this is the second line/;' myfile
$ git commit -am "changed line 2 to second line"
[master d6cbb19] changed line 2
 1 files changed, 1 insertions(+), 1 deletions(-)

$ perl -p -i -e 's/line 3/this is the third line/;' myfile
$ git commit -am "changed line 3 to third line"
[master dd054fe] changed line 3
 1 files changed, 1 insertions(+), 1 deletions(-)

$ git revert d6cbb19
Finished one revert.
[master 2db5c47] Revert "changed line 2"
 1 files changed, 1 insertions(+), 1 deletions(-)

Entonces todo funciona como se esperaba.

La segunda respuesta fue muy interesante. Hay una característica que aún no se ha lanzado oficialmente (aunque está disponible en Git v1.7.2-rc2) llamada Estrategia de reversión. Puedes invocar a git de esta manera:

git revert --strategy resolver < commitir >

y debería hacer un mejor trabajo descifrando lo que querías decir. No sé cuál es la lista de estrategias disponibles, ni sé la definición de ninguna estrategia.

Hal Eisen
fuente
1
perl -pes útil para escribir programas muy cortos (una línea) que repiten y pasan su entrada a la salida, de forma similar a sed. perl -ise usa para editar archivos en su lugar . perl -ees cómo enviar el código para ser evaluado .
Hal Eisen
281

Hay cuatro formas de hacerlo:

  • Forma limpia, revertiendo pero mantener en el registro la reversión:

    git revert --strategy resolve <commit>
    
  • De manera difícil, elimine por completo solo la última confirmación:

    git reset --soft "HEAD^"
    

Nota: Evite git reset --hardya que también descartará todos los cambios en los archivos desde la última confirmación. Si --softno funciona, mejor prueba --mixedo --keep.

  • Rebase (muestre el registro de los últimos 5 commits y elimine las líneas que no desea, o reordene, o aplaste múltiples commits en uno, o haga lo que quiera, esta es una herramienta muy versátil):

    git rebase -i HEAD~5
    

Y si se comete un error:

git rebase --abort
  • Rebase rápida: elimine solo una confirmación específica utilizando su id:

    git rebase --onto commit-id^ commit-id
    
  • Alternativas: también puedes probar:

    git cherry-pick commit-id
    
  • Otra alternativa más:

    git revert --no-commit
    
  • Como último recurso, si necesita una libertad total de edición de historial (por ejemplo, porque git no le permite editar lo que desea), puede usar esta aplicación de código abierto muy rápida : reposurgeon .

Nota: por supuesto, todos estos cambios se realizan localmente, git pushluego debe aplicar los cambios al control remoto. Y en caso de que su repositorio no quiera eliminar la confirmación ("no se permite el avance rápido", lo que sucede cuando desea eliminar una confirmación que ya presionó), puede usar git push -fpara forzar los cambios.

Nota 2: si trabaja en una rama y necesita forzar el empuje, debe evitarlo git push --forceporque puede sobrescribir otras ramas (si ha realizado cambios en ellas, incluso si su pago actual está en otra rama). Prefieren especificar siempre la rama remota cuando fuerza de empuje : git push --force origin your_branch.

gaborous
fuente
Basado en sugerencias de @gaborous: haz un "git rebase -i HEAD ~ 2". Ahora tienes varias opciones. En el vim puede ver algunas líneas comentadas: una de ellas le dice que simplemente puede eliminar una línea (que debería ser la confirmación de la que desea deshacerse) y esta confirmación se eliminará junto con su registro en su historial.
Ilker Cat
git revert --strategy resolve <commit> .Este comando funcionó para mí. gracias :)
Swathin
2
git rebase -i HEAD~5trabajó para mi. Luego eliminé los commits que no necesitaba y pude resolver el problema en menos de 30 segundos. Gracias.
lv10
117

Aquí hay una solución fácil:

git rebase -i HEAD~x

(Nota: xes el número de confirmaciones)

al ejecutar el archivo de bloc de notas se abrirá. Ingrese dropademás de su compromiso.
Si no conoce Vim, simplemente haga clic en cada selección de palabras que desea editar y luego presione la tecla "I" (para el modo de inserción). Una vez que haya terminado de escribir, presione la tecla "esc" para salir del modo de inserción.



ingrese la descripción de la imagen aquí

y listo, ya está ... Simplemente sincronice el panel de control de git y los cambios se enviarán al control remoto.

Si la confirmación que suelta ya estaba en el control remoto, tendrá que forzar la inserción. Dado que --force se considera dañino , use git push --force-with-lease.

JD-V
fuente
¿Hay una buena manera de averiguar qué debería ser 'x' si solo conoces el hash del commit en cuestión?
jlewkovich
1
Para personas con mentalidad cpp: x (no. De commits) es inclusivo. por ejemplo, HEAD ~ 4 incluye los últimos 4 commits.
Ingeniero libre de herpes
Sugerencia perfecta. solo quiero agregar, esto también descarta los cambios del commit descartado.
celerno
intentó revertir 3 commits: git rebase -i HEAD-3 obtuvo un error fatal: necesitaba una sola revisión inválida aguas arriba 'HEAD-3'
Ustin
1
@Ustin es ~ 3 no -3
JD-V
37

Tu elección es entre

  1. manteniendo el error e introduciendo una solución y
  2. eliminando el error y cambiando el historial.

Debe elegir (1) si el cambio erróneo ha sido detectado por otra persona y (2) si el error se limita a una rama privada no empujada.

Git revert es una herramienta automatizada para hacer (1), crea una nueva confirmación deshaciendo algunas confirmaciones anteriores. Verá el error y la eliminación en el historial del proyecto, pero las personas que extraen de su repositorio no tendrán problemas cuando actualicen. No está funcionando de manera automatizada en su ejemplo, por lo que debe editar 'myfile' (para eliminar la línea 2), hacer git add myfiley git commitlidiar con el conflicto. Luego terminarás con cuatro commits en tu historial, con commit 4 revertiendo commit 2.

Si a nadie le importa que su historial cambie, puede volver a escribirlo y eliminar commit 2 (opción 2). La manera fácil de hacer esto es usarlo git rebase -i 8230fa3. Esto lo colocará en un editor y puede elegir no incluir el compromiso erróneo eliminando el compromiso (y manteniendo la "selección" al lado de los otros mensajes de confirmación. Lea sobre las consecuencias de hacerlo .

Andrew Walker
fuente
Rebase puede ser complicado, ya que parece que ha habido una fusión.
Cascabel
3
git rebase -i 8230fa3, y eliminar la línea de confirmación funcionó muy bien para mí con mis cambios solo locales. ¡Gracias!
Samuel
26

Aproximación 1

Primero obtenga el hash de confirmación (ej .: 1406cd61) que necesita revertir. la solución simple estará debajo del comando,

$ git revert 1406cd61

si ha confirmado más cambios relacionados con los archivos 1406cd61 después de la confirmación 1406cd61, el comando anterior simple no funcionará. Luego tienes que hacer los pasos a continuación, que es recoger cerezas.

Aproximacion 2

Siga las siguientes acciones, ya que estamos usando --force necesita tener derechos de administrador sobre el repositorio de git para hacer esto.

Paso 1: busque el compromiso antes del compromiso que desea eliminargit log

Paso 2: Revisa ese commitgit checkout <commit hash>

Paso 3: Crea una nueva sucursal usando tu confirmación de pago actualgit checkout -b <new branch>

Paso 4: ahora debe agregar la confirmación después de la confirmación eliminadagit cherry-pick <commit hash>

Paso 5: Ahora repita el Paso 4 para todas las demás confirmaciones que desea mantener.

Paso 6: Una vez que todas las confirmaciones se han agregado a su nueva sucursal y se han confirmado. Verifique que todo esté en el estado correcto y funcione según lo previsto. Verifique que todo se haya comprometido:git status

Paso 7: cambia a tu rama rotagit checkout <broken branch>

Paso 8: Ahora realice un restablecimiento completo en la rama rota a la confirmación antes de la que desea eliminargit reset --hard <commit hash>

Paso 9: combine su rama fija en esta ramagit merge <branch name>

Paso 10: Empuje los cambios combinados nuevamente al origen. ADVERTENCIA: ¡Esto sobrescribirá el repositorio remoto!git push --force origin <branch name>

Puede hacer el proceso sin crear una nueva rama reemplazando los pasos 2 y 3 con el paso 8 y luego no llevar a cabo los pasos 7 y 9.

tk_
fuente
1
El primer enfoque funcionó a las mil maravillas. Incluye una confirmación que especifica que la confirmación deseada se ha revertido, lo que es realmente bueno para fines de seguimiento.
suarsenegger
18

Puede eliminar confirmaciones no deseadas con git rebase. Supongamos que incluyó algunas confirmaciones de la rama de tema de un compañero de trabajo en su rama de tema, pero luego decida que no desea esas confirmaciones.

git checkout -b tmp-branch my-topic-branch  # Use a temporary branch to be safe.
git rebase -i master  # Interactively rebase against master branch.

En este punto, su editor de texto abrirá la vista interactiva de rebase. Por ejemplo

git-rebase-todo

  1. Elimine las confirmaciones que no desea eliminando sus líneas
  2. Guardar y Salir

Si el rebase no tuvo éxito, elimine la rama temporal e intente otra estrategia. De lo contrario, continúe con las siguientes instrucciones.

git checkout my-topic-branch
git reset --hard tmp-branch  # Overwrite your topic branch with the temp branch.
git branch -d tmp-branch  # Delete the temporary branch.

Si está empujando su rama de tema a un control remoto, es posible que deba forzar el empuje ya que el historial de confirmación ha cambiado. Si otros están trabajando en la misma rama, avíseles.

Dennis
fuente
¿Podría por favor dar un ejemplo de 'probar otra estrategia'?
pfabri
@pfabri, por ejemplo, podría elegir dos rangos de confirmaciones donde omita la confirmación incorrecta. Podrías revertir el mal compromiso. Puede evitar el uso de una solución git incluso deshaciendo manualmente los cambios, o comenzando desde una rama nueva sin la confirmación incorrecta y rehaciendo manualmente los cambios buenos. Si la confirmación incorrecta contiene datos confidenciales, necesitará una estrategia más cuidadosa: help.github.com/en/articles/…
Dennis
8

De otras respuestas aquí, estaba un poco confundido con cómo git rebase -ipodría usarse para eliminar una confirmación, así que espero que esté bien anotar mi caso de prueba aquí (muy similar al OP).

Aquí hay un bashscript que puede pegar para crear un repositorio de prueba en la /tmpcarpeta:

set -x

rm -rf /tmp/myrepo*
cd /tmp

mkdir myrepo_git
cd myrepo_git
git init
git config user.name me
git config user.email me@myself.com

mkdir folder
echo aaaa >> folder/file.txt
git add folder/file.txt
git commit -m "1st git commit"

echo bbbb >> folder/file.txt
git add folder/file.txt
git commit -m "2nd git commit"

echo cccc >> folder/file.txt
git add folder/file.txt
git commit -m "3rd git commit"

echo dddd >> folder/file.txt
git add folder/file.txt
git commit -m "4th git commit"

echo eeee >> folder/file.txt
git add folder/file.txt
git commit -m "5th git commit"

En este punto, tenemos una file.txtcon estos contenidos:

aaaa
bbbb
cccc
dddd
eeee

En este punto, HEAD está en la 5ª confirmación, HEAD ~ 1 sería la 4ª y HEAD ~ 4 sería la primera confirmación (por lo que HEAD ~ 5 no existiría). Digamos que queremos eliminar la tercera confirmación: podemos emitir este comando en el myrepo_gitdirectorio:

git rebase -i HEAD~4

( Tenga en cuenta que los git rebase -i HEAD~5resultados con "fatal: se necesitaba una única revisión; HEAD ~ 5" no válido. ) Se abrirá un editor de texto (ver captura de pantalla en la respuesta de @Dennis ) con estos contenidos:

pick 5978582 2nd git commit
pick 448c212 3rd git commit
pick b50213c 4th git commit
pick a9c8fa1 5th git commit

# Rebase b916e7f..a9c8fa1 onto b916e7f
# ...

Entonces obtenemos todos los commits desde (pero sin incluir ) nuestro HEAD ~ 4 solicitado. Elimine la línea pick 448c212 3rd git commity guarde el archivo; obtendrá esta respuesta de git rebase:

error: could not apply b50213c... 4th git commit

When you have resolved this problem run "git rebase --continue".
If you would prefer to skip this patch, instead run "git rebase --skip".
To check out the original branch and stop rebasing run "git rebase --abort".
Could not apply b50213c... 4th git commit

En este punto, abra myrepo_git / folder/file.txten un editor de texto; verá que ha sido modificado:

aaaa
bbbb
<<<<<<< HEAD
=======
cccc
dddd
>>>>>>> b50213c... 4th git commit

Básicamente, gitve que cuando HEAD llegó a la segunda confirmación, había contenido de aaaa+ bbbb; y luego tiene un parche de cccc+ agregado ddddque no sabe cómo agregar al contenido existente.

Entonces, aquí gitno puede decidir por usted, es usted quien tiene que tomar una decisión: al eliminar la tercera confirmación, mantiene los cambios introducidos por ella (aquí, la línea cccc), o no lo hace. Si no lo hace, simplemente elimine las líneas adicionales, incluida la cccc, folder/file.txtusando un editor de texto, para que se vea así:

aaaa
bbbb
dddd

... y luego guardar folder/file.txt. Ahora puede emitir los siguientes comandos en el myrepo_gitdirectorio:

$ nano folder/file.txt  # text editor - edit, save
$ git rebase --continue
folder/file.txt: needs merge
You must edit all merge conflicts and then
mark them as resolved using git add

Ah, entonces para marcar que hemos resuelto el conflicto, debemos hacer git add lo siguiente folder/file.txtantes de hacer git rebase --continue:

$ git add folder/file.txt
$ git rebase --continue

Aquí se abre nuevamente un editor de texto, que muestra la línea 4th git commit; aquí tenemos la oportunidad de cambiar el mensaje de confirmación (que en este caso podría cambiarse significativamente 4th (and removed 3rd) commito similar). Digamos que no quiere, así que salga del editor de texto sin guardar; una vez que hagas eso, obtendrás:

$ git rebase --continue
[detached HEAD b8275fc] 4th git commit
 1 file changed, 1 insertion(+)
Successfully rebased and updated refs/heads/master.

En este punto, ahora tiene un historial como este (que también podría inspeccionar con say gitk .u otras herramientas) del contenido de folder/file.txt(con, aparentemente, marcas de tiempo sin cambios de los commits originales):

1st git commit  |  +aaaa
----------------------------------------------
2nd git commit  |   aaaa
                |  +bbbb
----------------------------------------------
4th git commit  |   aaaa
                |   bbbb
                |  +dddd
----------------------------------------------
5th git commit  |   aaaa
                |   bbbb
                |   dddd
                |  +eeee

Y si anteriormente, decidimos mantener la línea cccc(el contenido del tercer git commit que eliminamos), habríamos tenido:

1st git commit  |  +aaaa
----------------------------------------------
2nd git commit  |   aaaa
                |  +bbbb
----------------------------------------------
4th git commit  |   aaaa
                |   bbbb
                |  +cccc
                |  +dddd
----------------------------------------------
5th git commit  |   aaaa
                |   bbbb
                |   cccc
                |   dddd
                |  +eeee

Bueno, este era el tipo de lectura que esperaba haber encontrado, para comenzar a entender cómo git rebasefunciona en términos de eliminar commits / revisiones; así que espero que pueda ayudar a otros también ...

sdaau
fuente
Qué invaluable recorrido: el caso de uso exacto con el que estaba luchando, esto me ayudó inmensamente.
pfabri
2

Por lo tanto, parece que la confirmación incorrecta se incorporó en una confirmación de fusión en algún momento. ¿Ya se ha retirado su compromiso de fusión? Si es así, entonces querrás usar git revert; Tendrás que apretar los dientes y superar los conflictos. Si no, entonces posiblemente podría volver a crear un rebase o revertir, pero puede hacerlo antes de la confirmación de fusión y luego rehacer la fusión.

Realmente no hay mucha ayuda que podamos brindarle para el primer caso. Después de intentar la reversión y descubrir que la automática falló, debe examinar los conflictos y solucionarlos adecuadamente. Este es exactamente el mismo proceso que arreglar conflictos de fusión; puede usar git statuspara ver dónde están los conflictos, editar los archivos no fusionados, encontrar los trozos en conflicto, descubrir cómo resolverlos, agregar los archivos en conflicto y finalmente confirmar. Si lo usa git commitsolo (no -m <message>), el mensaje que aparece en su editor debe ser el mensaje de plantilla creado por git revert; puede agregar una nota sobre cómo solucionó los conflictos, luego guardar y salir para confirmar.

Para el segundo caso, solucionar el problema antes de la fusión, hay dos subcajas, dependiendo de si ha trabajado más desde la fusión. Si no lo ha hecho, puede simplemente git reset --hard HEAD^eliminar la fusión, hacer la reversión y luego rehacer la fusión. Pero supongo que sí. Entonces, terminarás haciendo algo como esto:

  • cree una rama temporal justo antes de la fusión y compruébelo
  • hacer la reversión (o usar git rebase -i <something before the bad commit> <temporary branch>para eliminar la mala confirmación)
  • rehacer la fusión
  • vuelva a basar su trabajo posterior en: git rebase --onto <temporary branch> <old merge commit> <real branch>
  • eliminar la rama temporal
Cascabel
fuente
1

Así que hiciste un trabajo y lo empujaste, llamémosles commits A y B. Tu compañero de trabajo también hizo algo de trabajo, comete C y D. Fusionaste a tus compañeros de trabajo con los tuyos (fusionar commit E), luego continuaste trabajando, comprometiste eso, también (cometer F), y descubrió que su compañero de trabajo cambió algunas cosas que no debería haber cambiado.

Entonces su historial de confirmación se ve así:

A -- B -- C -- D -- D' -- E -- F

Realmente quieres deshacerte de C, D y D '. Como usted dice que fusionó el trabajo de sus compañeros de trabajo con los suyos, estos commits ya están "ahí fuera", por lo que eliminar los commits utilizando, por ejemplo, git rebase es un no-no. Créeme, lo he intentado.

Ahora, veo dos salidas:

  • si aún no ha presionado E y F a su compañero de trabajo ni a ninguna otra persona (generalmente su servidor "de origen"), aún podría eliminarlos del historial por el momento. Este es tu trabajo que quieres guardar. Esto se puede hacer con un

    git reset D'
    

    (reemplace D 'con el hash de confirmación real que puede obtener de un git log

    En este punto, las confirmaciones E y F han desaparecido y los cambios son cambios no confirmados en su espacio de trabajo local nuevamente. En este punto, los movería a una rama o los convertiría en un parche y los guardaría para más adelante. Ahora, revierta el trabajo de su compañero de trabajo, ya sea automáticamente con ungit revert o manualmente. Cuando hayas hecho eso, repite tu trabajo además de eso. Es posible que tenga conflictos de fusión, pero al menos estarán en el código que escribió, en lugar del código de su compañero de trabajo.

  • Si ya ha presionado el trabajo que hizo después de las confirmaciones de su compañero de trabajo, aún puede intentar obtener un "parche inverso" ya sea manualmente o usando git revert, pero dado que su trabajo está "en el camino", por así decirlo, probablemente obtendrá más conflictos de fusión y más confusos. Parece que eso es en lo que terminaste ...

Frans
fuente
0

git revert --strategy resolve si commit es una fusión: usa git revert --strategy resolve -m 1

ShaDow RiDeR
fuente