A B
A B

Reputation: 33

How to "squash" subsequent merge commits

In my project, I have a history like this:

    b --- c --- g
  /        \     \ 
a           f --- h --- j
  \        /           /
    d --- e --- i ----

I want to remove intermediate merge commits and only have one merge commit with the same content like:

    b --- c --- g
  /              \ 
a                 j'
  \              /
    d --- e --- i 

One approach would be redoing everything from scratch, but f had ca. 600 conflicting files, g is in fact 200 new commits again, and I don't want to spend a week more resolving conflicts. How do I achieve this?

Upvotes: 3

Views: 472

Answers (3)

Mark Adelsberger
Mark Adelsberger

Reputation: 45819

I might be able to explain the process a tad more clearly if I knew the branching structure, but generally you would do this:

You really only need to create a new merge between g and i, because as you've drawn it, none of the merges you want to remove are in the history of either g or i, and all relevant changes are in the history of either g or i (i.e. the section being removed is only merge commits[1]).

You don't want to redo all the conflict resolution, but that's ok because you already know the desired result of the merge. So what you need is to make a copy of j whose parents are g and i. There are at least two ways to do that.

The most straight-forward way uses plumbing commands, so it may look less familiar.

git checkout $( git commit-tree j^{tree} -p g -p i -m "merge message here" )
git branch -f my_branch

where my_branch is the branch you currently have at j, and j g and i are expressions that resolve to the respective commits from your diagram. (That could be commit iD (SHA) values, refs currently pointed at those commits, etc.)

Remember that the commit's parents are ordered - the second -p option identifies the commit that will appear to have been 'merged into' the first -p option's commit.

A different approach would be to check out g (assuming g should be the first commit; swap g and i in this procedure otherwise), and

git merge --no-commit i
git rm -rf :/:
git checkout j -- :/:
git commit -m "merge message here"

In this approach, you're initiating a new merge, clearing the work tree and index, loading the commit tree and index from the previous merge result, and then completing the merge with that content.

I find this a little more involved, and it includes an rm command that might raise red flags if you're not confident about what's going on, but it has the advantage of sticking to commands meant for end users.


[1] I am sort of assuming that none of the merges introduce new changes (other than conflict resolution). If they do, they are what is sometimes called "evil merges"; the procedure above would still work, actually, but it would result in your new merge being an "evil merge" as well.

Upvotes: 5

David Sugar
David Sugar

Reputation: 1256

Here's one way to do this:

 git reset --hard g //reset branch to g commit (use 'i' and merge 'g' if the  branch originally came from i) 
 git merge i --no-commit  
 git reset j :/: //resolve all conflicts by replacing the staging area with all file versions from original commit j
 git commit
 optional:  git reset --hard //if you want the work tree to reflect the new merge commit

Upvotes: 1

eftshift0
eftshift0

Reputation: 30297

The only way I can think of to be able to do it in a single shot without going through the process of merging anything is by using git commit-tree. First, get the ID of the tree of the J revision with git cat-file:

git cat-file -p j

Copy the id of the tree object.

Then run git commit tree, you can provide the 2 parent revisions with -p, the comment with -m, and the tree id that you got from the previous git command will be used as the last argument to the call.

git commit-tree -m 'Here's the comment" -p g -p i tree-object-of-j

When you run that command, git will print out the id of the new revision object that was created. You can point a branch to it, or move j pointer if you have already checked that the revision is just want you wanted:

git branch -f j new-revision-of #move j branch to the new revision id

You will be the author of the revision.... if you want to change the dates or the authors, you can do it by setting environment variables. Check git help commit-tree.

Upvotes: 0

Related Questions