Search code examples
gitgit-mergegit-merge-conflict

Git file-level merge conflict caused by Git “suggesting [the file] should perhaps be moved”


I've got a Git repo of Markdown articles, where people create pull requests (one per Markdown article) that eventually get merged to master and then, working directly on master, I move that article into a folder called wordpressed and commit and push to master.

And I've got a very old pull request that is still sitting there in a branch called home-base, consisting of a file dan/homebase.md which I'm thinking of editing into shape with some more commits and then merging to master.

This home-base branch is so old that the repo, seen from its point of view, is totally different from how it looks today in master. So I thought it might be good to reverse merge master into home-base just to bring it more up to date and move the merge base up a lot.

But when I try to do that, I get a merge conflict that I can't understand.


To show you the situation, I'm going to show what a ls of files and folders looks like in each branch. I'll start with master:

$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean

$ ls -1 -R
README.md
dan
instructions.md
wordpressed

./dan:

./wordpressed:
AppDelegateRefactor.md
BareListContentView.md
CellContentConfiguration.md
ControlTargetActioniOS14.md
GetStartedWithPods.md
MultipleTrailingClosures.md
StatesAndBackgrounds.md
Swift52notes1.md
Swift52notes2.md
Swift52notes3.md
UserInteractiveCells.md
bdd-common-pitfalls.md
bdd.md
brantsMenuBar.md
collectionViewContentConfig.md
collectionViewLists.md
collectionViewLists2.md
collectionViewOutline.md
debuggingLinks.md
diffableDataSources.md
diffableDataSources2.md
forwardCompatibity.md
haptics.md
iOS13deprecations.md
images
inAppPurchases.md
logging.md
miscSwiftTricks1.md
miscSwiftTricks2.md
miscSwiftTricks3.md
miscSwiftTricks4.md
multipleSelection.md
mysteriesOfLayout.md
newInIOS14.md
packages.md
photoPicker.md
protocolParadox.md
splitViewControllers.md
splitViewControllers2.md
swiftTrickTwoClasses.md
targetActionRant.md
typescript-shape-of-things.md
untappableButton.md
what-vs-how.md
xcode12Editing.md
xcode12testing.md
xcodeWhereAmI.md
xcodeWorkInTwoPlaces.md

./wordpressed/images:
argumentsInScheme.png
bigFlags.png
callHierarchy.png
callersMenu.png
chooser.png
config.png
createdTester.png
dataOrUrl.png
ipadportraitoverlay.png
listOfPepBoys.png
listWithSectionHeaders.png
littleFlags.png
looksLikeADuck.png
newTester.png
outlineIndicatorsWrong.png
outlineIndicatorsWrong2.png
perfectAfterHack.png
pickAPeppa.png
rootOnly.png
sandboxAccountOnDevice.png
selfSizingStringDrawer.png
simpleList.png
split1.png
split2.png
splitViewControllerPadLandscape.png
tableViewAsChoice.png
threecolumns.png
twoPossibilities.png
twoscreensphone.png
variableRowHeights.png
workingOutline.png

As you can see, the main folder is itself mostly empty. All the articles are in wordpressed, along with their images which are in wordpressed/images.


Okay, now here is home-base:

$ git switch home-base
Switched to branch 'home-base'
Your branch is up to date with 'origin/home-base'.

$ ls -1 -R
GetStartedWithPods.md
README.md
Swift52notes1.md
Swift52notes2.md
Swift52notes3.md
dan
debuggingLinks.md
images
logging.md
miscSwiftTricks1.md
miscSwiftTricks2.md
miscSwiftTricks3.md
miscSwiftTricks4.md
packages.md
wordpressed
xcode12Editing.md
xcode12testing.md
xcodeWhereAmI.md
xcodeWorkInTwoPlaces.md

./dan:
bdd.md
homebase.md

./images:
chooser.png
config.png
split1.png
split2.png

./wordpressed:
images

./wordpressed/images:

As you can see, that's a much earlier situation. Nothing had been moved to wordpressed yet. There were a few articles floating around the top level, and the dan folder had two articles, including the one I'm interested in here, homebase.md.


Finally, for the sake of completeness, I'm going to show you the merge base between master and home-base:

$ git merge-base master home-base
b5d7355fe42eddad96beb200df2cba65381c288a
$ git checkout b5d7355fe
$ ls -1 -R
GetStartedWithPods.md
README.md
Swift52notes1.md
Swift52notes2.md
Swift52notes3.md
dan
debuggingLinks.md
images
logging.md
miscSwiftTricks1.md
miscSwiftTricks2.md
miscSwiftTricks3.md
miscSwiftTricks4.md
packages.md
wordpressed
xcode12Editing.md
xcode12testing.md
xcodeWhereAmI.md
xcodeWorkInTwoPlaces.md

