How can I easily fixup a past commit?
UPDATED ANSWER
A while ago, a new --fixup
argument was added to git commit
which can be used to construct a commit with a log message suitable for git rebase --interactive --autosquash
. So the simplest way to fixup a past commit is now:
$ 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
ORIGINAL ANSWER
Here's a little Python script I wrote a while ago which implements this git fixup
logic I hoped for in my original question. The script assumes that you staged some changes and then applies those changes to the given commit.
NOTE: This script is Windows-specific; it looks for git.exe
and sets the GIT_EDITOR
environment variable using set
. Adjust this as needed for other operating systems.
Using this script I can implement precisely the 'fix broken sources, stage fixes, run git fixup ' workflow I asked for:
#!/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)
A bit late to the party, but here is a solution that works as the author imagined.
Add this to your .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' -"
Example usage:
git add -p
git fixup HEAD~5
However if you have unstaged changes, you must stash them before the rebase.
git add -p
git stash --keep-index
git fixup HEAD~5
git stash pop
You could modify the alias to stash automatically, instead of giving a warning. However, if the fixup does not apply cleanly you will need pop the stash manually after fixing the conflicts. Doing both the saving and popping manually seems more consistent and less confusing.
What I do is:
git add ... # Add the fix. git commit # Committed, but in the wrong place. git rebase -i HEAD~5 # Examine the last 5 commits for rebasing.
Your editor will open with a list of the last 5 commits, ready to be meddled with. Change:
pick 08e833c Good change 1. pick 9134ac9 Good change 2. pick 5adda55 Bad change! pick 400bce4 Good change 3. pick 2bc82n1 Fix of bad change.
...to:
pick 08e833c Good change 1. pick 9134ac9 Good change 2. pick 5adda55 Bad change! f 2bc82n1 Fix of bad change. # Move up, and change 'pick' to 'f' for 'fixup'. pick 400bce4 Good change 3.
Save & exit your editor, and the fix will be squished back into the commit it belongs with.
After you've done that a few times, you'll do it in seconds in your sleep. Interactive rebasing is the feature that really sold me on git. It's incredibly useful for this and more...
To fixup one commit :
git commit --fixup a0b1c2d3 .
git rebase --autosquash -i HEAD~2
where a0b1c2d3 is commit that you want fixup and where 2 is the number of commits +1 pasted that you want to change.
Note: git rebase --autosquash without -i doesn't worked but with -i worked, which is strange.