git interactive rebase squash into next commit

Viewed 5341

In Git I can use an interactive rebase to re-write history, this is great because in my feature branch I made a ton of commits with partially working code as I explored different refactors and ways of getting it done.

I'd like to squash a lot of the commits together before rebasing or merging the branch onto master.

Some made up commits in order from first (top) to bottom (last)

1. Initial commit on feature branch "Automatic coffee maker UI"
2. Add hot chocolate as product
3. Add tea as product. Products are now generic
4. Create in memory data store for adapter tests
5. Cry because I can't get entity framework to create a composite key. Integration tests broken.
6. Implemented composite key!!
7. All tests green and feature done!

Lets say I want to keep commits 3, 4 and 7.

Using rebase I want to "squash" commits

  • 1 and 2 go into 3.
  • 4 stays
  • 5 and 6 go into 7

Ideally in the interactive rebase I would do

1. squash
2. squash
3. pick (contains the work of 1 & 2)
4. pick 
5. squash
6. squash
7. pick (contains the work of 5 & 6)

But that is backwards because squash merges a commit with its previous commit. I can't figure out how to make it squash forward.

Am I being difficult, and should I accept that won't work (I'd rather get it working), or is there a way to accomplish this?

I'm invoking this command with

git checkout My-feature-branch
git rebase master -i

Then I'm editing the list of commits that come up, and trying to finish it by saving the file and editing the editor, which typically works for me.

5 Answers

It is indeed possible to squash a commit into the following commit during interactive rebase, and fully preserve the identity of the second of those commits (including author, date etc.).

The method is somewhat involved however, so native support by git rebase -i would still be appreciated nevertheless.

I'll demonstrate with just three commits aaaaaaa A, bbbbbbb B and ccccccc C, where we want to fold A into B and preserve B's identity: (The method easily generalises to more commits)

  • git rebase -i aaaaaaa^
  • Modify the edit script to stop after commit B and exit the editor:
    pick aaaaaaa A
    edit bbbbbbb B
    pick ccccccc C
    
  • When rebase has stopped after B, revert the latest commit twice, then continue:
    git revert HEAD
    git revert HEAD
    git rebase --continue
    
    Because reverting the revert restores the previous state, all following commits will apply cleanly. The sequence of B and Revert "B" together has no effect, but the first of those commits carries the full identity of commit B (in fact, at this stage it still is commit B).
    B and Revert "B" could be squashed together to form a no-op commit carrying the identity of commit B forward with a separate rebase -i --keep-empty aaaaaaa^. This is very useful and recommended to prevent bogus merge conflicts, especially in more complex cases. However we'll skip that step here and just keep the commits together.
    The important commit now is Revert "Revert "B"" which will carry all the changes originally made by B.
  • Now rebase -i aaaaaaa^ again to move A past both B and Revert "B", while squashing B into both Revert "B" and A:
    pick   bbbbbbb B
    squash b111111 Revert "B"
    squash a222222 A
    squash b333333 Revert "Revert "B""
    pick   c444444 C
    
    Because the sequence of B and Revert "B" has no effect, you can move any commits past it while creating only trivially resolvable merge conflicts.
  • When rebase stops with a merge conflict, use git mergetool with a tool that can automatically resolve trivial conflicts, and let the tools do just that.
  • Rebase will stop again to let you edit the commit message of the squashed commit. Edit to your taste.
  • git rebase --continue and you are done.

My go-to way to do this is a simply a reset plus a commit -C.

git rebase -i origin/main # or some other commit

Which brings up your editor, which I make look like this:

...
pick aafaa Merge me forward
edit bbfbb Into me
...

Then when the rebase stops at bbfbb, do

git reset --soft HEAD^
git commit --amend -C HEAD@{1} # could put an explicit commit here
git rebase --continue

it can also be put into a single rebase todo-list:

...
pick aafaa Squash me forward
pick bbfbb Into me
exec git reset --soft HEAD^ && git commit --amend -C HEAD@{1}
...

What you want is:

1. pick
2. squash
3. fixup
4. pick
5. squash
6. squash
7. fixup

Then when the new combined commits come up in the editor delete the lines for the picked commits and leave the fixups. The resulting feature-patches should have the commit message from the last commit in the series that way.

Related