Wie kann ich ein früheres Commit einfach reparieren?

115

Ich habe gerade gelesen, wie ich eine einzelne Datei in einem früheren Commit in Git geändert habe, aber leider "ordnet" die akzeptierte Lösung die Commits neu, was nicht das ist, was ich will. Hier ist meine Frage:

Hin und wieder bemerke ich einen Fehler in meinem Code, während ich an einer (nicht verwandten) Funktion arbeite. Ein kurzer git blameBlick zeigt dann, dass der Fehler vor einigen Commits eingeführt wurde (ich begebe ziemlich viel, daher ist es normalerweise nicht das letzte Commit, das den Fehler verursacht hat). An diesem Punkt mache ich normalerweise Folgendes:

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

Dies passiert jedoch so oft, dass die obige Sequenz nervt. Besonders die "interaktive Rebase" ist langweilig. Gibt es eine Verknüpfung zu der obigen Sequenz, mit der ich ein beliebiges Commit in der Vergangenheit mit den inszenierten Änderungen ändern kann? Ich bin mir vollkommen bewusst, dass dies die Geschichte verändert, aber ich mache so oft Fehler, dass ich wirklich gerne so etwas hätte

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.

Vielleicht ein intelligentes Skript, das Commits mit Sanitärwerkzeugen oder so umschreiben kann?

Frerich Raabe
quelle
Was meinst du mit "Nachbestellungen" der Commits? Wenn Sie die Geschichte sind zu ändern dann alle Commits seit den geändertenen Commits haben , anders zu sein, aber die akzeptierte Antwort auf die verknüpfte Frage nicht erneut um die Commits in sinnvollem Sinne.
CB Bailey
1
@Charles: Ich meinte die Neuordnung wie in: Wenn ich bemerke, dass HEAD ~ 5 das fehlerhafte Commit ist, wird HEAD (die Spitze des Zweigs) durch das Akzeptieren der Antwort in der verknüpften Frage zum festen Commit. Ich möchte jedoch, dass HEAD ~ 5 das feste Commit ist - was Sie erhalten, wenn Sie eine interaktive Rebase verwenden und ein einzelnes Commit zum Fixieren bearbeiten.
Frerich Raabe
Ja, aber dann wird der Befehl rebase den Master erneut auschecken und alle nachfolgenden Commits auf das feste Commit zurücksetzen. Fahren Sie nicht so rebase -i?
CB Bailey
Eigentlich gibt es ein potenzielles Problem mit dieser Antwort, ich denke es sollte sein rebase --onto tmp bad-commit master. Wie geschrieben wird versucht, das fehlerhafte Festschreiben auf den festen Festschreibungsstatus anzuwenden.
CB Bailey
Hier ist ein weiteres Tool zur Automatisierung des Fixup / Rebase-Prozesses: stackoverflow.com/a/24656286/1058622
Mika Eloranta

Antworten:

164

AKTUALISIERTE ANTWORT

Vor einiger Zeit wurde ein neues --fixupArgument hinzugefügt, mit git commitdem ein Commit mit einer geeigneten Protokollnachricht erstellt werden kann git rebase --interactive --autosquash. Der einfachste Weg, um ein früheres Commit zu reparieren, ist jetzt:

$ 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

URSPRÜNGLICHE ANTWORT

Hier ist ein kleines Python-Skript, das ich vor einiger Zeit geschrieben habe und das diese git fixupLogik implementiert, auf die ich in meiner ursprünglichen Frage gehofft hatte. Das Skript geht davon aus, dass Sie einige Änderungen vorgenommen haben, und wendet diese Änderungen dann auf das angegebene Commit an.

HINWEIS : Dieses Skript ist Windows-spezifisch. Es sucht git.exeund setzt die GIT_EDITORUmgebungsvariable mit set. Passen Sie dies nach Bedarf für andere Betriebssysteme an.

Mit diesem Skript kann ich genau den Workflow implementieren, bei dem defekte Quellen behoben, Bühnenkorrekturen durchgeführt, Git-Korrekturen ausgeführt wurden, nach dem ich gefragt habe:

#!/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
quelle
2
Sie könnten git stashund git stash popum Ihre Rebase verwenden, um kein sauberes Arbeitsverzeichnis mehr zu benötigen
Tobias Kienzler
@TobiasKienzler: Informationen zur Verwendung git stashund git stash pop: Du hast Recht, aber leider git stashist viel langsamer auf Windows , als es unter Linux oder OS / X ist. Da mein Arbeitsverzeichnis normalerweise sauber ist, habe ich diesen Schritt weggelassen, um den Befehl nicht zu verlangsamen.
Frerich Raabe
Ich kann das bestätigen, insbesondere wenn ich an einer Netzwerkfreigabe arbeite: - /
Tobias Kienzler
1
Nett. Ich habe es versehentlich getan git rebase -i --fixup, und es wurde vom festgelegten Commit als Ausgangspunkt neu erstellt, sodass das sha-Argument in meinem Fall nicht benötigt wurde.
Fwielstra
1
Für Leute, die --autosquash häufig verwenden, kann es nützlich sein, es als Standardverhalten git config --global rebase.autosquash true
festzulegen
31

