¿Cómo puedo arreglar fácilmente una confirmación pasada?

116

Acabo de leer la modificación de un solo archivo en una confirmación anterior en git, pero desafortunadamente la solución aceptada 'reordena' las confirmaciones, que no es lo que quiero. Entonces esta es mi pregunta:

De vez en cuando, noto un error en mi código mientras trabajo en una función (no relacionada). git blameLuego, un rápido revela que el error se introdujo hace algunas confirmaciones (yo comprometo bastante, por lo que generalmente no es la confirmación más reciente la que introdujo el error). En este punto, suelo hacer esto:

git stash                      # temporarily put my work aside
git rebase -i <bad_commit>~1   # rebase one step before the bad commit
                               # mark broken commit for editing
vim <affected_sources>         # fix the bug
git add <affected_sources>     # stage fixes
git commit -C <bad_commit>     # commit fixes using same log message as before
git rebase --continue          # base all later changes onto this

Sin embargo, esto sucede con tanta frecuencia que la secuencia anterior se vuelve molesta. Especialmente la 'rebase interactiva' es aburrida. ¿Hay algún atajo a la secuencia anterior, que me permita modificar una confirmación arbitraria en el pasado con los cambios por etapas? Soy perfectamente consciente de que esto cambia la historia, pero cometo errores con tanta frecuencia que me encantaría tener algo como

vim <affected_sources>             # fix bug
git add -p <affected_sources>      # Mark my 'fixup' hungs for staging
git fixup <bad_commit>             # amend the specified commit with staged changes,
                                   # rebase any successors of bad commit on rewritten 
                                   # commit.

¿Quizás un script inteligente que pueda reescribir confirmaciones usando herramientas de plomería o algo así?

Frerich Raabe
fuente
¿Qué quieres decir con "reordenar" las confirmaciones? Si está cambiando el historial, todas las confirmaciones desde las confirmaciones modificadas tienen que ser diferentes, pero la respuesta aceptada a la pregunta vinculada no reordena las confirmaciones en ningún sentido significativo.
CB Bailey
1
@Charles: Me refiero a reordenar como en: si noto que HEAD ~ 5 es la confirmación rota, la siguiente respuesta aceptada en la pregunta vinculada hará que HEAD (la punta de la rama) sea la confirmación fija. Sin embargo, me gustaría que HEAD ~ 5 sea la confirmación fija, que es lo que se obtiene cuando se usa una rebase interactiva y se edita una confirmación única para corregirla.
Frerich Raabe
Sí, pero luego el comando rebase volverá a realizar la comprobación del maestro y volverá a basar todas las confirmaciones posteriores en la confirmación fija. ¿No es así como conduces el rebase -i?
CB Bailey
En realidad, hay un problema potencial con esa respuesta, creo que debería serlo rebase --onto tmp bad-commit master. Como está escrito, intentará aplicar el compromiso incorrecto al estado de compromiso fijo.
CB Bailey
Aquí hay otra herramienta para automatizar el proceso de reparación / rebase: stackoverflow.com/a/24656286/1058622
Mika Eloranta

Respuestas:

166

RESPUESTA ACTUALIZADA

Hace un tiempo, --fixupse agregó un nuevo argumento al git commitque se puede usar para construir una confirmación con un mensaje de registro adecuado para git rebase --interactive --autosquash. Entonces, la forma más sencilla de arreglar una confirmación anterior es ahora:

$ git add ...                           # Stage a fix
$ git commit --fixup=a0b1c2d3           # Perform the commit to fix broken a0b1c2d3
$ git rebase -i --autosquash a0b1c2d3~1 # Now merge fixup commit into broken commit

RESPUESTA ORIGINAL

Aquí hay un pequeño script de Python que escribí hace un tiempo que implementa esta git fixuplógica que esperaba en mi pregunta original. La secuencia de comandos asume que realizó algunos cambios y luego aplica esos cambios a la confirmación dada.

NOTA : Este script es específico de Windows; busca git.exey establece la GIT_EDITORvariable de entorno usando set. Ajústelo según sea necesario para otros sistemas operativos.

Usando este script puedo implementar precisamente el flujo de trabajo 'arreglar fuentes rotas, arreglar arreglos, ejecutar git fixup' que pedí:

#!/usr/bin/env python
from subprocess import call
import sys

# Taken from http://stackoverflow.com/questions/377017/test-if-executable-exists-in python
def which(program):
    import os
    def is_exe(fpath):
        return os.path.exists(fpath) and os.access(fpath, os.X_OK)

    fpath, fname = os.path.split(program)
    if fpath:
        if is_exe(program):
            return program
    else:
        for path in os.environ["PATH"].split(os.pathsep):
            exe_file = os.path.join(path, program)
            if is_exe(exe_file):
                return exe_file

    return None

if len(sys.argv) != 2:
    print "Usage: git fixup <commit>"
    sys.exit(1)

git = which("git.exe")
if not git:
    print "git-fixup: failed to locate git executable"
    sys.exit(2)

broken_commit = sys.argv[1]
if call([git, "rev-parse", "--verify", "--quiet", broken_commit]) != 0:
    print "git-fixup: %s is not a valid commit" % broken_commit
    sys.exit(3)

if call([git, "diff", "--staged", "--quiet"]) == 0:
    print "git-fixup: cannot fixup past commit; no fix staged."
    sys.exit(4)

if call([git, "diff", "--quiet"]) != 0:
    print "git-fixup: cannot fixup past commit; working directory must be clean."
    sys.exit(5)

call([git, "commit", "--fixup=" + broken_commit])
call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i", "--autosquash", broken_commit + "~1"], shell=True)
Frerich Raabe
fuente
2
podría usar git stashy git stash popalrededor de su rebase para que ya no requiera un directorio de trabajo limpio
Tobias Kienzler
@TobiasKienzler: Acerca de usar git stashy git stash pop: tienes razón, pero desafortunadamente git stashes mucho más lento en Windows que en Linux o OS / X. Dado que mi directorio de trabajo suele estar limpio, omití este paso para no ralentizar el comando.
Frerich Raabe
Puedo confirmar eso, especialmente cuando se trabaja en una red compartida: - /
Tobias Kienzler
1
Agradable. Lo hice accidentalmente git rebase -i --fixup, y se modificó a partir de la confirmación arreglada como punto de partida, por lo que el argumento sha no era necesario en mi caso.
fwielstra
1
Para las personas que usan --autosquash con frecuencia, puede ser útil configurarlo para que sea el comportamiento predeterminado: git config --global rebase.autosquash true
taktak004
31

Lo que hago es:

git add ... # Agrega la corrección.
git commit # Comprometido, pero en el lugar equivocado.
git rebase -i HEAD ~ 5 # Examina las últimas 5 confirmaciones para reajustar.

Su editor se abrirá con una lista de las últimas 5 confirmaciones, listas para ser entrometidas. Cambio:

pick 08e833c Buen cambio 1.
pick 9134ac9 Buen cambio 2.
elige 5adda55 ¡Mal cambio!
pick 400bce4 Buen cambio 3.
pick 2bc82n1 Corrección de cambio incorrecto.

...a:

pick 08e833c Buen cambio 1.
pick 9134ac9 Buen cambio 2.
elige 5adda55 ¡Mal cambio!
f 2bc82n1 Corrección de cambio incorrecto. # Mueva hacia arriba y cambie 'pick' a 'f' para 'fixup'.
pick 400bce4 Buen cambio 3.

Guarde y salga de su editor, y la corrección se volverá a colocar en la confirmación a la que pertenece.

Después de haberlo hecho varias veces, lo hará en segundos mientras duerme. El rebase interactivo es la característica que realmente me vendió en git. Es increíblemente útil para esto y más ...

Kris Jenkins
fuente
9
Obviamente, puede cambiar HEAD ~ 5 a HEAD ~ n para retroceder más. No querrá entrometerse con ningún historial que haya empujado hacia arriba, por lo que generalmente escribo 'git rebase -i origin / master' para asegurarme de que solo estoy cambiando el historial sin presionar.
Kris Jenkins
4
Esto es muy parecido a lo que siempre hice; FWIW, es posible que le interese el --autosquashcambio git rebase, que reordena automáticamente los pasos en el editor. Vea mi respuesta para un script que aprovecha esto para implementar un git fixupcomando.
Frerich Raabe
No sabía que se podía reordenar los hashes de confirmación, ¡bien!
Aaron Franke
¡Eso es genial! Solo para asegurarse de que todo el trabajo de rebase se realiza es una rama de función separada. Y no meterse con la rama común como maestro.
Jay Modi
22

