Come posso riparare facilmente un commit passato?


116

Ho appena letto la modifica di un singolo file in un commit passato in git, ma sfortunatamente la soluzione accettata "riordina" i commit, che non è quello che voglio. Quindi ecco la mia domanda:

Di tanto in tanto, noto un bug nel mio codice mentre lavoro su una funzionalità (non correlata). Una rapida git blamequindi rivela che il bug è stato introdotto alcuni commit fa (mi impegno parecchio, quindi di solito non è il commit più recente che ha introdotto il bug). A questo punto, di solito faccio questo:

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

Tuttavia, questo accade così spesso che la sequenza di cui sopra sta diventando fastidiosa. Soprattutto il "rebase interattivo" è noioso. C'è qualche scorciatoia per la sequenza di cui sopra, che mi consente di modificare un commit arbitrario in passato con le modifiche graduali? Sono perfettamente consapevole che questo cambia la storia, ma faccio errori così spesso che mi piacerebbe davvero avere qualcosa di simile

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.

Forse uno script intelligente in grado di riscrivere i commit utilizzando strumenti idraulici o giù di lì?


Cosa intendi per "riordini" i commit? Se stai modificando la cronologia, tutti i commit dal momento che i commit modificati devono essere diversi, ma la risposta accettata alla domanda collegata non riordina i commit in alcun senso significativo.
CB Bailey

1
@Charles: intendevo riordinare come in: se noto che HEAD ~ 5 è il commit non funzionante, la seguente risposta accettata nella domanda collegata renderà HEAD (la punta del ramo) il commit fisso. Tuttavia, vorrei che HEAD ~ 5 fosse il commit corretto, che è ciò che ottieni quando usi un rebase interattivo e modifichi un singolo commit per la correzione.
Frerich Raabe

Sì, ma il comando rebase effettuerà nuovamente il checkout del master e riassegnerà tutti i commit successivi sul commit fisso. Non è così che guidi rebase -i?
CB Bailey

In realtà, c'è un potenziale problema con quella risposta, penso che dovrebbe essere rebase --onto tmp bad-commit master. Come scritto, proverà ad applicare il bad commit allo stato di commit fisso.
CB Bailey

Ecco un altro strumento per automatizzare il processo di correzione / riassegnazione: stackoverflow.com/a/24656286/1058622
Mika Eloranta

Risposte:


166

RISPOSTA AGGIORNATA

Tempo fa è --fixupstato aggiunto un nuovo argomento al git commitquale è possibile utilizzare per costruire un commit con un messaggio di log adatto git rebase --interactive --autosquash. Quindi il modo più semplice per correggere un commit passato è ora:

$ 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

RISPOSTA ORIGINALE

Ecco un piccolo script Python che ho scritto qualche tempo fa che implementa questa git fixuplogica che speravo nella mia domanda originale. Lo script presume che tu abbia messo in scena alcune modifiche e quindi applica tali modifiche al commit specificato.

NOTA : questo script è specifico di Windows; cerca git.exee imposta la GIT_EDITORvariabile d'ambiente usando set. Regola questo come necessario per altri sistemi operativi.

Usando questo script posso implementare precisamente il flusso di lavoro 'fix broken sources, stage fixup, run git fixup' che ho richiesto:

#!/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)

2
potresti usare git stashe git stash popaggirare il tuo rebase per non richiedere più una directory di lavoro pulita
Tobias Kienzler

@TobiasKienzler: sull'utilizzo di git stashe git stash pop: hai ragione, ma sfortunatamente git stashè molto più lento su Windows rispetto a Linux o OS / X. Poiché la mia directory di lavoro è solitamente pulita, ho omesso questo passaggio per non rallentare il comando.
Frerich Raabe

Posso confermarlo, soprattutto quando si lavora su una condivisione di rete: - /
Tobias Kienzler

