git rebase interactive: squash merge commits together

29,519

Solution 1

This is an old topic, but I just ran across it while looking for similar information.

A trick similar to the one described in Subtree octopus merge is a really good solution to this type of problem:

git checkout my-feature
git reset --soft Y
git rev-parse f > .git/MERGE_HEAD
git commit

That will take the index as it exists at the tip of my-feature, and use it to create a new commit off of Y, with 'f' as a second parent. The result is the same as if you'd never performed M1, but gone straight to performing M2.

Solution 2

if you haven't published the last two merge commits, you could do a reset and a simple merge.

git reset --hard Y
git merge stable

Solution 3

I came to this topic wanting to squash a single merge commit; so my answer is not that useful to the original question.

               X
                \   
                 \  
a --- b --- c --- M1 (subtree merge)

What I wanted was to rebase the M1 merge and squash everything as a single commit on top of b.

a --- b --- S (include the changes from c, X and M1)

I tried all kinds of different combinations but this is what worked:

git checkout -b rebase b (checkout a working branch at point b)
git merge --squash M1

This will apply the changes into the index where they can be committed git commit

Solution 4

Using the tree object of the original merge commit will ensure the content is left unchanged. commit-tree can be used to make a new commit with the desired parents and the same content. But, for fmt-merge-msg to produce a normal merge message, you'll need to first soft reset back to Y. Here is everything packaged up with a generic recipe:

parent2=$(git rev-parse f)
parent1=Y
merge_branch=stable
tree=$(git rev-parse HEAD^{tree})
git reset --soft $parent1
commit=$(echo $parent2$'\t\t'"branch $merge_branch" | git fmt-merge-msg | git commit-tree -p $parent1 -p $parent2 -F - $tree)
git reset --hard $commit

Here is an alias that can be put in your ~/.gitconfig:

[alias]
    remerge = "!f() { p1=$1; p2=`git rev-parse $2`; t=`git rev-parse HEAD^{tree}`; git reset --soft $p1; git reset --hard `echo $p2$'\t\t'"branch ${3:-$2}" | git fmt-merge-msg | git commit-tree -p $p1 -p $p2 -F - $t`; }; f"

To enable:

git remerge <parent1-rev> <parent2-rev> [<parent2-branch>]

Solution 5

None of the mentioned methods works for me with a recent git version. In my case the following did the trick:

git reset --soft Y
git reset --hard $(git commit-tree $(git write-tree) -p HEAD -p stable < commit_msg)

You'll have to write the commit message to the file commit_msg first, though.

Share:
29,519
Mildred
Author by

Mildred

Updated on December 22, 2021

Comments

  • Mildred
    Mildred over 2 years

    I wanted to have a simple solution to squash two merge commits together during an interactive rebase.

    My repository looks like:

       X --- Y --------- M1 -------- M2 (my-feature)
      /                 /           /
     /                 /           /
    a --- b --- c --- d --- e --- f (stable)
    

    That is, I have a my-feature branch that has been merged twice recently, with no real commits in between. I don't just want to rebase the my-feature branch since it is a published branch of its own, I just want to squash together the last two merge commits into one (haven't published those commits yet)

       X --- Y ---- M (my-feature)
      /            /
     /            /
    a --- ... -- f (stable)
    

    I tried:

    git rebase -p -i M1^
    

    But I got:

    Refusing to squash a merge: M2
    

    What I finally did is:

    git checkout my-feature
    git reset --soft HEAD^  # remove the last commit (M2) but keep the changes in the index
    git commit -m toto      # redo the commit M2, this time it is not a merge commit
    git rebase -p -i M1^    # do the rebase and squash the last commit
    git diff M2 HEAD        # test the commits are the same
    

    Now, the new merge commit is not considered a merge commit anymore (it only kept the first parent). So:

    git reset --soft HEAD^               # get ready to modify the commit
    git stash                            # put away the index
    git merge -s ours --no-commit stable # regenerate merge information (the second parent)
    git stash apply                      # get the index back with the real merge in it
    git commit -a                        # commit your merge
    git diff M2 HEAD                     # test that you have the same commit again
    

    But this can get complicated if I have many commits, do you have a better solution ? Thanks.

    Mildred