Was ich tue ist:

git add ... # Füge das Update hinzu.
git commit # Committed, aber am falschen Ort.
git rebase -i HEAD ~ 5 # Untersuche die letzten 5 Commits auf Rebasing.

Ihr Editor wird mit einer Liste der letzten 5 Commits geöffnet, in die Sie sich einmischen können. Veränderung:

Wählen Sie 08e833c Gute Änderung 1.
Pick 9134ac9 Gute Änderung 2.
wähle 5adda55 Schlechtes Wechselgeld!
wähle 400bce4 Gute Veränderung 3.
Wählen Sie 2bc82n1.

...zu:

Wählen Sie 08e833c Gute Änderung 1.
Pick 9134ac9 Gute Änderung 2.
wähle 5adda55 Schlechtes Wechselgeld!
f 2bc82n1 Fehlerbehebung behoben. # Bewegen Sie sich nach oben und ändern Sie 'pick' für 'fixup' in 'f'.
wähle 400bce4 Gute Veränderung 3.

Speichern und beenden Sie Ihren Editor, und das Update wird wieder in das Commit eingefügt, zu dem es gehört.

Nachdem Sie dies einige Male getan haben, werden Sie es in Sekundenschnelle im Schlaf tun. Interaktives Rebasing ist das Feature, das mich bei Git wirklich verkauft hat. Es ist unglaublich nützlich für dieses und mehr ...

Kris Jenkins
quelle
9
Natürlich können Sie HEAD ~ 5 in HEAD ~ n ändern, um weiter zurück zu gehen. Sie möchten sich nicht in einen Verlauf einmischen, den Sie in den Upstream verschoben haben. Daher gebe ich normalerweise "git rebase -i origin / master" ein, um sicherzustellen, dass ich nur den nicht gepushten Verlauf ändere.
Kris Jenkins
4
Das ist sehr ähnlich zu dem, was ich immer getan habe. FWIW, Sie könnten an dem --autosquashSchalter für interessiert sein git rebase, der die Schritte im Editor automatisch für Sie neu anordnet . In meiner Antwort finden Sie ein Skript, das dies nutzt, um einen git fixupBefehl zu implementieren .
Frerich Raabe
Ich wusste nicht, dass Sie Commit-Hashes einfach nachbestellen können, nett!
Aaron Franke
Das ist großartig! Nur um sicherzustellen, dass alle Rebase-Arbeiten ausgeführt werden, ist ein separater Feature-Zweig vorhanden. Und nicht mit gewöhnlichen Zweigen wie Meister zu spielen.
Jay Modi
22

Ein bisschen spät zur Party, aber hier ist eine Lösung, die so funktioniert, wie es sich der Autor vorgestellt hat.

Fügen Sie dies Ihrer .gitconfig hinzu:

