62

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

OrangeDog
  • 36,653
  • 12
  • 122
  • 207
Mildred
  • 3,887
  • 4
  • 36
  • 44
  • Well, when you do your second merge, you can always use `--squash` to avoid creating a commit, and then use `git commit --amend` to modify the previous merge. – Mildred Nov 12 '09 at 22:42
  • This won't work, it won't save the new version of the branch you merged from in the commit – Mildred Nov 13 '09 at 10:59

6 Answers6

61

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.

sleske
  • 81,358
  • 34
  • 189
  • 227
Matt
  • 611
  • 5
  • 2
  • Note that this kind of merge has nothing to do with a subtree or octopus merge. The blog you link just uses the technique to *combine* a subtree merge and an octopus merge into one merge commit (because git cannot directly do both merges in one go). – sleske Oct 30 '13 at 11:06
  • 1
    The drawback of this is that git won't be able to generate proper commit messages. I copied the messages from the old merge commits. Otherwise a nice, easy solution. – Yorick Sijsling Jul 28 '14 at 09:21
  • so much upvote! we just did a very difficult merge and my colleagues approach was to merge each commit from his tree to mine, one at a time. then came the squashing and us finding this solution. it is a shame you need to write your own commit message. – Sam Oct 21 '14 at 15:06
  • if you start the failed merge you can get what the merge message would have been and then use that – Sam Nov 25 '14 at 19:39
  • Pro tip: don't misread `f` (the commit id) as `-f` or you end up with `fatal: Corrupt MERGE_HEAD file (-f)` – OrangeDog Jan 20 '16 at 11:20
  • 16
    I prefer using `git update-ref MERGE_HEAD f` instead of `git rev-parse f > .git/MERGE_HEAD`, since it doesn't modify internal git files directly. Is it ok if I edit that part? – LuxDie Jan 31 '16 at 19:22
  • Wow!! So pretty, especially with the @LuxDie's comment. Thank you, you have saved me lots of time now – Kolay.Ne Jan 04 '21 at 13:18
8

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
bobDevil
  • 27,758
  • 3
  • 32
  • 30
  • 4
    Yes, but the merge was difficult, I'd rather merge as few changes as possible. I don't want to solve conflicts that I already have solved. – Mildred Nov 13 '09 at 11:02
  • 10
    if you don't want to re-resolve conflicts, you need to be "using" git-rerere (and by "using" I really mean "turning it on" 'cause git handles re-fixing identical conflicts automatically once this is enabled). – Brian Phillips Nov 13 '09 at 13:43
5

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

  • 1
    For this case, you can do `git diff b > diff.patch`, then `git checkout b`, `cat diff.patch | patch -p1` and then `git commit`. This works if the *merge* include resolutions. The original question is different; but I think you came here looking for the same thing as me. You can get the check-in message(s) with `git log` before hand. – artless noise Sep 22 '14 at 21:09
  • additional steps required if you want to have the situation on your master: `git checkout master && git reset --hard b && git rebase rebase`. This is just to reming myself. You could've chose another name for the branch than "rebase" :) – eis May 22 '17 at 16:12
2

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>]
  • Nice one, didn't know about `git fmt-merge-msg`, TIL. Why not copy the existing commit message, though, or get replace and filter-branch to do it for you? `git replace --graft my-feature{,~2,^2}; git filter-branch -- myfeature@^!`? – jthill Dec 13 '21 at 01:10
  • I assumed that unmodified merge commit messages were used to begin with and a standard one was OK for this replacement merge commit - as though the new simple merge was done as @bobDevil suggested. `git filter-branch` is new to me and looks like another very good solution to this problem. – Eric Mahurin Dec 13 '21 at 02:40
1

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.

Michael Wild
  • 24,977
  • 3
  • 43
  • 43
0

In my opinion, the best method is to place yourself on top of the merge-commits and undo the top commit and amend the changes to the previous merge-commit.

You will undo a git-commit with the following command:

    git reset HEAD~1

or

    git reset HEAD^

Then use:

    git add . && git commit --amend

Then check the results with:

    git log

You should see that the first merge-commit now has both changes included.

This method can be used for any such needs for all type of commits, or when

    git rebase -i HEAD~10

... cannot be used.