merge: check for path conflicts when merging (
issue5628)
When merging, check for any path conflicts introduced by the manifest
merge and rename the conflicting file to a safe name.
Differential Revision: https://phab.mercurial-scm.org/D784
--- a/mercurial/merge.py Mon Oct 02 14:05:30 2017 -0700
+++ b/mercurial/merge.py Mon Oct 02 14:05:30 2017 -0700
@@ -851,6 +851,107 @@
This is currently not implemented -- it's an extension point."""
return True
+def _filesindirs(repo, manifest, dirs):
+ """
+ Generator that yields pairs of all the files in the manifest that are found
+ inside the directories listed in dirs, and which directory they are found
+ in.
+ """
+ for f in manifest:
+ for p in util.finddirs(f):
+ if p in dirs:
+ yield f, p
+ break
+
+def checkpathconflicts(repo, wctx, mctx, actions):
+ """
+ Check if any actions introduce path conflicts in the repository, updating
+ actions to record or handle the path conflict accordingly.
+ """
+ mf = wctx.manifest()
+
+ # The set of local files that conflict with a remote directory.
+ localconflicts = set()
+
+ # The set of directories that conflict with a remote file, and so may cause
+ # conflicts if they still contain any files after the merge.
+ remoteconflicts = set()
+
+ # The set of directories that appear as both a file and a directory in the
+ # remote manifest. These indicate an invalid remote manifest, which
+ # can't be updated to cleanly.
+ invalidconflicts = set()
+
+ # The set of files deleted by all the actions.
+ deletedfiles = set()
+
+ for f, (m, args, msg) in actions.items():
+ if m in ('c', 'dc', 'm', 'cm'):
+ # This action may create a new local file.
+ if mf.hasdir(f):
+ # The file aliases a local directory. This might be ok if all
+ # the files in the local directory are being deleted. This
+ # will be checked once we know what all the deleted files are.
+ remoteconflicts.add(f)
+ for p in util.finddirs(f):
+ if p in mf:
+ if p in mctx:
+ # The file is in a directory which aliases both a local
+ # and a remote file. This is an internal inconsistency
+ # within the remote manifest.
+ invalidconflicts.add(p)
+ else:
+ # The file is in a directory which aliases a local file.
+ # We will need to rename the local file.
+ localconflicts.add(p)
+ if p in actions and actions[p][0] in ('c', 'dc', 'm', 'cm'):
+ # The file is in a directory which aliases a remote file.
+ # This is an internal inconsistency within the remote
+ # manifest.
+ invalidconflicts.add(p)
+
+ # Track the names of all deleted files.
+ if m == 'r':
+ deletedfiles.add(f)
+ if m == 'm':
+ f1, f2, fa, move, anc = args
+ if move:
+ deletedfiles.add(f1)
+ if m == 'dm':
+ f2, flags = args
+ deletedfiles.add(f2)
+
+ # Rename all local conflicting files that have not been deleted.
+ for p in localconflicts:
+ if p not in deletedfiles:
+ ctxname = str(wctx).rstrip('+')
+ pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
+ actions[pnew] = ('pr', (p,), "local path conflict")
+ actions[p] = ('p', (pnew, 'l'), "path conflict")
+
+ if remoteconflicts:
+ # Check if all files in the conflicting directories have been removed.
+ ctxname = str(mctx).rstrip('+')
+ for f, p in _filesindirs(repo, mf, remoteconflicts):
+ if f not in deletedfiles:
+ m, args, msg = actions[p]
+ pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
+ if m in ('dc', 'm'):
+ # Action was merge, just update target.
+ actions[pnew] = (m, args, msg)
+ else:
+ # Action was create, change to renamed get action.
+ fl = args[0]
+ actions[pnew] = ('dg', (p, fl), "remote path conflict")
+ actions[p] = ('p', (pnew, 'r'), "path conflict")
+ remoteconflicts.remove(p)
+ break
+
+ if invalidconflicts:
+ for p in invalidconflicts:
+ repo.ui.warn(_("%s: is both a file and a directory\n") % p)
+ raise error.Abort(_("destination manifest contains path conflicts"))
+
def manifestmerge(repo, wctx, p2, pa, branchmerge, force, matcher,
acceptremote, followcopies, forcefulldiff=False):
"""
@@ -1026,6 +1127,9 @@
actions[f] = ('dc', (None, f, f, False, pa.node()),
"prompt deleted/changed")
+ # If we are merging, look for path conflicts.
+ checkpathconflicts(repo, wctx, p2, actions)
+
return actions, diverge, renamedelete
def _resolvetrivial(repo, wctx, mctx, ancestor, actions):
--- a/tests/test-audit-path.t Mon Oct 02 14:05:30 2017 -0700
+++ b/tests/test-audit-path.t Mon Oct 02 14:05:30 2017 -0700
@@ -103,7 +103,8 @@
back/test
#if symlink
$ hg update -Cr2
- abort: path 'back/test' traverses symbolic link 'back'
+ back: is both a file and a directory
+ abort: destination manifest contains path conflicts
[255]
#else
('back' will be a file and cause some other system specific error)
@@ -160,8 +161,12 @@
$ hg up -qC 1
$ hg merge 2
- abort: path 'a/poisoned' traverses symbolic link 'a'
- [255]
+ a: path conflict - a file or link has the same name as a directory
+ the local file has been renamed to a~aa04623eb0c3
+ resolve manually then use 'hg resolve --mark a'
+ 1 files updated, 0 files merged, 0 files removed, 1 files unresolved
+ use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+ [1]
try rebase onto other revision: cache of audited paths should be discarded,
and the rebase should fail (issue5628)
@@ -169,8 +174,11 @@
$ hg up -qC 2
$ hg rebase -s 2 -d 1 --config extensions.rebase=
rebasing 2:e73c21d6b244 "file a/poisoned" (tip)
- abort: path 'a/poisoned' traverses symbolic link 'a'
- [255]
+ a: path conflict - a file or link has the same name as a directory
+ the local file has been renamed to a~aa04623eb0c3
+ resolve manually then use 'hg resolve --mark a'
+ unresolved conflicts (see hg resolve, then hg rebase --continue)
+ [1]
$ ls ../merge-symlink-out
$ cd ..
@@ -202,7 +210,8 @@
$ hg up -qC 0
$ hg up 1
- abort: path 'a/b' traverses symbolic link 'a'
+ a: is both a file and a directory
+ abort: destination manifest contains path conflicts
[255]
try linear update including symlinked directory and its content: paths are
@@ -211,7 +220,8 @@
$ hg up -qC null
$ hg up 1
- abort: path 'a/b' traverses symbolic link 'a'
+ a: is both a file and a directory
+ abort: destination manifest contains path conflicts
[255]
$ ls ../update-symlink-out
@@ -222,7 +232,8 @@
$ rm -f a
$ hg up -qC 2
$ hg up 1
- abort: path 'a/b' traverses symbolic link 'a'
+ a: is both a file and a directory
+ abort: destination manifest contains path conflicts
[255]
$ ls ../update-symlink-out
--- a/tests/test-commandserver.t Mon Oct 02 14:05:30 2017 -0700
+++ b/tests/test-commandserver.t Mon Oct 02 14:05:30 2017 -0700
@@ -966,8 +966,12 @@
*** runcommand up -qC 2
*** runcommand up -qC 1
*** runcommand merge 2
- abort: path 'a/poisoned' traverses symbolic link 'a'
- [255]
+ a: path conflict - a file or link has the same name as a directory
+ the local file has been renamed to a~aa04623eb0c3
+ resolve manually then use 'hg resolve --mark a'
+ 1 files updated, 0 files merged, 0 files removed, 1 files unresolved
+ use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+ [1]
$ ls ../merge-symlink-out
cache of repo.auditor should be discarded, so matcher would never traverse
--- a/tests/test-pathconflicts-basic.t Mon Oct 02 14:05:30 2017 -0700
+++ b/tests/test-pathconflicts-basic.t Mon Oct 02 14:05:30 2017 -0700
@@ -25,11 +25,16 @@
$ hg bookmark -i
$ hg merge --verbose dir
resolving manifests
+ a: path conflict - a file or link has the same name as a directory
+ the local file has been renamed to a~853701544ac3
+ resolve manually then use 'hg resolve --mark a'
+ moving a to a~853701544ac3
getting a/b
- abort: *: '$TESTTMP/repo/a/b' (glob)
- [255]
+ 1 files updated, 0 files merged, 0 files removed, 1 files unresolved
+ use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
+ [1]
$ hg update --clean .
- 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
Basic update - local directory conflicts with remote file