I would not do this using MERGE_HEAD
since there are no promises about how it will continue to work in the future.
You can, however, construct any arbitrary history you desire using git commit-tree
, which is a plumbing command that takes:
- a tree hash ID (the source tree to record for the new commit),
- zero or more parent hash IDs (the parents for the new commit), and
- a commit message (for the new commit).
The result is a commit object with the given parents. For instance, given a tree that looks in part like:
o--o--o <-- br1
/
o--o--o--o <-- br2
\
o--o--o <-- br3
we can pick any two, three, four, or whatever number of commit hashes we like and make a new merge commit. Let's pick the top row middle commit and the bottom row rightmost commit. We'll use the tree of the tip of br2
as the actual tree:
$ hash=$(git commit-tree -m 'mystery merge' -p br1~1 -p br3 br2)
We now have the hash ID of this new merge commit in $hash
. No branch name points to it, so let's create a new one:
$ git branch br4 $hash
giving:
o--o--o <-- br1
/ \
o--o--o--o o <-- br4
\ /
o--o--o <-- br3
(Branch br2
is still in there pointing to the one node in the middle, I just ran out of room to draw it.)
Since we used br2
as the source of the tree (snapshot) to save, if we now run:
$ git diff br2 br4
we will get nothing at all: the two commits have the same tree (but different parent hash IDs, and of course the commit message for the tip of br4
is "mystery merge").
The way this affects future operations is that when Git goes to compute the merge base of the new commit, or any commit downstream of the new commit, Git will follow the parent links we just made as part of the new commit. For most cherry-pick operations this has little effect, since cherry-picking a commit diffs that commit against its immediate parent.
Note that when you make a normal, ordinary, every-day merge, what Git is doing is:
- taking two specific commits, one being your current branch tip commit L (local or left or
--ours
) and the other being a commit you name, usually another branch tip, R (remote or right or --theirs
);
- finding their merge base B through the commit graph like the ones we drew above;
- running two
git diffs
: git diff B L
, git diff B R
- combining the two diff results and applying that to B to get a new tree (Git stores this tree in a flattened form, in the index, and then uses the C code equivalent of
git write-tree
to write the tree object);
- writing the merge commit, as if by
git commit-tree
, using the tree hash from the previous step and the two parent hashes L and R in that order; and finally
- moving the current branch name so that it points to the new merge commit.
If you make your own tree, by whatever means, Git believes that this is the full and correct result of merging those two other commits. A future git merge
sees this merge and uses that to inform it as to what the next merge's merge-base commit B should be. If we go back to the above drawings, and instead of making a new branch name br4
, have the branch name br3
move to accommodate the new commit, we get:
o--o--o <-- br1
/ \
o--o--o--o \ <-- br2
\ \
o--o--o---o <-- br3
which means that as far as Git is concerned, the correct result of merging the upper left middle commit (br1~1
) into the previous tip of br3
(now br3^2
—this is a bit weird; see note below) is the source tree that's at the tip of br2
. This means that if we now run:
git merge br1
while on br3
, Git will find the merge base by walking backwards from the tip of br3
to find the first commit that is on both branches. That's our br1~1
. There is one commit since then, namely the tip of br1
, so we diff br1~1
against br3
(producing whatever is different between these two commits) as "what's in br3 since the merge base", then diff br1~1
against br1
as "what's in br1 since the merge base". Git then tries to combine these as usual, then make a new merge commit on br3
.
In short, you need to know what you are doing: you are telling Git to use whatever tree you like as the correct result of the merge you build, which affects future merges.
Note: when we made the merge, we chose br1~1
as the first parent and br3
(its previous value) as the second. If we're going to stick the merge into br3
like this, that's an unusual order: it's kind of backwards. It declares, in effect, that br1~1
is the "main line" that someone should follow later. There is nothing fundamentally wrong with this "backwards merge"—the tree is whatever we choose, regardless of which "direction" the merge takes—but if you plan to view this later using --first-parent
to follow the main line of work, you'll see br1~1
as this --first-parent
link.
(The git pull
command also makes these kinds of "reversed" merges, which some call a foxtrot merge. Of course its merges are using a normal merge base and merge process, rather than some arbitrarily-chosen tree. But this means that the merges that git pull
makes are "backwards" when trying to follow --first-parent
links, which is yet another reason one might wish to avoid git pull
.)