Search code examples
gitgit-cherry-pick

Unexpected behaviour when git cherry-pick and merge between branches


I'm surprised that changes done after cherry-picking in git become obsolete when merging. Here is a full example.

The following is business as usual.

  1. Create a repo
  2. Add a file with test "rotums kanoner och krut"
  3. Check out a new branch, and add the text line "mutors kanoner och krut"
  4. Check out master and cherry-pick the commit with "mutors kanoner och krut"
Mac:git user1$ mkdir myrepo; cd myrepo; git init
Initialized empty Git repository in /Users/user1/tmp/git/myrepo/.git/

Mac:myrepo user1$ echo "rotums kanoner och krut" > rotum.txt

Mac:myrepo user1$ git add rotum.txt 

Mac:myrepo user1$ git commit -m "Added file"
[master (root-commit) 1044abb] Added file
 1 file changed, 1 insertion(+)
 create mode 100644 rotum.txt

Mac:myrepo user1$ git checkout -b mybranch
Switched to a new branch 'mybranch'

Mac:myrepo user1$ echo "mutors kanoner och krut" >> rotum.txt  

Mac:myrepo user1$ git commit -am "Added mutor"
[mybranch 19afeba] Added mutor
 1 file changed, 1 insertion(+)

Mac:myrepo user1$ git checkout master
Switched to branch 'master'

Mac:myrepo user1$ git cherry-pick 19af
[master cce2ca5] Added mutor
 Date: Wed May 19 16:12:04 2021 +0200
 1 file changed, 1 insertion(+)

Mac:myrepo user1$ cat rotum.txt  
rotums kanoner och krut
mutors kanoner och krut

Now is when the unexpected behaviour occurs.

  1. I remove the line that was added and cherry-picked (I do this by overriding the file, unconventional method, but useful in this case).
  2. Then I merge mybranch to master. I would expect the changes done in f63dc50, removal of a line, to remain, but it mysteriously vanishes. The line "mutors kanoner och krut" is back.
Mac:myrepo user1$ echo "rotums kanoner och krut" > rotum.txt  

Mac:myrepo user1$ cat rotum.txt  
rotums kanoner och krut

Mac:myrepo user1$ git commit -am "Removed mutor"
[master f63dc50] Removed mutor
 1 file changed, 1 deletion(-)

Mac:myrepo user1$ git merge mybranch
Merge made by the 'recursive' strategy.
 rotum.txt | 1 +
 1 file changed, 1 insertion(+)

Mac:myrepo user1$ cat rotum.txt
rotums kanoner och krut
mutors kanoner och krut

Is this expected behaviour or a bug?


Solution

  • With a diagram :

       Initial commit: create file
       |    Add a line in file (cherry-pick b)
       |    |    Remove line, return file to its state in a.
       v    v    v
       a----c----d  <- master
        \
         b <- mybranch
         ^
         Add a line in file
    

    When you merge mybranch into master :

    • git looks for the closes common ancestor (aka the merge base, commit a. in the diagram),
    • it looks at master (commit d) and sees that, when compared to a., the file is left unchanged
    • it looks at mybranch (commit b) and sees that, when compared to a., a line should be added

    so the merge succeeds without conflicts, and "brings in" the changes from mybranch.


    You reach this situation because :

    1. git merge does not inspect the intermediate commits in the history of master and mybranch ,
    2. git cherry-pick creates new, unrelated commits, and nothing is kept in the commit graph to indicate "these changes were already included",
    3. it so happens that the changes can be combined without conflicts (your example is really simple, but "no conlicts" may very well happen in actual situations).

    To give further perspective :

    • if you use git rebase :
    git checkout mybranch
    git rebase master
    

    unlike git merge, git rebase does compare the list of commits, and if a rebased commit introduces the exact same changes as another commit in the target branch, that commit is dropped.

    In your example : b wouldn't be reapplied, because it introduces the exact same changes as c which is already on master.

    • if you had merged commit b instead of cherry-picking :
           merge 'mybranch'
           v
       a---c----d  <- master
        \ /
         b <- mybranch
    

    then git merge mybranch would have said already merged, and wouldn't have re-applied commit b