tags:

views:

84

answers:

3

I just read amending a single file in a past commit in git but unfortunately the accepted solution 'reorders' the commits, which is not what I want. So here's my question:

Every now and then, I notice a bug in my code while working on an (unrelated) feature. A quick git blame then reveals that the bug has been introduced a few commits ago (I commit quite a lot, so usually it's not the most recent commit which introduced the bug). At this point, I usually do this:

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

However, this happens so often that the above sequence is getting annoying. Especially the 'interactive rebase' is boring. Is there any shortcut to the above sequence, which lets me amend an arbitrary commit in the past with the staged changes? I'm perfectly aware that this changes the history, but I'm doing mistakes so often that I'd really love to have something like

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.

Maybe a smart script which can rewrite commits using plumbing tools or so?

A: 

I'm not aware of an automated way, but here's a solution that might by easier to human-botize:

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
See my response for a script which takes advantage of this to implement a `git fixup` command.
Frerich Raabe
@Frerich Raabe: sounds good, I dind't know about `--autosquash`
Tobias Kienzler
+2  A: 

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

Kris Jenkins
Obviously you can change HEAD~5 to HEAD~n to go back further. You won't want to meddle with any history you've pushed upstream, so I usually type 'git rebase -i origin/master' to ensure that I'm only changing unpushed history.
Kris Jenkins
This is much like what I always did; FWIW, you might be interested in the `--autosquash` switch for `git rebase`, which automatically reorders the steps in the editor for you. See my response for a script which takes advantage of this to implement a `git fixup` command.
Frerich Raabe
+1  A: 

For what it's worth, here's a little Python script I wrote a while ago which implements the 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, Popen, PIPE
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)

logmsg = Popen([git, "log", "-1", "--format=%s", broken_commit], stdout=PIPE).communicate()[0].strip()
call([git, "commit", "-m", "fixup! " + logmsg])
call(["set", "GIT_EDITOR=true", "&&", git, "rebase", "-i", "--autosquash", broken_commit + "~1"], shell=True)
Frerich Raabe
you could use `git stash` and `git stash pop` around your rebase to no longer require a clean working directory
Tobias Kienzler
I didn't test your script, but I followed the "instructions" and can +1 this
Tobias Kienzler