How can I easily fixup a past commit?
Solution 1:
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)
Solution 2:
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...
Solution 3:
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.