[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' -"

Anwendungsbeispiel:

git add -p
git fixup HEAD~5

Wenn Sie jedoch nicht bereitgestellte Änderungen vorgenommen haben, müssen Sie diese vor der erneuten Basis speichern.

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

Sie können den Alias ​​so ändern, dass er automatisch gespeichert wird, anstatt eine Warnung zu geben. Wenn das Update jedoch nicht sauber angewendet wird, müssen Sie den Stash nach dem Beheben der Konflikte manuell öffnen. Das manuelle Speichern und Poppen erscheint konsistenter und weniger verwirrend.

dschlyter
quelle
Das ist ziemlich hilfreich. Für mich ist es am häufigsten, die Änderungen am vorherigen Commit zu korrigieren. Daher habe git fixup HEADich einen Alias ​​für erstellt. Ich könnte auch eine Änderung dafür verwenden, nehme ich an.
Heuschrecke
Danke dir! Ich verwende es auch am häufigsten beim letzten Commit, habe aber einen anderen Alias ​​für eine schnelle Änderung. amend = commit --amend --reuse-message=HEADDann können Sie einfach den Editor für die Festschreibungsnachricht eingeben git amendoder git amend -aüberspringen.
Dschlyter
3
Das Problem bei der Änderung ist, dass ich mich nicht daran erinnere, wie ich es buchstabieren soll. Ich muss immer denken, ob es geändert oder geändert wird und das ist nicht gut.
Heuschrecke
11

So beheben Sie ein Commit:

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

Dabei ist a0b1c2d3 ein Commit, das Sie korrigieren möchten, und wobei 2 die Anzahl der eingefügten Commits +1 ist, die Sie ändern möchten.

Hinweis: git rebase --autosquash ohne -i hat nicht funktioniert, aber mit -i hat funktioniert, was seltsam ist.

Sérgio
quelle
2016 und --autosquashohne -ifunktioniert immer noch nicht.
Jonathan Cross
1
Wie die Manpage sagt: Diese Option ist nur gültig, wenn die Option --interactive verwendet wird. Aber es gibt eine einfache Möglichkeit, den Editor zu überspringen:EDITOR=true git rebase --autosquash -i
Joeytwiddle
Der zweite Schritt funktioniert bei mir nicht und lautet: Bitte geben Sie an, gegen welchen Zweig Sie eine Basis erstellen möchten.
Djangonaut
git rebase --autosquash -i HEAD ~ 2 (wobei 2 die Anzahl der eingefügten Commits +1 ist, die Sie ändern möchten.
Sérgio
6

UPDATE: Eine übersichtlichere Version des Skripts finden Sie jetzt hier: https://github.com/deiwin/git-dotfiles/blob/docs/bin/git-fixup .

Ich habe nach etwas Ähnlichem gesucht. Dieses Python-Skript scheint jedoch zu kompliziert zu sein, daher habe ich meine eigene Lösung zusammengestellt:

Erstens sehen meine Git-Aliase so aus (von hier entlehnt ):

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

Jetzt wird die Bash-Funktion ganz einfach:

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
}

Dieser Code führt zuerst alle aktuellen Änderungen durch (Sie können diesen Teil entfernen, wenn Sie die Dateien selbst bereitstellen möchten). Anschließend wird das Fixup erstellt (Squash kann auch verwendet werden, wenn Sie dies benötigen). Danach startet es eine interaktive Rebase mit dem--autosquash Flag auf dem übergeordneten Commit , das Sie als Argument . Dadurch wird Ihr konfigurierter Texteditor geöffnet, sodass Sie überprüfen können, ob alles wie erwartet ist. Durch einfaches Schließen des Editors wird der Vorgang abgeschlossen.

Der if [[ "$1" == HEAD* ]]Teil (von hier ausgeliehen ) wird verwendet. Wenn Sie beispielsweise HEAD ~ 2 als Commit-Referenz (das Commit, mit dem Sie aktuelle Änderungen korrigieren möchten) verwenden, wird der HEAD verschoben, nachdem das Fixup-Commit erstellt wurde und Sie müssten HEAD ~ 3 verwenden, um auf dasselbe Commit zu verweisen.

Deiwin
quelle
Interessante Alternative. +1
VonC
4

Sie können die interaktive Phase vermeiden, indem Sie einen "Null" -Editor verwenden:

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

Dies wird /bin/trueanstelle von als Editor verwendet /usr/bin/vim. Es akzeptiert immer alles, was Git vorschlägt, ohne Aufforderung.

joeytwiddle
quelle
Genau das habe ich in meiner Python-Skriptantwort "Originalantwort" vom 30. September 2010 getan (beachten Sie, wie am Ende des Skripts steht call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i" ...).
Frerich Raabe
4

Was mich am Fixup-Workflow wirklich störte, war, dass ich selbst herausfinden musste, in welches Commit ich die Änderung jedes Mal einfließen lassen wollte. Ich habe einen "git fixup" Befehl erstellt, der dabei hilft.

Dieser Befehl erstellt Fixup-Commits mit der zusätzlichen Magie, dass Git-Deps verwendet werden , um das relevante Commit automatisch zu finden. Daher läuft der Workflow häufig auf Folgendes hinaus :

# 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

Dies funktioniert nur, wenn die bereitgestellten Änderungen eindeutig einem bestimmten Commit im Arbeitsbaum (zwischen Master und HEAD) zugeordnet werden können. Ich finde, dass dies sehr oft bei kleinen Änderungen der Fall ist, für die ich dies verwende, z. B. Tippfehler in Kommentaren oder Namen neu eingeführter (oder umbenannter) Methoden. Ist dies nicht der Fall, wird mindestens eine Liste der Kandidaten-Commits angezeigt.

Ich benutze diese viel in meinem täglichen Arbeitsablauf, um schnell kleine Änderungen an zuvor geänderten Zeilen in Commits auf meinem Arbeitszweig zu integrieren. Das Skript ist nicht so schön wie es sein könnte, und es ist in zsh geschrieben, aber es hat die Arbeit für mich schon eine Weile gut genug gemacht, da ich nie das Bedürfnis hatte, es neu zu schreiben:

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

Valodim
quelle
2

Mit diesem Alias können Sie eine Korrektur für eine bestimmte Datei erstellen .

[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' -"

Wenn Sie einige Änderungen vorgenommen haben, diese myfile.txtaber nicht in ein neues Commit einfügen möchten, git fixup-file myfile.txterstellen Sie ein fixup!für das Commit, in dem myfile.txtzuletzt Änderungen vorgenommen wurden, und dann rebase --autosquash.

Alvaro
quelle
Sehr klug, obwohl ich es vorziehen würde, dass git rebasedas nicht automatisch aufgerufen wurde.
Hurikhan77
2

commit --fixupund rebase --autosquashsind großartig, aber sie tun nicht genug. Wenn ich eine Folge von Commits habe A-B-Cund weitere Änderungen in meinen Arbeitsbaum schreibe, die zu einem oder mehreren dieser vorhandenen Commits gehören, muss ich den Verlauf manuell betrachten, entscheiden, welche Änderungen zu welchen Commits gehören, sie inszenieren und die erstellen fixup!begeht. Aber git hat bereits Zugriff auf genügend Informationen, um all das für mich tun zu können. Deshalb habe ich ein Perl-Skript geschrieben, das genau das tut.

Für jeden Teil im git diffSkript wird verwendet git blame, um das Commit zu finden, das zuletzt die relevanten Zeilen berührt hat, und git commit --fixupum die entsprechenden fixup!Commits zu schreiben , wobei im Wesentlichen das gleiche getan wird, was ich zuvor manuell getan habe.

Wenn Sie es nützlich finden, können Sie es gerne verbessern und wiederholen. Vielleicht erhalten wir eines Tages eine solche Funktion git. Ich würde gerne ein Tool sehen, das verstehen kann, wie ein Zusammenführungskonflikt gelöst werden sollte, wenn er durch eine interaktive Rebase eingeführt wurde.

Oktalist
quelle
Ich hatte auch Träume von Automatisierung: Git sollte einfach versuchen, sie so weit wie möglich in die Geschichte zurückzubringen, ohne dass der Patch kaputt geht. Aber Ihre Methode ist wahrscheinlich vernünftiger. Schön zu sehen, dass Sie es versucht haben. Ich werde es ausprobieren! (Natürlich gibt es Zeiten, in denen der Fixup-Patch an einer anderen Stelle in der Datei angezeigt wird und nur der Entwickler weiß, zu welchem ​​Commit er gehört. Oder ein neuer Test in der Testsuite kann dem Computer helfen, herauszufinden, wohin der Fix gehen soll.)
Joeytwiddle
1

Ich habe eine kleine Shell-Funktion geschrieben, die aufgerufen wird gcf, um das Fixup-Commit und die Rebase automatisch durchzuführen:

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

Beispielsweise können Sie das zweite Commit vor dem letzten mit folgendem Patch patchen: gcf HEAD~~

Hier ist die Funktion . Sie können es in Ihre einfügen~/.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'

Es wird verwendet --autostash, um nicht festgeschriebene Änderungen zu speichern und bei Bedarf zu speichern.

--autosquasherfordert eine --interactiveRebase, aber wir vermeiden die Interaktion durch die Verwendung eines Dummys EDITOR.

--no-fork-pointschützt Commits vor dem stillschweigenden Löschen in seltenen Situationen (wenn Sie einen neuen Zweig verlassen haben und jemand bereits frühere Commits neu basiert hat).

joeytwiddle
quelle
0

Mir ist kein automatisierter Weg bekannt, aber hier ist eine Lösung, die sich möglicherweise leichter von Menschen botieren lässt:

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
quelle
In meiner Antwort finden Sie ein Skript, das dies nutzt, um einen git fixupBefehl zu implementieren .
Frerich Raabe
@ Frerich Raabe: Hört sich gut an, ich weiß nichts über--autosquash
Tobias Kienzler
0

Ich würde https://github.com/tummychow/git-absorb empfehlen :

Aufzugspräsentation

Sie haben einen Feature-Zweig mit einigen Commits. Ihr Teamkollege hat den Zweig überprüft und auf einige Fehler hingewiesen. Sie haben Korrekturen für die Fehler, aber Sie möchten nicht alle in ein undurchsichtiges Commit verschieben, das Korrekturen enthält, weil Sie an atomare Commits glauben. Gehen Sie folgendermaßen vor git commit --fixup, anstatt Commit-SHAs für manuell zu finden oder eine manuelle interaktive Rebase auszuführen:

  • git add $FILES_YOU_FIXED

  • git absorb --and-rebase

  • oder: git rebase -i --autosquash master

git absorberkennt automatisch, welche Commits sicher geändert werden können und welche indizierten Änderungen zu jedem dieser Commits gehören. Es wird dann Fixup schreiben! legt für jede dieser Änderungen fest. Sie können die Ausgabe manuell überprüfen, wenn Sie ihr nicht vertrauen, und dann die Korrekturen mit der integrierten Autosquash-Funktion von git in Ihren Feature-Zweig einbinden.

Petski
quelle