2

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?

A B
  • 23
  • 2
  • 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. – Romain Valeri Nov 11 '19 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 Nov 11 '19 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 Nov 11 '19 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 Nov 11 '19 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 – Mark Adelsberger Nov 11 '19 at 14:58

3 Answers3

4

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.

eftshift0
  • 26,375
  • 3
  • 36
  • 60
Mark Adelsberger
  • 42,148
  • 4
  • 35
  • 52
  • 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. – Mark Adelsberger Nov 11 '19 at 14:50
  • I had a hard time finding what `^{tree}` means, if one wonders, it is described in [Tree Objects](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects#_tree_objects) and it is just the `` to which that `` points. Since [the `git-commit-tree` command](https://git-scm.com/docs/git-commit-tree) expects a `` and not a ``, we cannot just use the `` ([a `` is also ``](https://stackoverflow.com/a/18605496/525036)). I don’t know why it doesn’t accept a `` though. – Didier L Aug 03 '22 at 09: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
David Sugar
  • 1,042
  • 6
  • 8
  • 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') – elfprince13 Apr 29 '22 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.

eftshift0
  • 26,375
  • 3
  • 36
  • 60
  • 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 – Mark Adelsberger Nov 11 '19 at 15:00
  • Nice trick with `j^{tree}`. Another one for my bag of tricks. – eftshift0 Nov 11 '19 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 Nov 11 '19 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 Nov 11 '19 at 15:24