1
Bello. L'ho fatto accidentalmente git rebase -i --fixupe ha ribasato dal commit fisso come punto di partenza, quindi l'argomento sha non era necessario nel mio caso.
fwielstra

1
Per le persone che usano spesso --autosquash, può essere utile impostarlo come comportamento predefinito: git config --global rebase.autosquash true
taktak004

31

Quello che faccio è:

git add ... # Aggiungi la correzione.
git commit # Commesso, ma nel posto sbagliato.
git rebase -i HEAD ~ 5 # Esamina gli ultimi 5 commit per il rebase.

Il tuo editor si aprirà con un elenco degli ultimi 5 commit, pronto per essere immischiato. Modificare:

pick 08e833c Buon cambiamento 1.
pick 9134ac9 Buon cambiamento 2.
scegli 5adda55 Cattivo cambiamento!
pick 400bce4 Buon cambiamento 3.
pick 2bc82n1 Correzione di un brutto cambiamento.

...per:

pick 08e833c Buon cambiamento 1.
pick 9134ac9 Buon cambiamento 2.
scegli 5adda55 Cattivo cambiamento!
f 2bc82n1 Correzione di una modifica errata. # Sposta in alto e cambia 'pick' in 'f' per 'fixup'.
pick 400bce4 Buon cambiamento 3.

Salva ed esci dall'editor e la correzione verrà nuovamente compressa nel commit a cui appartiene.

Dopo averlo fatto alcune volte, lo farai in pochi secondi nel sonno. Il rebase interattivo è la caratteristica che mi ha davvero venduto su git. È incredibilmente utile per questo e altro ...


9
Ovviamente puoi cambiare HEAD ~ 5 in HEAD ~ n per tornare indietro. Non vorrai immischiarti con la cronologia che hai inserito a monte, quindi di solito digito "git rebase -i origin / master" per assicurarmi di cambiare solo la cronologia non premuta.
Kris Jenkins,

4
Questo è molto simile a quello che ho sempre fatto; FWIW, potresti essere interessato allo --autosquashswitch per git rebase, che riordina automaticamente i passaggi nell'editor per te. Vedere la mia risposta per uno script che ne approfitta per implementare un git fixupcomando.
Frerich Raabe

Non sapevo che potessi riordinare gli hash di commit, bello!
Aaron Franke

È grandioso! Solo per assicurarsi che tutto il lavoro di rebase sia fatto è un ramo di funzionalità separato. E non scherzare con il ramo comune come il maestro.
Jay Modi

22

Un po 'in ritardo per la festa, ma ecco una soluzione che funziona come immaginava l'autore.

Aggiungi questo al tuo .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' -"

Utilizzo di esempio:

git add -p
git fixup HEAD~5

Tuttavia, se hai modifiche non organizzate, devi nasconderle prima del rebase.

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

È possibile modificare l'alias in modo che venga nascosto automaticamente, invece di fornire un avviso. Tuttavia, se la correzione non si applica in modo pulito, sarà necessario aprire manualmente lo stash dopo aver risolto i conflitti. Fare sia il salvataggio che lo scoppio manualmente sembra più coerente e meno confuso.


Questo è molto utile. Per me il caso d'uso più comune è correggere le modifiche al commit precedente, quindi git fixup HEADè per questo che ho creato un alias. Potrei anche usare la modifica per quello suppongo.
cavalletta

Grazie! Lo uso anche più spesso nell'ultimo commit, ma ho un altro alias per una rapida modifica. amend = commit --amend --reuse-message=HEADQuindi puoi semplicemente digitare git amendo git amend -ae saltare l'editor per il messaggio di commit.
dschlyter

3
Il problema con la modifica è che non ricordo come si scrive. Devo sempre pensare, ammenda o modifichi e non va bene.
cavalletta

12

Per correggere un commit:

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

dove a0b1c2d3 è il commit che si desidera correggere e dove 2 è il numero di commit incollati +1 che si desidera modificare.

