1

I have a branch I've been working on. git status shows that it is up to date.

I checked out master, which is "behind by 43 commits and can be fast-forwarded".

git merge --ff-only

Now master is up to date and I checked out my branch again. git status shows it is up to date still.

I now want to rebase to master:

git pull --rebase

But I end up with a stream of merge conflicts on a file that only I've changed. I tried fixing the merge conflict, but each git rebase --continue comes up with another conflict seeming to reply every change I made to it.

In fact the problematic file was renamed and the merge conflicts I get are on an old filename. I performed many commits over the past days making updates to the file and renaming it a few times. All were done in my branch. Each time I made updates I committed and pushed it to my branch.

I don't understand the reason for the merge conflicts. The file looks to be up-to-date in my branch. What could cause this to occur?

David Parks
  • 30,789
  • 47
  • 185
  • 328

1 Answers1

5

I don't understand the reason for the merge conflicts.

The problem appears to be that Git has mis-paired the renamed files.

In fact the problematic file was renamed and the merge conflicts I get are on an old filename. I performed many commits over the past days making updates to the file and renaming it a few times. All were done in my branch. Each time I made updates I committed and pushed it to my branch.

This is about git merge, but it applies to git rebase as well

When Git is asked to merge two branch tips—or indeed any pair of commits (HEAD, the current branch, and some other branch tip commit or any other commit at all), Git will first use git merge-base --all to identify the merge base(s) between these two tip commits:

...--o--B--o--o--...--H   <-- branchA (HEAD)
         \
          o--o--...--T   <-- branchB

It does not matter how many times the file was renamed, nor on which branch(es). What matters is what Git sees when it runs:

git diff --find-renames B H
git diff --find-renames B T

Any file that Git identifies as "the same" in commits B and H, or "the same" in commit B and T, is decreed to be "the same file", regardless of its actual path name in commits B, H, and T.

Whatever changes Git sees between B and H are "our" changes (--ours). Whatever changes Git sees between B and T are "their" changes (--theirs). Git now tries to combine these changes. Where they overlap, Git complains of a merge conflict.

If Git is mis-identifying some path in B with the same or a different path in H or T, the diff between the version of the file that (Git thinks) is in B vs the version of that same file (with maybe different path name) is in H or T will not be a "good diff", and will clash badly with the other diff. This will produce the merge conflicts you are seeing.

The possible solutions are:

  • Rename the file again in a new commit added to H and/or T so that Git doesn't mis-identify the files.
  • Use the -X find-renames=<value> extended option to git merge to change the similarity index, to affect which files Git thinks are "the same".
  • Allow the merge conflict to happen, but then just construct the correct result by wiping out Git's attempt to merge the file(s), substituting in correct hand-merged file(s) instead.

Note that if some file in B exists with the same name in H or T, Git will (currently) always pair those two files up, even if they're not related. For instance, suppose commit B had a file named polish, referring to the country Poland, and both commits H and T rename this to polish-as-in-the-country while one commit, either H or T, creates a new file named polish that refers to shoe polish. Git will identify B's polish (country) with the new polish (shoe) because these are the same name. Plain git diff can be told to break this association, but git merge cannot.

Renaming the shoe-polish file to polish-as-in-the-shoes will cause Git not to see a polish in both B and the one tip commit. Now Git will search for a "best match", and find instead polish-as-in-the-country each time, and know that the same rename was performed in both branches.

A rebase is (like) repeated cherry-picks, and each cherry-pick is a merge

Running git rebase <upstream> or git rebase --onto <target> <upstream> tells Git to copy a series of commits. The commits to copy are (essentially) as the documentation says those shown by git log <upstream>..HEAD.1 The commit after which they get copied is the one given by <target>, or is <upstream> if you do not specify a <target>.

In any case, once Git has the list of commits to copy, it copies them as if by running git cherry-pick on each one. (Depending on your particular git rebase command, this may actually run git cherry-pick, or it may simulate it through git format-patch ... | git am -3. I have not yet constructed a good example to illustrate when these produce different results—but if you do get a merge conflict, it's because git am -3 fell back to three-way merge, which has the same effect as using git cherry-pick.)

Annoyingly, while you can control merge results with git merge (by adding new commits, or supplying -X options), you have little to no control over each cherry-pick during a rebase. The merge base of any cherry-pick is the parent commit of the commit being picked. The --theirs commit is the commit being cherry-picked, and the --ours or HEAD commit is the commit being built-upon. If the rebase is just starting, that's the <target> you gave, otherwise it's the commit most recently successfully copied.

In any case, at this point you're down to the one option: merge the file correctly by hand. (Well, that or terminate the rebase attempt entirely, with git rebase --abort. You could then use git cherry-pick yourself, repeatedly, and add -X arguments if appropriate.)


1The documentation has a white lie here. It's almost those commits that Git copies; it is actually those listed by git rev-list --cherry-pick --right-only --no-merges <upstream>...HEAD. This is the same list, except that it:

  • omits any merge commits, and
  • omits any commit that has the same git patch-id in the upstream target.

Moreover, as is actually documented, --fork-point changes the starting point from the given <upstream> argument in some cases, so as to omit even more commits. See Git rebase - commit select in fork-point mode.

torek
  • 448,244
  • 59
  • 642
  • 775
  • A phenomenal explanation! This has made a measurable improvement in my understanding of Git. Thanks for going to the effort here! – David Parks Nov 12 '17 at 16:23
  • 1
    I forgot to add: `git rebase` works much as if by `git cherry-pick`ing each commit, and you have no control over which commits are base, HEAD/ours, and theirs, so when things go wrong during `git rebase` it's even more painful than when they go wrong during `git merge`. – torek Nov 12 '17 at 17:48