Un poco tarde para la fiesta, pero aquí hay una solución que funciona como imaginó el autor.

Agregue esto a su .gitconfig:

[alias]
    fixup = "!sh -c '(git diff-files --quiet || (echo Unstaged changes, please commit or stash with --keep-index; exit 1)) && COMMIT=$(git rev-parse $1) && git commit --fixup=$COMMIT && git rebase -i --autosquash $COMMIT~1' -"

Uso de ejemplo:

git add -p
git fixup HEAD~5

Sin embargo, si tiene cambios sin etapas, debe guardarlos antes de la reorganización.

git add -p
git stash --keep-index
git fixup HEAD~5
git stash pop

Puede modificar el alias para ocultarlo automáticamente, en lugar de dar una advertencia. Sin embargo, si la reparación no se aplica correctamente, necesitará sacar el alijo manualmente después de solucionar los conflictos. Hacer tanto guardar como hacer estallar manualmente parece más consistente y menos confuso.

dschlyter
fuente
Esto es muy útil. Para mí, el caso de uso más común es corregir los cambios en la confirmación anterior, así que git fixup HEADes para lo que creé un alias. También me vendría bien enmendar eso, supongo.
saltamontes
¡Gracias! También lo uso con mayor frecuencia en la última confirmación, pero tengo otro alias para una modificación rápida. amend = commit --amend --reuse-message=HEADLuego puede simplemente escribir git amendo git amend -ay omitir el editor para el mensaje de confirmación.
dschlyter
3
El problema de enmendar es que no recuerdo cómo se escribe. Siempre tengo que pensar, es enmendar o enmendar y eso no es bueno.
saltamontes
12

Para arreglar una confirmación:

git commit --fixup a0b1c2d3 .
git rebase --autosquash -i HEAD~2

donde a0b1c2d3 es el compromiso que desea corregir y donde 2 es el número de confirmaciones +1 pegadas que desea cambiar.

Nota: git rebase --autosquash sin -i no funcionó pero con -i funcionó, lo cual es extraño.

