Search code examples
git

How do I create a "True merge" from the current HEAD into a previous commit reachable from HEAD?


Let's imagine that the last 4 commits represents a change that I want to create a no-ff merge into the original state to preserve the individual commits explaining why they themselves made a good change, but have the merge commit message describe what the feature is about, and what benefit it provides to users.

The first attempt

I could just reset my branch, and then merge the previous HEAD using a no-ff merge.

git reset HEAD~4 --hard    # Reset HEAD to the commit before the feature
git merge --no-ff HEAD@{1} # HEAD@{1} is now the prev. commit, thus my features.

This creates exactly the commit I want, but that has an unfortunate side effect, as the git reset --hard changes my working tree, despite the end state should be identical to where I started.

The changing of work tree can have some unfortunate consequences, e.g. tsserver, the TypeScript/JavaScript language server can sometimes go into 100% CPU if files are moved/removed. It could also trigger automatic test runs, which could have side effects I didn't want.

Attempt to do this without changing the work tree

So I wanted to see if I could perform that operation without changes to the working folder.

After reading the documentation for merge, I see that what I want is a True merge, which merges from MERGE_HEAD into HEAD. Setting MERGE_HEAD alone is not enough, so I try to see what goes on behind the scenes while in the middle of a git merge --no-ff, and I try to replicate that.

# reset to the commit I want to merge _to_, but keep working dir
git reset HEAD~4
git update-ref MERGE_HEAD HEAD@{1} # Gathered from merge documentation
git update-ref ORIG_HEAD HEAD      # Not in doc, but `merge --no-ff` updates this ref
echo "no-ff" > .git/MERGE_MODE     # Not in doc, but `merge --no-ff` creates this file
git add .                          # Gathered from merge documentation
git merge --continue

While it creates the commit, the commit only has one parent, not 2.

I also notice that no-ff adds an AUTO_MERGE file pointing to a commit root tree object.

I tried to add (just using the hardcoded tree object id in this particular test - as I did not know how to get the ID of the root tree from a commit).

git update-ref AUTO_MERGE f9e3c4cb... # just for this test

But that didn't have any effect. The new commit still has only one parent, not two.

How can I manually create a commit with two parents?

And what goes on behind the scenes in the git commit --no-ff <ref> that I was unable to detect when inspecting the intermediate state?

note

The accepted answer answers exactly the first of these two questions, which was really the original problem. And also the elegant solution to my problem, rather than my hacky attempt.

Still a bit puzzled though, that there appears to be some hidden magic in merge --no-ff I was unable to identify.


Solution

  • if git diff HEAD~4..HEAD is empty (and it should given your assertion that the first command does not change the working tree), you could get the same thing without moving around by creating a commit from thin air:

    git merge "$( git commit-tree -p HEAD~4 -p HEAD -m "The comment for the merge commit" HEAD^{tree} )"
    

    Consider also adding --ff-only as an option to git merge, per @knittl's feedback (check comments).