gitrebasegit-stashgit-amendgit-stage

Quick, small, casual git rebase for only one or a few hunks: any facility?


I'm in the process of editing my source files to produce a new commit atop my dev branch. My changes are saved on disk but not yet staged.

Then I notice a small mistake introduced by a former commit, say HEAD~10. Once fixed, the result is only one (or a few) additional git diff hunk(s) on my disk.

I don't want these hunks to be recorded within the new commit, because they belong to HEAD~10. So I need to rebase. Here is my workflow then, but I'm not satisfied with it:

$ git stash # save my current work + the small fix
$ git rebase --interactive HEAD~11
# set first commit appearing in todo list to `edit` action

$ git stash pop # re-introduce my changes here (**may conflict**)
$ git add --patch fixed_files # or similar, to only stage relevant fix hunk(s)
$ git commit --amend # fix old commit
$ git stash # save my current work again
$ git rebase --continue # get back to my current commit (will likely *not* conflict)
$ git stash pop # back here with HEAD~10 fixed

What I'm not happy with is that the process is complicated, and that the first git stash pop line may introduce meaningless conflicts, even though I'm confident that no conflicts will occur during execution of the git rebase --continue line.

Is there a better way? Suppose I only have a few staged hunks in HEAD, can I easily introduce them earlier in my branch with some magical:

git amend-old-commit-then-rebase HEAD~10

yet keep my unstaged changes? (of course I'd be warned in case the inherent rebase does conflict)


Solution

  • Interactive rebase already has a built in way to deal with this. Let's demonstrate it. We start by checking out the tip commit of some existing branch name:

    git switch main
    

    then creating our feature or topic branch:

    git switch -c topic
    

    We begin working and make a commit that has a small error in it:

    ... edit ...
    git add frotz.c
    git commit
    

    Let's say that this is commit a123456 (we'll find the hash ID via git log later, or we'll use HEAD~10 as you did in your example; I just want something concrete to put here).

    We make more commits:

    ... edit and commit repeatedly ...
    

    then discover the mistake. We hold off on fixing the mistake just yet, making the commit we need to make:

    git commit
    

    and then we fix the mistake, git add, and use git commit --fixup:

    ... edit frotz.c to fix the mistake ...
    git add frotz.c
    git commit --fixup a123456   # or git commit --fixup HEAD~10
    

    The --fixup option directs git commit to use a specially-formatted commit message.

    Now that we have the fixup commit, we run:

    git rebase -i --autosquash main
    

    (since topic was based on main; use whatever you need here). The form with all the pick commands pops up, but when we look closely at it, we see that one of the lines doesn't say pick, it says fixup:

    pick a123456   commit subject line
    fixup b789abc  fixup! commit subject line
    pick 9876543   another commit subject
    ... and so on ...
    

    Writing out this set of commands (or not bothering since we don't need to change them around) and exiting the editor fires up the actual rebase, which ... absorbs the fixup commit into the bad commit, so that it's now all just one commit.

    The fixup command basically tells Git: squash this commit into the previous commit, while dropping this commit's log message entirely. Using squash instead of fixup tells Git: squash this commit into the previous one, but stop and give me a chance to write a new log message, showing me the two existing log messages. These are effectively the same except for the chance to edit the commit message.

    Since you didn't mention wanting to fix the commit message, we used --fixup here. To automatically get a "squash" command, use git commit --squash. In both cases git commit arranges for the future git rebase --autosquash to see a message that tells rebase rearrange the order of the pick commands, and change the second one to squash or fixup as appropriate.

    As always, you can also re-order and/or modify the instructions manually. (I usually do this rather than using git commit --fixup anyway, as I often want to do a lot of commit message rewording, and think about which commits to combine and what order to put them in.)