Search code examples
gitgit-cherry-pickgit-merge-conflict

How to resolve "added by us" git cherry-pick conflict?


I ran into a problem with git, during a conflict resolution after a cherry-pick. Problem involved modification made on file that was git mv in a previous commit.

Here is an example of the full command to reproduce:

mkdir git_repository && cd git_repository
git init
echo "foo" > myFooFile
git add myFooFile 
git commit -m "First commit add myFooFile"

git checkout -b branch-a
rm myFooFile
echo "bar" > myBarFile
git add -A
git commit -m "rm myFooFile add myBarFile"

git checkout master
git mv myFooFile myBarFile
git add -u
git commit -m "git mv myFooFile myBarFile"

Now lets get the modification made on branch-a

git cherry-pick $(git show-ref branch-a)
>error: could not apply 70c80f3... rm myFooFile add myBarFile
>hint: after resolving the conflicts, mark the corrected paths
>hint: with 'git add <paths>' or 'git rm <paths>'
>hint: and commit the result with 'git commit'

Here is the conflict:

git status
>On branch master
>You are currently cherry-picking commit 70c80f3.
>  (fix conflicts and run "git cherry-pick --continue")
>  (use "git cherry-pick --abort" to cancel the cherry-pick operation)
>
>Unmerged paths:
>  (use "git add <file>..." to mark resolution)
>
>   added by us:     myBarFile
>
>no changes added to commit (use "git add" and/or "git commit -a")

Unfortunately doing git add myBarFile then git cherry-pick --continue isn't the solution as it creates an empty commit.

Going with git rm myBarFile during resolution is not better as it creates a commit removing foo from myBarFile.

How to properly fix the cherry-pick conflict added by us and end-up in a situation where I got these 3 commits on my master branch ?

  • rm myFooFile add myBarFile
  • git mv myFooFile myBarFile
  • First commit add myFooFile

With myBarFile containing bar ?

Note: I known that I could use git checkout branch-a -- myBarFile during git conflict resolution but this not the solution I'm looking for because I don't think it's the git way of doing it.


Solution

  • Minor: don't do this:

    git cherry-pick $(git show-ref branch-a)
    

    Do this instead:

    git cherry-pick branch-a
    

    The cherry-pick command takes anything that identifies a revision, or even a revision range, as described in the gitrevisions documentation. The git show-ref command outputs both the hash ID and the name, so this tries to cherry-pick the commit twice. (Fortunately git cherry-pick is smart enough to eliminate the extra.)

    A top level view of resolving the conflict

    There is no single right way to resolve the conflict. You may use anything you like. There are several important things to remember here:

    • Cherry-picking is essentially a three-way merge a la git merge, except that instead of finding the actual merge base, Git just uses the parent of the commit you are cherry-picking as the merge base (and of course, the final commit is a regular non-merge commit).

    • Git doesn't track file name changes. Instead, when doing a merge operation—including a cherry-pick—Git in essence runs two git diff commands:

      git diff --find-renames <merge-base> HEAD     # figure out what we did
      git diff --find-renames <merge-base> <other>  # figure out what they did
      

      If the result is a merge conflict, Git leaves all three "interesting" files in the index—but one or two such files can be missing (as is the case here). The git status command shows these as "unmerged", but you can find the full details using git ls-files --stage. I'll show details in a moment.

    • Your job, at this point, is simply to arrange, in the index, the files you want in the final commit to be made as a result of the merge (or cherry-pick, in this case). These files need to be at stage zero, which is where normal, un-conflicted files live in the index. Any entries at stage 1 represent the version of the file in the merge base, any at stage 2 represent the version of the file in the HEAD commit, and any at stage 3 represent the version of the file in the other commit you're merging with (or cherry-picking).

    The conflict itself

    So, let's take a look at what git diff --find-renames found, knowing that the merge base is the parent of the commit being cherry-picked. We can identify the cherry-pick commit using the name branch-a. Its parent is therefore branch-a^ (again, see the gitrevisions documentation).

    $ git diff --find-renames branch-a^ HEAD
    diff --git a/myFooFile b/myBarFile
    similarity index 100%
    rename from myFooFile
    rename to myBarFile
    

    This is "what we changed": a rename operation.

    $ git diff --find-renames branch-a^ branch-a
    diff --git a/myBarFile b/myBarFile
    new file mode 100644
    index 0000000..5716ca5
    --- /dev/null
    +++ b/myBarFile
    @@ -0,0 +1 @@
    +bar
    diff --git a/myFooFile b/myFooFile
    deleted file mode 100644
    index 257cc56..0000000
    --- a/myFooFile
    +++ /dev/null
    @@ -1 +0,0 @@
    -foo
    

    This is "what they changed": delete one file, add another.

    Git is now charged with the job of renaming myFooFile to myBarFile (with contents foo) and, simultaneously, deleting myFooFile while creating myBarFile with contents bar.

    It cannot possibly do both, so we get a conflict. Meanwhile it can do the rename that we did, so it does do that in the work-tree (only), leaving myBarFile containing foo.

    You want to know:

    How to properly fix the cherry-pick conflict added by us and end-up ... With myBarFile containing bar ?

    All Git needs is for you to write, to the work-tree, a version of myBarFile containing the contents that you want, and then adjust the index so so that the entry for it is at stage zero. Currently, what's in the index is:

    $ git ls-files --stage
    100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 2      myBarFile
    

    and myBarFile has contents you don't want.

    Note: I known that I could use git checkout branch-a -- myBarFile during git conflict resolution ...

    Yes: that extracts the version of myBarFile from the commit identified by branch-a, writes it into the index at stage zero, removes the entry at stage 2, and leaves everything ready for committing. (As a bonus, it also writes the file into the work-tree so that you can see it, though Git doesn't actually care about the work-tree version at this point.) So that's a good and fast method of achieving your desired result. We can look at another method, though, which may be useful in some cases:

    [but] I don't think it's the git way of doing it.

    Git doesn't have a (single) way. Git is a tool. Use it however you prefer. If you prefer to adjust the work-tree contents without first touching the index, you can use git show to extract the branch-a version of the file:

    $ git show branch-a:myBarFile > tmp
    $ cat tmp
    bar
    $ mv tmp myBarFile
    $ git add myBarFile
    

    If we check the actual staging area, we find:

    $ git ls-files --stage
    100644 5716ca5987cbf97d6bb54920bea6adde242d87e6 0       myBarFile
    

    which is what we want, so we can now git cherry-pick --continue or git commit to finish off this cherry-pick:

    $ git cherry-pick --continue
    

    At this point, your preferred editor should open on a message file containing the subject rm myFooFile add myBarFile and additional data (since that's the commit you're cherry-picking). Writing this out results in:

    [master 1837c17] rm myFooFile add myBarFile                   
     Date: Fri Jun 15 22:21:48 2018 -0700
     1 file changed, 1 insertion(+), 1 deletion(-)
    

    and git log --oneline shows what you want:

    1837c17 (HEAD -> master) rm myFooFile add myBarFile
    a30b874 git mv myFooFile myBarFile
    bde7df5 First commit add myFooFile
    

    (note: I have log.decorate = auto in my configuration).