3

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?

5
  • I'm somewhat suspecting an XY problem. Might be useful to rule it out, so bear with me. If the end state of both situations are the same, I guess we can say it's not a functional problem you have. Why do you want/need to have a history which does not reflect what happened? It's generally more useful to have reliable information than a pretty but false story. Your specific context could explain all this, I'm just asking to better understand your question. Commented Nov 11, 2019 at 14:11
  • @RomainValeri Your suspicion is right, the problem is mostly aesthetic. One functional thing with which I can justify my goal is that I (might) have done mistakes in f or h that I will find out later and I will not be able to amend those but I could amend j' before I present my big merge to the team.
    – A B
    Commented Nov 11, 2019 at 14:21
  • Into what do you want to squash the commits of f and h? Like do you want f commits to be part of j, or you want them to be dupicated in c and e, or not used? I want to make a suggestion to use merge strategy options because it sounds like you have the files you want and just want to avoid conflicts.
    – leorleor
    Commented Nov 11, 2019 at 14:43
  • @leorleor I want to squash the three merge commits: f, h and j together into a new commit j'. The conflicts have been resolved when they were created and now my repo is in a state I want j' to be. I don't want to and can't modify histories of g or i. All I wish is to get rid of this complicated history on my branch.
    – A B
    Commented Nov 11, 2019 at 14:52
  • @AB Your concern about mistakes in f or h does not make this a functional issue, because you could either (1) add the changes to a new commit (which is what you should do), or (2) amend j just like you'd be able to amend j'. "To hide the fact that errors ever existed" is not really a compelling reason to edit history Commented Nov 11, 2019 at 14:58

3 Answers 3

5

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.

2
  • While I've taken the approach of just answering the question, I'll also point out that I'm generally not in favor of altering history unnecessarily - and 99% of the time something like this would be unnecessary. Commented Nov 11, 2019 at 14:50
  • I had a hard time finding what ^{tree} means, if one wonders, it is described in Tree Objects and it is just the <tree> to which that <commit-ish> points. Since the git-commit-tree command expects a <tree> and not a <tree-ish>, we cannot just use the <commit-ish> (a <commit-ish> is also <tree-ish>). I don’t know why it doesn’t accept a <tree-ish> though.
    – Didier L
    Commented Aug 3, 2022 at 9:03
1

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
1
  • I didn't read carefully enough to figure out if this solved the crazy scenario in the original question, but it worked for my use-case (basically just "squashing" f and h into h') Commented Apr 29, 2022 at 19:04
0

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.

4
  • This is basically the plumbing approach I noted below; but be aware that the steps where you run commands to learn SHA values and then pass them to other commands are not necessary and can introduce error Commented Nov 11, 2019 at 15:00
  • Nice trick with j^{tree}. Another one for my bag of tricks.
    – eftshift0
    Commented Nov 11, 2019 at 15:05
  • Hmm.... but j^{tree} doesn't make you think that it is the tree of Js parent? Shouldn't it be j{tree}? Not sure, would have to learn about this apparently double use of ^ (^ vs ^{})
    – eftshift0
    Commented Nov 11, 2019 at 15:07
  • @eftshift0 thanks for your answer. I wish I could accept both your and Mark's answer, because although he was first, your explanation of the commands involved was clearer to me.
    – A B
    Commented Nov 11, 2019 at 15:24

Not the answer you're looking for? Browse other questions tagged or ask your own question.