4 ms·
It's doing something weirder than that, but "it applies a diff between a specified commit and its parent on top of your current work" is more accurate/intuitive
by geofft 5y ago
It's doing something weirder than that, but "it applies a diff between a specified commit and its parent on top of your current work" is more accurate/intuitive than "it does a three-way merge with a common ancestor". No common ancestor is involved.
The first hint that it's doing something interesting is that the implementation of cherry-pick is in revert.c, because it's implemented as a variant of revert: https://github.com/git/git/blob/v2.35.1/builtin/revert.c#L232-L256 https://github.com/git/git/blob/v2.35.1/builtin/revert.c#L23...
Both the "revert" and "cherry-pick" operations initialize a sequencer object with a single operation in the sequence. (This is the same mechanism underlying "git rebase -i").
From there we can find the call to sequencer_pick_revisions, which leads us to the sequencer implementation, where there's a fast-path for ordinary cherry-picks leading us to a short function single_pick, which in turn calls do_pick_commit: https://github.com/git/git/blob/v2.35.1/sequencer.c#L2066 https://github.com/git/git/blob/v2.35.1/sequencer.c#L2066
This operation then does the following:
- Determine what we're applying a diff on top of: https://github.com/git/git/blob/v2.35.1/sequencer.c#L2083-L2107 https://github.com/git/git/blob/v2.35.1/sequencer.c#L2083-L2...
- Find a parent commit. If the commit has more than one parent, it requires you to specify a "-m" option indicating which parent to diff against. https://github.com/git/git/blob/v2.35.1/sequencer.c#L2109-L2136 https://github.com/git/git/blob/v2.35.1/sequencer.c#L2109-L2...
- Set up the commit message and some other stuff.
- Set the variable "base" to the parent it found and "next" to the commit being cherry-picked. https://github.com/git/git/blob/v2.35.1/sequencer.c#L2187-L2190 https://github.com/git/git/blob/v2.35.1/sequencer.c#L2187-L2...
- Assuming no merge strategy (or either of the standard strategies "ort" or "recursive") was specified on the command line, call do_recursive_merge. https://github.com/git/git/blob/v2.35.1/sequencer.c#L2237-L2242 https://github.com/git/git/blob/v2.35.1/sequencer.c#L2237-L2...
In turn, do_recursive_merge calculates a head_tree from the current HEAD (https://github.com/git/git/blob/v2.35.1/sequencer.c#L645 https://github.com/git/git/blob/v2.35.1/sequencer.c#L645), and then calls either merge_incore_nonrecursive (the current default, from the new "ort" merge strategy) or merge_trees (from the older "recursive" merge strategy) with three trees (i.e., with no further ancestry information): the base is the parent it found, and the two sides being merged are your current HEAD and the (tree of) the commit being cherry-picked.
That is to say, at no point does it care whether the two commits even have a common ancestor! It's just doing an operation on trees. It is doing a three-way merge, yes, but the graph of the merge it's doing is one that potentially doesn't actually exist in reality.
Or, in other words, it's trying to compute the tree that could be equally well described as the result of
- applying the diff of the commit you're cherry-picking to your current HEAD
- applying the diff between your current HEAD and the parent of the commit you're cherry-picking to the commit you're cherry-picking
So this is more powerful than purely applying a diff with no information about the base of the diff, but it is very much like applying a diff.
One way you can test this without drilling into source code is to make two independent commit histories (using either two git repos, or git checkout --orphan, or whatever) where a commit in one history has a diff that would apply to a file in the other history if applied with the "patch" command. Then try cherry-picking it into the second history. It should work.
(In the revert case, it more or less does the same thing, just with "base" and "next" flipped. That is, if you have commits A, B, and C, and you want to revert B, it does a three-way merge where the base is the tree after B, one side is the tree after A, and the other side is the tree after C!)
- kazinator 5y agoIt just sounds like, more or less: $ diff3 -m my-file pick-file-parent pick-file
- mastazi 5y agoWow this is very interesting, thank you for the explanation > "it applies a diff between a specified commit and its parent on top of your current work" this is in line with my intuitive understanding which was based purely on usage.
- martinvonz 5y agoYep. Same thing in Jujutsu (the recursive merge is at https://github.com/martinvonz/jj/blob/a6ef792ba66b5b19e752827a89a3366d57500c34/lib/src/rewrite.rs#L31 https://github.com/martinvonz/jj/blob/a6ef792ba66b5b19e75282...). Probably the biggest difference compared Git there is that it'll still work even if there are conflicts. FYI, this is also how `jj undo` works, except that it's a three-way merge at the repo level (https://github.com/martinvonz/jj/blob/d9b364442e2246a734d600b2a4e6475dcd48319b/src/commands.rs#L3894 https://github.com/martinvonz/jj/blob/d9b364442e2246a734d600...). So that applies changes to branches, checkouts (think: git HEAD), and sets of anonymous heads. This is how you can undo an operation even if it wasn't the most recent one (just like you can `git revert` a commit that wasn't the most recent one).