./dan:
bdd.md

./images:
chooser.png
config.png
split1.png
split2.png

./wordpressed:
images

./wordpressed/images:

Well, it looks an awful lot like home-base looks now, doesn't it? The main difference is that that's before homebase.md appeared in the dan folder.


Now then. Ask yourself, please, what should happen when I try to merge master into home-base. What are contributions from both sides of the merge? In my view, Git should realize that in master a lot of new files appeared in wordpressed, and that some of them are renames of files that used to be at the top level. Plus, of course, in home-base, a new file homebase.md has appeared in dan.

Those contributions should not conflict (in my view).

Okay, let's try it.

$ git switch home-base
$ git merge master
CONFLICT (file location): dan/homebase.md added in HEAD inside a directory that was renamed in master, suggesting it should perhaps be moved to wordpressed/homebase.md.
Automatic merge failed; fix conflicts and then commit the result.

$ git status
On branch home-base
Your branch is up to date with 'origin/home-base'.

You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Changes to be committed:
    deleted:    dan/homebase.md
    new file:   instructions.md
    new file:   wordpressed/AppDelegateRefactor.md
    new file:   wordpressed/BareListContentView.md
    new file:   wordpressed/CellContentConfiguration.md
    new file:   wordpressed/ControlTargetActioniOS14.md
    renamed:    GetStartedWithPods.md -> wordpressed/GetStartedWithPods.md
    new file:   wordpressed/MultipleTrailingClosures.md
    new file:   wordpressed/StatesAndBackgrounds.md
    renamed:    Swift52notes1.md -> wordpressed/Swift52notes1.md
    renamed:    Swift52notes2.md -> wordpressed/Swift52notes2.md
    renamed:    Swift52notes3.md -> wordpressed/Swift52notes3.md
    new file:   wordpressed/UserInteractiveCells.md
    new file:   wordpressed/bdd-common-pitfalls.md
    renamed:    dan/bdd.md -> wordpressed/bdd.md
    new file:   wordpressed/brantsMenuBar.md
    new file:   wordpressed/collectionViewContentConfig.md
    new file:   wordpressed/collectionViewLists.md
    new file:   wordpressed/collectionViewLists2.md
    new file:   wordpressed/collectionViewOutline.md
    renamed:    debuggingLinks.md -> wordpressed/debuggingLinks.md
    new file:   wordpressed/diffableDataSources.md
    new file:   wordpressed/diffableDataSources2.md
    new file:   wordpressed/forwardCompatibity.md
    new file:   wordpressed/haptics.md
    new file:   wordpressed/iOS13deprecations.md
    new file:   wordpressed/images/argumentsInScheme.png
    new file:   wordpressed/images/bigFlags.png
    new file:   wordpressed/images/callHierarchy.png
    new file:   wordpressed/images/callersMenu.png
    renamed:    images/chooser.png -> wordpressed/images/chooser.png
    renamed:    images/config.png -> wordpressed/images/config.png
    new file:   wordpressed/images/createdTester.png
    new file:   wordpressed/images/dataOrUrl.png
    new file:   wordpressed/images/ipadportraitoverlay.png
    new file:   wordpressed/images/listOfPepBoys.png
    new file:   wordpressed/images/listWithSectionHeaders.png
    new file:   wordpressed/images/littleFlags.png
    new file:   wordpressed/images/looksLikeADuck.png
    new file:   wordpressed/images/newTester.png
    new file:   wordpressed/images/outlineIndicatorsWrong.png
    new file:   wordpressed/images/outlineIndicatorsWrong2.png
    new file:   wordpressed/images/perfectAfterHack.png
    new file:   wordpressed/images/pickAPeppa.png
    new file:   wordpressed/images/rootOnly.png
    new file:   wordpressed/images/sandboxAccountOnDevice.png
    new file:   wordpressed/images/selfSizingStringDrawer.png
    new file:   wordpressed/images/simpleList.png
    renamed:    images/split1.png -> wordpressed/images/split1.png
    renamed:    images/split2.png -> wordpressed/images/split2.png
    new file:   wordpressed/images/splitViewControllerPadLandscape.png
    new file:   wordpressed/images/tableViewAsChoice.png
    new file:   wordpressed/images/threecolumns.png
    new file:   wordpressed/images/twoPossibilities.png
    new file:   wordpressed/images/twoscreensphone.png
    new file:   wordpressed/images/variableRowHeights.png
    new file:   wordpressed/images/workingOutline.png
    new file:   wordpressed/inAppPurchases.md
    renamed:    logging.md -> wordpressed/logging.md
    renamed:    miscSwiftTricks1.md -> wordpressed/miscSwiftTricks1.md
    renamed:    miscSwiftTricks2.md -> wordpressed/miscSwiftTricks2.md
    renamed:    miscSwiftTricks3.md -> wordpressed/miscSwiftTricks3.md
    renamed:    miscSwiftTricks4.md -> wordpressed/miscSwiftTricks4.md
    new file:   wordpressed/multipleSelection.md
    new file:   wordpressed/mysteriesOfLayout.md
    new file:   wordpressed/newInIOS14.md
    renamed:    packages.md -> wordpressed/packages.md
    new file:   wordpressed/photoPicker.md
    new file:   wordpressed/protocolParadox.md
    new file:   wordpressed/splitViewControllers.md
    new file:   wordpressed/splitViewControllers2.md
    new file:   wordpressed/swiftTrickTwoClasses.md
    new file:   wordpressed/targetActionRant.md
    new file:   wordpressed/typescript-shape-of-things.md
    new file:   wordpressed/untappableButton.md
    new file:   wordpressed/what-vs-how.md
    renamed:    xcode12Editing.md -> wordpressed/xcode12Editing.md
    renamed:    xcode12testing.md -> wordpressed/xcode12testing.md
    renamed:    xcodeWhereAmI.md -> wordpressed/xcodeWhereAmI.md
    renamed:    xcodeWorkInTwoPlaces.md -> wordpressed/xcodeWorkInTwoPlaces.md

