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
This commit is contained in:
Mark Thomas 2017-10-02 14:05:30 -07:00
parent e2916432c0
commit 75628a759b
4 changed files with 137 additions and 13 deletions

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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