From 75628a759b6ba5d8eeffebc2d2bcb92155580bdf Mon Sep 17 00:00:00 2001 From: Mark Thomas Date: Mon, 2 Oct 2017 14:05:30 -0700 Subject: [PATCH] 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 --- mercurial/merge.py | 104 +++++++++++++++++++++++++++++++ tests/test-audit-path.t | 27 +++++--- tests/test-commandserver.t | 8 ++- tests/test-pathconflicts-basic.t | 11 +++- 4 files changed, 137 insertions(+), 13 deletions(-) diff --git a/mercurial/merge.py b/mercurial/merge.py index 4ad46fd4e6..e36d282174 100644 --- a/mercurial/merge.py +++ b/mercurial/merge.py @@ -851,6 +851,107 @@ def driverconclude(repo, ms, wctx, labels=None): 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 @@ def manifestmerge(repo, wctx, p2, pa, branchmerge, force, matcher, 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): diff --git a/tests/test-audit-path.t b/tests/test-audit-path.t index 4bad0a0cdf..37d4b65dc8 100644 --- a/tests/test-audit-path.t +++ b/tests/test-audit-path.t @@ -103,7 +103,8 @@ attack back/test where back symlinks to .. 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 @@ try trivial merge $ 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 @@ 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 .. @@ -202,7 +210,8 @@ try linear update where symlink already exists: $ 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 @@ audited first by calculateupdates(), where no symlink is created so both $ 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 @@ a symlink. $ 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 diff --git a/tests/test-commandserver.t b/tests/test-commandserver.t index c727971b42..3e78b234e0 100644 --- a/tests/test-commandserver.t +++ b/tests/test-commandserver.t @@ -966,8 +966,12 @@ and the merge should fail (issue5628) *** 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-pathconflicts-basic.t b/tests/test-pathconflicts-basic.t index a521cf1936..2b8ec7abea 100644 --- a/tests/test-pathconflicts-basic.t +++ b/tests/test-pathconflicts-basic.t @@ -25,11 +25,16 @@ Basic merge - local file conflicts with remote directory $ 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