2

I have two branches whose merging is quite tedious for some reasons. Sometimes I want to add a merge commit to indicate I have taken some information from one branch to another, e.g. I have checked out one file.

Since using git merge will produce a lot of merge conflicts and the only thing I am interested in is to create a commit with two parents, is it safe to edit MERGE_HEAD and commit? I mean is there a risk that I will get weird errors if I use git rebase or git cherry-pick in the future?

Aratz
  • 430
  • 5
  • 16

2 Answers2

5

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.)

torek
  • 448,244
  • 59
  • 642
  • 775
1

As far as I understand, you want to "pick" just some information from another branch, yes? For example: I changed files foo, bar and baz in feature and want to "merge" foo into the master without taking bar and baz. I think there's much better possibilities then messing with the MERGE_HEAD.

Use git cherry-pick

git cherry-pick --no-commit -x feature
git reset -p # Interactive reset similar to git add -p
Unstage foo? N
Unstage bar? Y
Unstage baz? Y
git commit

You end up with a commit for foo with a message saying cherry-picked from... However, git reset -p allows to unstage any chunk of any file. You could also take just a line from another branch.

Use git checkout

git checkout feature -- path/to/foo
git commit

This checks out foo in the version available in branch feature. In this case, you will have to write an own commit message. But you could create a git alias for that by adding the following to your .git/config file.

[alias]
    pick-file = !git checkout $1 -- $2 && git commit -i --edit -m \"Picked $2 from $1\"

Usage: git pick file branch file

Use git show

For example:

git show feature:path/to/file > file

You then can use git add to stage the whole file or git add -p for deciding which lines you need. Again, you have to commit after that. You may also create an alias for this task.

SVSchmidt
  • 6,269
  • 2
  • 26
  • 37
  • This is useful, but what I want is to have a link to the two branches for my own understanding of my project. If I create a commit using one of your method, it becomes unclear about what was the state of the second branch when these changes where imported. Hence the need to create an artificial merge commit. I'm just afraid of the consequences of such a merge. – Aratz Aug 01 '17 at 13:47
  • 1
    @Aratz I see what you mean. You may change that `pick-file` thing to `pick-file = !git checkout $1 -- $2 && git commit --edit -im \"Picked $2 from $1:$(git log -n1 $1 --format=%H)\" -- $2`, which will include the hash of the current commit in feature to the message (or `--format="%H (%s)` which is hash + subject). However, maybe someone has an answer to your original question regarding the merge head. – SVSchmidt Aug 01 '17 at 13:51