Nota: git rebase --autosquash senza -i non ha funzionato ma con -i ha funzionato, il che è strano.


2016 e --autosquashsenza -iancora non funziona.
Jonathan Cross

1
Come dice la pagina man: Questa opzione è valida solo quando viene usata l'opzione --interactive. Ma c'è un modo semplice per saltare l'editor:EDITOR=true git rebase --autosquash -i
joeytwiddle

Il secondo passaggio non funziona per me, dicendo: per favore specifica quale ramo vuoi ribasare.
djangonaut il

git rebase --autosquash -i HEAD ~ 2 (dove 2 è il numero di commit incollati +1 che desideri modificare.
Sérgio

6

AGGIORNAMENTO: una versione più pulita dello script ora può essere trovata qui: https://github.com/deiwin/git-dotfiles/blob/docs/bin/git-fixup .

Ho cercato qualcosa di simile. Questo script Python sembra troppo complicato, quindi ho messo insieme la mia soluzione:

Innanzitutto, i miei alias git sono così (presi in prestito da qui ):

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

Ora la funzione bash diventa abbastanza semplice:

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
}

Questo codice innanzitutto mette in scena tutte le modifiche correnti (puoi rimuovere questa parte, se desideri organizzare i file da solo). Quindi crea la correzione (si può usare anche squash, se è quello che ti serve) commit. Dopodiché avvia un rebase interattivo con il --autosquashflag sul genitore del commit che dai come argomento. Questo aprirà il tuo editor di testo configurato, in modo da poter verificare che tutto sia come ti aspetti e semplicemente chiudendo l'editor terminerà il processo.

La if [[ "$1" == HEAD* ]]parte (presa in prestito da qui ) viene utilizzata, perché se si utilizza, ad esempio, HEAD ~ 2 come riferimento per il commit (il commit con cui si desidera correggere le modifiche correnti), HEAD verrà spostato dopo che è stato creato il commit di correzione e dovresti usare HEAD ~ 3 per fare riferimento allo stesso commit.


Alternativa interessante. +1
VonC

4

Puoi evitare la fase interattiva utilizzando un editor "nullo":

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

Questo userà /bin/truecome editor, invece di /usr/bin/vim. Accetta sempre qualsiasi cosa suggerisca git, senza chiedere conferma.


