Squashing multiple commits that contain merge with main commits

140 views Asked by At

Suppose we have the following scenario:

  • main branch
  • feature branch for new improvements

In the feature branch the commit history looks like this (main branch updates during the development process of feature branch and we want to keep feature branch up to date with main):

  • E <- (HEAD, feature branch)
  • Merge main into feature
  • D
  • C
  • Merge main into feature
  • B
  • A <- first commit on feature branch

Now we want to squash all of these commits into a single commit. When I tried to use git rebase -i HEAD~7 => a list of 9 lines which contains the new commits from feature branch (A, B, C, D, E) and also the commits that were merged from main (not the merge commits the actual commits).

  • pick A
  • pick B
  • pick New_commit_from_main_1
  • pick C
  • pick D
  • pick New_commit_from_main_2
  • pick New_commit_from_main_3
  • pick E

When I tried to use git rebase -i main => a list of 5 commits that does not contain the merge commits or the commits taken from main as in the above example

  • pick A
  • pick B
  • pick C
  • pick D
  • pick E

I don't understand why is this happening. I would expect the following list of commits:

  • pick A
  • pick B
  • pick Merge main into feature
  • pick C
  • pick D
  • pick Merge main into feature
  • pick E

git rebase -i main git rebase -i HEAD~7

2

There are 2 answers

0
LeGEC On BEST ANSWER

The default behavior of git rebase is to ignore merges (as in: completely drop them from the list of rebased commits, be it with or without the --interactive flag).

This behavior is pretty surprising when you are first confronted to it :)


If you want to handle merges in your rebase history, you can use the explicit -r|--rebase-merges option :

-r, --rebase-merges[=(rebase-cousins|no-rebase-cousins)]

By default, a rebase will simply drop merge commits from the todo list, and put the rebased commits into a single, linear branch. With --rebase-merges, the rebase will instead try to preserve the branching structure within the commits that are to be rebased, by recreating the merge commits. Any resolved merge conflicts or manual amendments in these merge commits will have to be resolved/re-applied manually.

[...]


Another option could be to try to squash all the feature commits into 1, then add the merge result on top of it.

If the merges from master didn't result in a lot of conflicts, then this should be doable :

  1. write down the current hash of your E commit,
  2. run git rebase -i HEAD~7, remove the commits coming from master from that list, mark commits B .. E to squash,
  3. save & exit, have git rebase do its job

-> if you don't have too many conflicts, you should have the "content of feature" condensed in one single commit

  1. create a merge commit with the original content of E (the one you wrote down at step 1.)
git merge -n master
# if you have conflicts, ignore them:
git reset .
# restore the content from original E:
git restore -SW -s <original hash of commit E>
git commit
0
TTT On

Now we want to squash all of these commits into a single commit.

Because you have merges from main, in feature, one simple way to achieve the squash is:

# prep
git status # make sure you're clean! (Stash, commit, or undo changes first.)
git switch feature # check out your feature branch if not already
git rev-parse HEAD # save this commit hash, which is "E" in your question

# squash
git reset --hard main # change your branch to point to main
git merge --squash E # E = the commit ID you saved from the rev-parse command
git commit # All commit messages will be shown in the default message- rewrite as needed

The end result is a single commit containing only your changes, sitting on top of main as if you had done that in the first place.

See LeGEC's answer for more details regarding the fact that rebasing ignores merge commits by default. Note that when you run the command git rebase X, you are basically instructing Git to:

Take all of the (non-merge) commits that are on my branch, that aren't on X, and replay them one by one, in order, on top of X.

So, when X = HEAD~7, all the commits that are on your branch but not on HEAD~7 include all the (non-merge) commits you merged in from main. But when X = main, only your 5 commits from your branch aren't on main.