git smash

A common thing I end up doing while working on code is to make a series of commits, and  then end up work changes in my working directory which I need to apply to an earlier revision in the history than the top-most one.

One common way to do this is to make a temporary commit with those changes, then use git rebase -i and move that commit below the one I want to amend and choose fixup to have it applied.

But that’s annoying manual work. There’s a more fun way. I have this script in my path as git-smash, it takes a revision as a single argument, e.g. git smash deadbeef:

git reset --keep "$1"
EDITOR=true git commit -a --amend
git checkout HEAD@{2}
git rebase --onto HEAD@{1} HEAD@{2}

This resets the revision history, keeping local changes, back to the given revision. Unfortunately git reset doesn’t have a mode which preserves the index so we then have to use commit -a to capture all of the local changes.

Now we use the reflog (history of revisions in the working tree) to manipulate the tree back to the previous state, first checking out the revision that was two back (before the amended commit and the reset, i.e. where we began). Then we rebase that onto the revision one back (before the checkout, i.e. the amended revision) using the revision that’s now two back (before the checkout and commit, i.e. the original revision we changed).

Mental gymnastics over, this is the same as what we were doing before, just in one handy command.

Git still sucks though.

7 thoughts on “git smash

  1. Monty Taylor

    Just for the sake of completeness, there’s another way to go about that:

    git stash
    git rebase -i origin/master
    # change pick to edit on the commit you want to apply it to
    git stash pop
    git commit -a –amend
    git rebase –continue

    This will allow you to edit the commit that is the target you’re trying to amend, and will carry the edit down through its dependent changes pretty cleanly.

    Of course – if you have pushed your changes anywhere that someone may have consumed them, you don’t want to do either your method or my method – unless of course you’re using gerrit, in which case you can happily re-submit them to gerrit and the right thing will happen.

    1. scott Post author

      I’m pretty sure you can, doing the git rebase -i, add a line saying fixup STASH@{0} below the commit you want amended; which drops three commands. The downside here is that you can end up rebasing onto a later origin/master which may not be what you want.

      1. Daniel

        I’m not sure, but something like this might work:

        git rebase -i $(git merge-base HEAD origin/master)

  2. daniels

    Or another way to do it:
    [hack one]
    git commit -a -m ‘did some stuff’
    [hack two]
    git commit -a -m ‘some other stuff’
    [hack three]
    git commit -a -m ‘fixup! did some stuff’
    git rebase -i –autosquash

    The autosquash bit will automatically move commit #3 as a fixup of the ‘did some stuff’ commit, with prefix matching. Setting rebase.autosquash = yes in the config will make this the default behaviour on rebase -i.

  3. Jo Liss

    The advantage of using –autosquash might be that if the rebase goes wrong, you can easily back out using rebase –abort, where with git-smash, you’re stuck in the middle of your history.

    That said, I appreciate that the smash command you posted is faster than committing followed by rebase -i.

Comments are closed.