D784: merge: check for path conflicts when merging

mbthomas (Mark Thomas) phabricator at mercurial-scm.org
Fri Sep 22 09:28:58 UTC 2017


mbthomas created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  When merging, check for any path conflicts introduced by the manifest
  merge and rename the conflicting file to a safe name.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D784

AFFECTED FILES
  mercurial/merge.py
  tests/test-audit-path.t
  tests/test-commandserver.t
  tests/test-pathconflicts-basic.t

CHANGE DETAILS

diff --git a/tests/test-pathconflicts-basic.t b/tests/test-pathconflicts-basic.t
--- a/tests/test-pathconflicts-basic.t
+++ b/tests/test-pathconflicts-basic.t
@@ -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
 
diff --git a/tests/test-commandserver.t b/tests/test-commandserver.t
--- a/tests/test-commandserver.t
+++ b/tests/test-commandserver.t
@@ -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
diff --git a/tests/test-audit-path.t b/tests/test-audit-path.t
--- a/tests/test-audit-path.t
+++ b/tests/test-audit-path.t
@@ -160,17 +160,24 @@
 
   $ 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)
 
   $ 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 ..
diff --git a/mercurial/merge.py b/mercurial/merge.py
--- a/mercurial/merge.py
+++ b/mercurial/merge.py
@@ -849,6 +849,77 @@
     This is currently not implemented -- it's an extension point."""
     return True
 
+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 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, check if it
+            # conflicts with a local path.
+            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:
+                    # The file is in a directory which aliases a local file.
+                    # We will need to rename the local file.
+                    localconflicts.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:
+            pnew = util.safename(p, str(wctx), 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.
+        for f in mf:
+            if f not in deletedfiles:
+                for p in util.finddirs(f):
+                    if p in remoteconflicts:
+                        m, args, msg = actions[p]
+                        pnew = util.safename(p, str(mctx), 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
+
 def manifestmerge(repo, wctx, p2, pa, branchmerge, force, matcher,
                   acceptremote, followcopies, forcefulldiff=False):
     """
@@ -1024,6 +1095,10 @@
                     actions[f] = ('dc', (None, f, f, False, pa.node()),
                                   "prompt deleted/changed")
 
+    # If we are merging, look for path conflicts.
+    if branchmerge or (not force and wctx.dirty(missing=True, branch=False)):
+        checkpathconflicts(repo, wctx, p2, actions)
+
     return actions, diverge, renamedelete
 
 def _resolvetrivial(repo, wctx, mctx, ancestor, actions):



To: mbthomas, #hg-reviewers
Cc: mercurial-devel


More information about the Mercurial-devel mailing list