Sérgio
fuente
2016 y --autosquashsin -itodavía no funciona.
Jonathan Cross
1
Como dice la página del manual: Esta opción solo es válida cuando se usa la opción --interactive. Pero hay una manera más fácil saltarse el editor:EDITOR=true git rebase --autosquash -i
joeytwiddle
El segundo paso no funciona para mí, diciendo: Por favor, especifique contra qué rama desea rebasar.
djangonaut
git rebase --autosquash -i HEAD ~ 2 (donde 2 es el número de confirmaciones +1 pegadas que desea cambiar.
Sérgio
6

ACTUALIZACIÓN: ahora se puede encontrar una versión más limpia del script aquí: https://github.com/deiwin/git-dotfiles/blob/docs/bin/git-fixup .

He estado buscando algo similar. Sin embargo, este script de Python parece demasiado complicado, por lo tanto, he elaborado mi propia solución:

Primero, mis alias de git se ven así (tomados de aquí ):

[alias]
  fixup = !sh -c 'git commit --fixup=$1' -
  squash = !sh -c 'git commit --squash=$1' -
  ri = rebase --interactive --autosquash

Ahora la función bash se vuelve bastante simple:

function gf {
  if [ $# -eq 1 ]
  then
    if [[ "$1" == HEAD* ]]
    then
      git add -A; git fixup $1; git ri $1~2
    else
      git add -A; git fixup $1; git ri $1~1
    fi
  else
    echo "Usage: gf <commit-ref> "
  fi
}

Este código primero prepara todos los cambios actuales (puede eliminar esta parte, si desea preparar los archivos usted mismo). Luego crea la reparación (también se puede usar squash, si eso es lo que necesita) commit. Después de eso, comienza una rebase interactiva con la --autosquashbandera en el padre de la confirmación que le da como argumento. Eso abrirá tu editor de texto configurado, para que puedas verificar que todo es como esperas y simplemente cerrar el editor finalizará el proceso.

La if [[ "$1" == HEAD* ]]parte (tomada de aquí ) se usa, porque si usa, por ejemplo, HEAD ~ 2 como su referencia de compromiso (el compromiso con el que desea corregir los cambios actuales), entonces el HEAD se desplazará después de que se haya creado el compromiso de reparación y necesitaría usar HEAD ~ 3 para referirse a la misma confirmación.

Deiwin
fuente
Interesante alternativa. +1
VonC
4

Puede evitar la etapa interactiva utilizando un editor "nulo":

$ EDITOR=true git rebase --autosquash -i ...

Esto se utilizará /bin/truecomo editor, en lugar de /usr/bin/vim. Siempre acepta cualquier sugerencia de git, sin preguntar.

joeytwiddle
fuente
De hecho, esto es exactamente lo que hice en mi respuesta del script Python de 'respuesta original' del 30 de septiembre de 2010 (observe cómo en la parte inferior del script, dice call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i" ...).
Frerich Raabe
4

Lo que realmente me molestó del flujo de trabajo de reparación fue que tenía que averiguar yo mismo en qué compromiso quería aplastar el cambio cada vez. Creé un comando "git fixup" que ayuda con esto.

Este comando crea confirmaciones de reparación, con la magia añadida de que utiliza git-deps para encontrar automáticamente la confirmación relevante, por lo que el flujo de trabajo a menudo se reduce a:

# discover and fix typo in a previously committed change
git add -p # stage only typo fix
git fixup

# at some later point squash all the fixup commits that came up
git rebase --autosquash master

Esto solo funciona si los cambios por etapas se pueden atribuir sin ambigüedades a una confirmación en particular en el árbol de trabajo (entre el maestro y HEAD). Encuentro que ese es el caso muy a menudo para el tipo de pequeños cambios para los que uso esto, por ejemplo, errores tipográficos en comentarios o nombres de métodos recién introducidos (o renombrados). Si este no es el caso, al menos mostrará una lista de confirmaciones candidatas.

Utilizo mucho esto en mi flujo de trabajo diario, para integrar rápidamente pequeños cambios en líneas previamente cambiadas en confirmaciones en mi rama de trabajo. El guión no es tan hermoso como podría ser, y está escrito en zsh, pero me ha estado haciendo el trabajo lo suficientemente bien durante un buen tiempo, ahora que nunca sentí la necesidad de reescribirlo:

https://github.com/Valodim/git-fixup

Valodim
fuente
2

Puede crear una reparación para un archivo en particular utilizando este alias.

[alias]
...
# fixup for a file, using the commit where it was last modified
fixup-file = "!sh -c '\
        [ $(git diff          --numstat $1 | wc -l) -eq 1 ] && git add $1 && \
        [ $(git diff --cached --numstat $1 | wc -l) -eq 1 ] || (echo No changes staged. ; exit 1) && \
        COMMIT=$(git log -n 1 --pretty=format:"%H" $1) && \
            git commit --fixup=$COMMIT && \
            git rebase -i --autosquash $COMMIT~1' -"

Si ha realizado algunos cambios myfile.txtpero no desea ponerlos en una nueva confirmación, git fixup-file myfile.txtcreará un fixup!para la confirmación donde myfile.txtse modificó por última vez, y luego lo hará rebase --autosquash.

Álvaro
fuente
Muy inteligente, aunque preferiría que git rebaseno se llamara automáticamente.
hurikhan77
2

commit --fixup y rebase --autosquash son geniales, pero no hacen lo suficiente. Cuando tengo una secuencia de confirmaciones A-B-Cy escribo algunos cambios más en mi árbol de trabajo que pertenecen a una o más de esas confirmaciones existentes, tengo que mirar manualmente el historial, decidir qué cambios pertenecen a qué confirmaciones, organizarlos y crear el fixup!se compromete. Pero git ya tiene acceso a suficiente información para poder hacer todo eso por mí, así que escribí un script en Perl que hace precisamente eso.

Por cada trozo en git diff el script, se usa git blamepara encontrar la confirmación que tocó por última vez las líneas relevantes y las llamadas git commit --fixuppara escribir las fixup!confirmaciones apropiadas , esencialmente haciendo lo mismo que estaba haciendo manualmente antes.

Si lo encuentra útil, siéntase libre de mejorarlo e iterarlo y tal vez algún día tengamos dicha función en forma gitadecuada. Me encantaría ver una herramienta que pueda comprender cómo se debe resolver un conflicto de fusión cuando ha sido introducido por una rebase interactiva.

Oktalista
fuente
También soñé con la automatización: git debería intentar ponerlo lo más atrás posible en la historia, sin que se rompa el parche. Pero su método probablemente sea más sensato. Es bueno ver que lo has intentado. ¡Lo intentaré! (Por supuesto, hay ocasiones en las que el parche de reparación aparece en otra parte del archivo, y solo el desarrollador sabe a qué confirmación pertenece. O quizás una nueva prueba en el conjunto de pruebas podría ayudar a la máquina a determinar dónde debería ir la corrección).
joeytwiddle
1

Escribí una pequeña función de shell llamada gcfpara realizar la confirmación de reparación y la rebase automáticamente:

$ git add -p

  ... select hunks for the patch with y/n ...

$ gcf <earlier_commit_id>

  That commits the fixup and does the rebase.  Done!  You can get back to coding.

Por ejemplo, puede parchear la segunda confirmación antes de la última con: gcf HEAD~~

Aquí está la función . Puedes pegarlo en tu~/.bashrc

git_commit_immediate_fixup() {
  local commit_to_amend="$1"
  if [ -z "$commit_to_amend" ]; then
    echo "You must provide a commit to fixup!"; return 1
  fi

  # Get a static commit ref in case the commit is something relative like HEAD~
  commit_to_amend="$(git rev-parse "${commit_to_amend}")" || return 2

  #echo ">> Committing"
  git commit --no-verify --fixup "${commit_to_amend}" || return 3

  #echo ">> Performing rebase"
  EDITOR=true git rebase --interactive --autosquash --autostash \
                --rebase-merges --no-fork-point "${commit_to_amend}~"
}

alias gcf='git_commit_immediate_fixup'

Se utiliza --autostashpara guardar y hacer estallar cualquier cambio no confirmado si es necesario.

--autosquash requiere un --interactive rebase, pero evitamos la interacción usando un maniquí EDITOR.

--no-fork-pointprotege las confirmaciones de ser eliminadas silenciosamente en situaciones raras (cuando ha bifurcado una nueva rama y alguien ya ha vuelto a basar las confirmaciones pasadas).

joeytwiddle
fuente
0

No conozco una forma automatizada, pero aquí hay una solución que podría ser más fácil de manipular por humanos:

git stash
# write the patch
git add -p <file>
git commit -m"whatever"   # message doesn't matter, will be replaced via 'fixup'
git rebase -i <bad-commit-id>~1
# now cut&paste the "whatever" line from the bottom to the second line
# (i.e. below <bad-commit>) and change its 'pick' into 'fixup'
# -> the fix commit will be merged into the <bad-commit> without changing the
# commit message
git stash pop
Tobias Kienzler
fuente
Vea mi respuesta para un script que aprovecha esto para implementar un git fixupcomando.
Frerich Raabe
@Frerich Raabe: suena bien, no lo sé--autosquash
Tobias Kienzler
0

Recomendaría https://github.com/tummychow/git-absorb :

Parcela de ascensor

Tienes una rama de características con algunas confirmaciones. Su compañero de equipo revisó la rama y señaló algunos errores. Tiene correcciones para los errores, pero no quiere meterlos todos en una confirmación opaca que dice correcciones, porque cree en las confirmaciones atómicas. En lugar de buscar manualmente SHA de confirmación para git commit --fixup, o ejecutar una rebase interactiva manual, haga esto:

  • git add $FILES_YOU_FIXED

  • git absorb --and-rebase

  • o: git rebase -i --autosquash master

git absorbidentificará automáticamente qué confirmaciones son seguras de modificar y qué cambios indexados pertenecen a cada una de esas confirmaciones. ¡Luego escribirá la reparación! confirma para cada uno de esos cambios. Puede verificar su salida manualmente si no confía en él, y luego plegar las correcciones en su rama de características con la funcionalidad incorporada de autosquash de git.

Petski
fuente