diff --git a/mercurial/merge.py b/mercurial/merge.py --- a/mercurial/merge.py +++ b/mercurial/merge.py @@ -848,6 +848,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): """ @@ -1023,6 +1094,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): 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,8 +160,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 +173,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 .. 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-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