A cherry-pick is a merge, of sorts: it's a merge in which the "merge base" is forced to be the parent of the commit being cherry-picked ("copied").
That is, given a branch structure like this:
I--J <-- br1
/
...--G--H
\
K--L <-- br2
when we run git switch br1 && git merge br2
we're asking Git to combine work done since a common starting point. Here, the "work done" is "whatever changed from commit H
to commit J
" ("our" work on br1
), vs "whatever changed from commit H
to commit L
" ("their" work on br2
). So Git diffs each file in commit H
against each file in commit J
: whatever changed, that's "our" work. Git then diffs each file in commit H
against each file in commit L
: whatever changed, that's "their" work. Git then combines the two sets of changes. Where the changes overlap, but don't exactly match, that's a conflict (note: this isn't a full list of all possible conflicts, just a high-speed review to cover the major cases).
Cherry picking is similar but different. We're given a structure like this:
o--o--P--C--o--o <-- br2
/
...--*
\
A--B <-- br1 (HEAD)
where we're sitting at commit B
. We ask to "copy" commit C
. This means find out what changed in C
, which means Git needs to run the same kind of git diff
of P
, C
's parent, and C
, that git merge
would do for a regular merge. That gets a set of changes from commit P
that C
makes.
To apply those changes to commit B
, though, Git needs to know where those changes fit in. What if we moved a block of code down by inserting a bunch of new code in A
? What if we moved a block of code up by deleting some code in one of the o
's before P
, or in P
itself? To find out which parts of commit C
match up, Git does a git diff
of the snapshot in commit P
against the snapshot in commit B
. Now Git knows about the blocks of code inserted or deleted.
To apply the change from P
-to-C
, then, Git can now use the information it found from P
-vs-B
. But—hang on a minute... that's exactly how git merge
works in the first place. All Git has to do is combine the changes from P
-to-C
, "their" work, with the changes from P
-to-B
, "our" work. So Git literally uses the same git merge
code.
For text files this works great: any changes "we" "made", including the "backing out" of stuff that happens because P
is later than *
, get backed out if appropriate. Any changes we actually made, like changes we made in A
since *
, get added in. Git does not actually look at each individual change, one commit at a time: it just uses the wholesale P
-vs-whatever diff to get everything at once.
For the binary file, though, all Git knows is "hey, this is different". The binary file in P
is different from the binary file in C
, and that one is different from the one in B
. That's your conflict.
If you cherry-pick multiple commits, though, this might change. There are no guarantees here, but suppose that the binary file changed between *
and the first o
, but not between *
and A
or between A
and B
. Then if we cherry-pick the first o
we pick up the change of the binary file: there's no conflict because we got just one change, from their side, *
vs first o
. Then there's another change of the same binary file between the two o
s or between the second o
and commit P
. There's still no change on "our" side because we've now picked up the *
-vs-first-o
change, so we pick up their change. Then when we get to P
-vs-C
, there's a change of the binary file again, but this time we're on a commit that has the P
version of the binary file, so there's no conflict switching to the C
version.
To reason about these things, it's necessary to look at every commit in the chain when cherry-picking multiple commits, and to not look at every commit in the chain when cherry-picking just one commit. This is why rebase is much more complicated than merge: rebase is repeated merging, and that's a different proposition than one-time merging.