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 ?
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.
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.)
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).
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).