git rebase interactive: squash merge commits together
Asked Answered
M

6

63

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

Mitten answered 12/11, 2009 at 21:59 Comment(2)
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.Mitten
This won't work, it won't save the new version of the branch you merged from in the commitMitten
S
62

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.

Sombrous answered 9/11, 2010 at 20:51 Comment(7)
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).Richie
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.Uranus
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.Thorner
if you start the failed merge you can get what the merge message would have been and then use thatThorner
Pro tip: don't misread f (the commit id) as -f or you end up with fatal: Corrupt MERGE_HEAD file (-f)Hypothec
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?Am‚lie
Wow!! So pretty, especially with the @LuxDie's comment. Thank you, you have saved me lots of time nowBarbarity
I
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
Issuable answered 12/11, 2009 at 22:34 Comment(2)
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.Mitten
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).Imagery
B
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

Burgoo answered 4/7, 2013 at 20:23 Comment(2)
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.Murdoch
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" :)Carycaryatid
I
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>]
Innis answered 12/12, 2021 at 21:44 Comment(2)
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@^!?Oxygen
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 @Issuable suggested. git filter-branch is new to me and looks like another very good solution to this problem.Innis
N
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.

Nazarite answered 9/3, 2012 at 5:39 Comment(1)
you can substitute commit_msg with $($EDITOR commit_msg), or write <<< 'here is my commit'Giraud
E
-1

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.

Ellett answered 21/12, 2021 at 14:6 Comment(1)
Downvoting because this makes the diff part of the merge commit, but does not tell git (or readers of the git history) about the original commit which the changes came from. The result is a cherry-pick pretending to be a merge. If you ever again merge from the same parent, you will get duplicated commits unless git's heuristics for detecting cherry-picking bail you out.Macymad

© 2022 - 2024 — McMap. All rights reserved.