In effetti, questo è esattamente quello che ho fatto nella mia risposta allo script Python "risposta originale" del 30 settembre 2010 (nota come in fondo allo script, dice call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i" ...).
Frerich Raabe

4

Quello che mi ha davvero infastidito del flusso di lavoro di riparazione è stato il fatto che dovevo capire da solo in quale impegno volevo schiacciare il cambiamento ogni volta. Ho creato un comando "git fixup" che aiuta con questo.

Questo comando crea commit di correzione, con la magia aggiunta che utilizza git-deps per trovare automaticamente il commit pertinente, quindi il flusso di lavoro spesso si riduce 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

Questo funziona solo se le modifiche staged possono essere attribuite in modo inequivocabile a un particolare commit sull'albero di lavoro (tra master e HEAD). Trovo che questo sia il caso molto spesso per il tipo di piccole modifiche per cui lo uso, ad esempio errori di battitura nei commenti o nomi di metodi appena introdotti (o rinominati). In caso contrario, verrà visualizzato almeno un elenco di commit candidati.

Lo uso molto nel mio flusso di lavoro quotidiano, per integrare rapidamente piccole modifiche a linee precedentemente modificate nei commit nel mio ramo di lavoro. La sceneggiatura non è così bella come potrebbe essere, ed è scritta in zsh, ma ha svolto il lavoro abbastanza bene per me da un bel po 'di tempo ora che non ho mai sentito il bisogno di riscriverla:

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


2

È possibile creare una correzione per un particolare file utilizzando questo 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' -"

Se hai apportato alcune modifiche myfile.txtma non desideri inserirle in un nuovo commit, git fixup-file myfile.txtcreerà un fixup!per il commit in cui è myfile.txtstato modificato l'ultima volta, e poi lo farà rebase --autosquash.


Molto intelligente, anche se preferirei che git rebasenon fosse chiamato automaticamente.
hurikhan77

2

commit --fixupe rebase --autosquashsono fantastici, ma non fanno abbastanza. Quando ho una sequenza di commit A-B-Ce scrivo altre modifiche nel mio albero di lavoro che appartengono a uno o più di quei commit esistenti, devo guardare manualmente la cronologia, decidere quali modifiche appartengono a quali commit, metterli in scena e creare il fixup!impegna. Ma git ha già accesso a informazioni sufficienti per essere in grado di fare tutto ciò per me, quindi ho scritto uno script Perl che fa proprio questo.

Per ogni pezzo nello git diffscript usa git blameper trovare il commit che ha toccato per ultimo le righe pertinenti e chiama git commit --fixupper scrivere i fixup!commit appropriati , essenzialmente facendo la stessa cosa che stavo facendo manualmente prima.

Se lo trovi utile, sentiti libero di migliorarlo e ripeterlo e forse un giorno avremo una funzionalità del genere git appropriato. Mi piacerebbe vedere uno strumento in grado di capire come dovrebbe essere risolto un conflitto di unione quando è stato introdotto da un rebase interattivo.


Avevo anche dei sogni sull'automazione: git dovrebbe semplicemente provare a metterlo il più indietro possibile nella storia, senza che la patch si rompa. Ma il tuo metodo è probabilmente più sano. È bello vedere che ci hai provato. Lo proverò! (Ovviamente ci sono momenti in cui la patch di correzione appare altrove nel file e solo lo sviluppatore sa a quale commit appartiene. O forse un nuovo test nella suite di test potrebbe aiutare la macchina a capire dove dovrebbe andare la correzione.)
joeytwiddle

1

Ho scritto una piccola funzione di shell chiamata gcfper eseguire automaticamente il commit di correzione e il rebase:

$ 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.

Ad esempio, puoi patchare il secondo commit prima dell'ultimo con: gcf HEAD~~

Ecco la funzione . Puoi incollarlo nel tuo file~/.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'

Utilizza --autostash per nascondere e visualizzare eventuali modifiche non salvate, se necessario.

--autosquashrichiede un --interactiverebase, ma evitiamo l'interazione usando un dummy EDITOR.

--no-fork-pointprotegge i commit dall'essere rilasciati silenziosamente in rare situazioni (quando hai biforcato un nuovo ramo e qualcuno ha già ribasato i commit passati).


0

Non sono a conoscenza di un modo automatizzato, ma ecco una soluzione che potrebbe essere più facile da botizzare dall'uomo:

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

Vedere la mia risposta per uno script che ne approfitta per implementare un git fixupcomando.
Frerich Raabe

@Frerich Raabe: suona bene, non lo so--autosquash
Tobias Kienzler

0

Consiglierei https://github.com/tummychow/git-absorb :

Piazzola ascensore

Hai un ramo di funzionalità con pochi commit. Il tuo compagno di squadra ha esaminato il ramo e ha segnalato alcuni bug. Hai correzioni per i bug, ma non vuoi inserirli tutti in un commit opaco che dice correzioni, perché credi nei commit atomici. Invece di trovare manualmente SHA di commit per git commit --fixupo eseguire un rebase interattivo manuale, fai questo:

  • git add $FILES_YOU_FIXED

  • git absorb --and-rebase

  • o: git rebase -i --autosquash master

git absorbidentificherà automaticamente quali commit sono sicuri da modificare e quali modifiche indicizzate appartengono a ciascuno di questi commit. Quindi scriverà fixup! si impegna per ciascuna di queste modifiche. Puoi controllarne l'output manualmente se non ti fidi, quindi piegare le correzioni nel tuo ramo delle funzionalità con la funzionalità autosquash integrata di git.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.