Unmerged paths:
  (use "git add <file>..." to mark resolution)
    added by us:     wordpressed/homebase.md 

Okay, so that last line is the one I don't understand. What does it mean to say that "added by us" is wordpressed/homebase.md? NO! That's not where we added it. We added it in dan.

Neither master nor home-base has a file wordpressed/homebase.md. So where is Git getting the idea that there is one??? It seems to have something to do with the same reason why it thinks there is a merge conflict at all:

dan/homebase.md added in HEAD inside a directory that was renamed in master, suggesting it should perhaps be moved to wordpressed/homebase.md.

So: What is Git talking about here? Why does Git think that dan/homebase.md should be wordpressed/homebase.md, on no evidence whatever that I can find? How can I prevent this merge conflict and get Git to be more simplistic about this? Could I perhaps use a different merge strategy? Or how can I simply resolve the conflict in such a way as to say, No, Git, just leave dan/homebase.md alone, please?

Again, apologies for the length of the detail in the question.


Solution

  • This particular conflict:

    CONFLICT (file location): dan/homebase.md added in HEAD inside a
    directory that was renamed in master, suggesting it should perhaps
    be moved to wordpressed/homebase.md.
    

    is a relatively modern invention in Git. Git now tries to find directory renames between the merge base commit and each tip commit. That is, having run git diff --find-renames <base> <left> and git diff --find-renames <base> <right>, it detected some number of renames on the left and some number of renames on the right (with the total number being positive). At least some of those renames implied a directory-name change. So Git now thinks that maybe the other side of the merge should have the same directory-name-changes applied.

    (Git pre-release versions in the mid 2.teens or so had various experimental versions of this code added and I forget when it really stabilized. Perusing the release notes, it looks like 2.17 is when it went in, and then there was a subsequent corner case fixed in 2.19.)

    [Edit, 15 Apr 2021:] When this new feature went in, so did a new control knob: merge.directoryRenames. Its default value is conflict, but you can set it to true (meaning always make the assumption it is already making, but don't call it a conflict) or false (meaning never make the new assumption, and thus don't call it a conflict).

    This is the only conflict that Git is announcing, so it's the only file that shows up in the final "unmerged files" section:

    Unmerged paths:
      (use "git add <file>..." to mark resolution)
        added by us:     wordpressed/homebase.md
    

    Git has, here, taken its own internal suggestion: it believes that everything added to dan/ between the merge base and HEAD should have been added instead to wordpressed/ due to changes it saw between the merge base and the tip of master.

    If this seems correct to you, Git would like you to confirm this, by running git add on that path name. If not, Git would like you to move the file to wherever you think it should go, and git add the result (and remove, with git rm or perhaps with git add, the name Git used).