1

I’ve some issues finding the right git command for the job.

A place I work at is currently switching source control (TFVC -> Git). However, the old version in TFVC still needs bug fixing from time to time.

I’ve handled that by creating a build with some PowerShell scripts that ensure that my “tfvc-merge” branch contains those changes.

So here is what I do afterwards.

  • Checkout from integration branch
  • I cherry-pick the commit from “tfvc-merge” branch to my “mergerhandler” branch
  • (my Issue) Now I wanna make an empty commit that have 2 parents. First parent is the last commit i have made on "MergeHandler" branch, next parent is the commit of the “tfvc-merge” branch.

I tried to draw it up. So basically, the empty commit at the pull request will have the parent of 1 and 2 in my drawing.

Cherry-Pick Strategy

I've been searching around, for a few hours without finding the right way to do this. So it would be nice with a hint that could point me in the right direction. Thanks in advance.

HitzSPB
  • 158
  • 10

1 Answers1

5

TL;DR

I think you just want git merge -s ours.

Description

Cherry picking some existing commit makes a new commit that does not link back to the original commit (the new commit has a single parent; this parent is the commit that was HEAD when you ran git cherry-pick). The drawing you made shows commit #2 with two parents, as if your cherry-pick was a merge. Hence I'm not sure I understand the situation itself correctly.

The phrase empty commit is one that Git itself uses, but I find very misleading: commits do not normally have an empty tree object (see Is git's semi-secret empty tree object reliable, and why is there not a symbolic name for it?); what's empty about a commit made with git commit --allow-empty is the difference between that commit and its parent.

There are several ways to make such a commit, obviously including git commit --allow-empty, but that first method won't make a merge commit. The only user-facing command that makes merge commits is git merge. This leaves you with the problem of "merging without merging", perhaps, if that phrase even means anything. Sometimes it does: git merge -s ours.

(Note that since a merge commit has two parents, using the phrase "empty commit" to talk about this merge makes even less sense than it did when talking about a single-parent commit, because there are two diffs to make against the merge commit's two parents. It's likely that if one is empty, the other is not.)

When you run git merge, you may supply a merge strategy name. Git has four built in strategies, named recursive, resolve, octopus, and ours. The normal one that people encounter during normal merges is recursive. The resolve strategy is a simplified variant of -s recursive that is almost the same thing (the two differ only in the relatively rare case of a merge with multiple merge bases). The octopus strategy is the one Git uses when you make a true merge with more than two parents, which we won't discuss at all here. That leaves -s ours. Before we get to it, let's take a brief moment to review the normal (recursive/resolve) method:

  1. You check out some particular branch: git checkout mainline.
  2. You run git merge sidebranch.

At this point, Git computes a merge base between the tip commit of mainline—the commit to which the branch name points, which I will call L for left/local/--ours—and the tip commit of sidebranch, which I will call R for right/remote/--theirs:

...--o--*--o--L   <-- mainline
         \
          o----R   <-- sidebranch

The merge base is a point where the two branches join up: here, it's commit *. Git then, in essence, computes two git diffs:

git diff --find-renames <hash-of-base> <hash-of-L>   # what we did
git diff --find-renames <hash-of-base> <hash-of-R>   # what they did

Git then tries to combine the two sets of changes, making those combined changes apply to the contents of the merge base *, and if all goes well, Git makes a new merge commit M, with L as its first parent and R as its second parent:

...--o--*--o--L--M   <-- mainline
         \      /
          o----R   <-- sidebranch

Comparing (diffing) L vs M will show the changes that this merge obtained from *-vs-R that were not already in *-vs-L. That is, we picked up their work. Comparing R vs M will show the changes that this merge obtained from *-vs-L that were not already in *-vs-R. That is, we also kept our work.

The -s ours strategy

What -s ours does is quite simple. Instead of bothering with all the diff-ing, Git just keeps the tree from L. Git makes the new merge commit with L as its first parent and R as its second parent, so the graph looks exactly the same:

...--o--*--o--L--M   <-- mainline
         \      /
          o----R   <-- sidebranch

But now if we run git diff mainline^1 mainline to compare L and M, the output is empty. If we run git diff sidebranch mainline to compare R and M, they can be completely different. Git has completely ignored the contents of commit R: it has made M using the contents of L.

If you call an empty diff an "empty commit", then L vs M is just such an empty commit. But there's still R vs M as part of this merge commit, so as we noted above, the phrase "empty commit" does not really make sense here.

A long aside

The whole thing would make much more sense if there were an existing merge commit from the old tvfc branch to the new branches:

  ...--o--M--o--<arbitrarily complex work>--I   <-- integration
         /
...--o--*   <-- tvfc

which over time becomes:

  ...--o--M--o--<arbitrarily complex work>--I   <-- integration
         /
...--o--*--o--<arbitrarily complex work>-----X   <-- tvfc

In this case, any and all new commits added to tvfc itself can simply be merged to integration with git checkout integration; git merge tvfc as the merge base of integration and tvfc is now commit *. Git will diff * vs X to see what they changed, and combine this with * vs I to see what you changed. After combining these changes, Git will make a new merge commit N and the new merge base in the future will be X itself:

  ...--o--M--o--<arbitrarily complex work>--I--N   <-- integration
         /                                    /
...--o--*--o--<arbitrarily complex work>-----X   <-- tvfc

but that seems not to be what you are doing. I wonder a bit here if you are simply setting up this situation now. If so, the git merge -s ours command above is probably what you want.

torek
  • 448,244
  • 59
  • 642
  • 775