diff --git a/hgext/fsmonitor/__init__.py b/hgext/fsmonitor/__init__.py --- a/hgext/fsmonitor/__init__.py +++ b/hgext/fsmonitor/__init__.py @@ -382,7 +382,7 @@ def overridewalk(orig, self, match, subr visit.update(f for f in copymap if f not in results and matchfn(f)) - audit = pathutil.pathauditor(self._root).check + audit = pathutil.pathauditor(self._root, cached=True).check auditpass = [f for f in visit if audit(f)] auditpass.sort() auditfail = visit.difference(auditpass) diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py --- a/mercurial/cmdutil.py +++ b/mercurial/cmdutil.py @@ -3538,7 +3538,7 @@ def _performrevert(repo, parents, ctx, a pass repo.dirstate.remove(f) - audit_path = pathutil.pathauditor(repo.root) + audit_path = pathutil.pathauditor(repo.root, cached=True) for f in actions['forget'][0]: if interactive: choice = repo.ui.promptchoice( diff --git a/mercurial/dirstate.py b/mercurial/dirstate.py --- a/mercurial/dirstate.py +++ b/mercurial/dirstate.py @@ -1153,7 +1153,7 @@ class dirstate(object): # that wasn't ignored, and everything that matched was stat'ed # and is already in results. # The rest must thus be ignored or under a symlink. - audit_path = pathutil.pathauditor(self._root) + audit_path = pathutil.pathauditor(self._root, cached=True) for nf in iter(visit): # If a stat for the same file was already added with a diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py --- a/mercurial/localrepo.py +++ b/mercurial/localrepo.py @@ -339,11 +339,11 @@ class localrepository(object): # only used when writing this comment: basectx.match self.auditor = pathutil.pathauditor(self.root, self._checknested) self.nofsauditor = pathutil.pathauditor(self.root, self._checknested, - realfs=False) + realfs=False, cached=True) self.baseui = baseui self.ui = baseui.copy() self.ui.copy = baseui.copy # prevent copying repo configuration - self.vfs = vfsmod.vfs(self.path) + self.vfs = vfsmod.vfs(self.path, cacheaudited=True) if (self.ui.configbool('devel', 'all-warnings') or self.ui.configbool('devel', 'check-locks')): self.vfs.audit = self._getvfsward(self.vfs.audit) @@ -426,12 +426,13 @@ class localrepository(object): '"sparse" extensions to access')) self.store = store.store( - self.requirements, self.sharedpath, vfsmod.vfs) + self.requirements, self.sharedpath, + lambda base: vfsmod.vfs(base, cacheaudited=True)) self.spath = self.store.path self.svfs = self.store.vfs self.sjoin = self.store.join self.vfs.createmode = self.store.createmode - self.cachevfs = vfsmod.vfs(cachepath) + self.cachevfs = vfsmod.vfs(cachepath, cacheaudited=True) self.cachevfs.createmode = self.store.createmode if (self.ui.configbool('devel', 'all-warnings') or self.ui.configbool('devel', 'check-locks')): diff --git a/mercurial/pathutil.py b/mercurial/pathutil.py --- a/mercurial/pathutil.py +++ b/mercurial/pathutil.py @@ -33,13 +33,18 @@ class pathauditor(object): The file system checks are only done when 'realfs' is set to True (the default). They should be disable then we are auditing path for operation on stored history. + + If 'cached' is set to True, audited paths and sub-directories are cached. + Be careful to not keep the cache of unmanaged directories for long because + audited paths may be replaced with symlinks. ''' - def __init__(self, root, callback=None, realfs=True): + def __init__(self, root, callback=None, realfs=True, cached=False): self.audited = set() self.auditeddir = set() self.root = root self._realfs = realfs + self._cached = cached self.callback = callback if os.path.lexists(root) and not util.fscasesensitive(root): self.normcase = util.normcase @@ -96,10 +101,11 @@ class pathauditor(object): self._checkfs(prefix, path) prefixes.append(normprefix) - self.audited.add(normpath) - # only add prefixes to the cache after checking everything: we don't - # want to add "foo/bar/baz" before checking if there's a "foo/.hg" - self.auditeddir.update(prefixes) + if self._cached: + self.audited.add(normpath) + # only add prefixes to the cache after checking everything: we don't + # want to add "foo/bar/baz" before checking if there's a "foo/.hg" + self.auditeddir.update(prefixes) def _checkfs(self, prefix, path): """raise exception if a file system backed check fails""" diff --git a/mercurial/scmutil.py b/mercurial/scmutil.py --- a/mercurial/scmutil.py +++ b/mercurial/scmutil.py @@ -738,7 +738,7 @@ def _interestingfiles(repo, matcher): This is different from dirstate.status because it doesn't care about whether files are modified or clean.''' added, unknown, deleted, removed, forgotten = [], [], [], [], [] - audit_path = pathutil.pathauditor(repo.root) + audit_path = pathutil.pathauditor(repo.root, cached=True) ctx = repo[None] dirstate = repo.dirstate diff --git a/mercurial/vfs.py b/mercurial/vfs.py --- a/mercurial/vfs.py +++ b/mercurial/vfs.py @@ -295,8 +295,13 @@ class vfs(abstractvfs): This class is used to hide the details of COW semantics and remote file access from higher level code. + + 'cacheaudited' should be enabled only if (a) vfs object is short-lived, or + (b) the base directory is managed by hg and considered sort-of append-only. + See pathutil.pathauditor() for details. ''' - def __init__(self, base, audit=True, expandpath=False, realpath=False): + def __init__(self, base, audit=True, cacheaudited=False, expandpath=False, + realpath=False): if expandpath: base = util.expandpath(base) if realpath: @@ -304,7 +309,7 @@ class vfs(abstractvfs): self.base = base self._audit = audit if audit: - self.audit = pathutil.pathauditor(self.base) + self.audit = pathutil.pathauditor(self.base, cached=cacheaudited) else: self.audit = (lambda path, mode=None: True) self.createmode = None 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 @@ -169,9 +169,9 @@ 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) - saved backup bundle to * (glob) + abort: path 'a/poisoned' traverses symbolic link 'a' + [255] $ ls ../merge-symlink-out - poisoned $ cd .. @@ -211,10 +211,9 @@ audited first by calculateupdates(), whe $ hg up -qC null $ hg up 1 - 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + abort: path 'a/b' traverses symbolic link 'a' + [255] $ ls ../update-symlink-out - b - $ rm ../update-symlink-out/b try branch update replacing directory with symlink, and its content: the path 'a' is audited as a directory first, which should be audited again as @@ -223,9 +222,9 @@ a symlink. $ rm -f a $ hg up -qC 2 $ hg up 1 - 2 files updated, 0 files merged, 1 files removed, 0 files unresolved + abort: path 'a/b' traverses symbolic link 'a' + [255] $ ls ../update-symlink-out - b $ cd .. diff --git a/tests/test-commandserver.t b/tests/test-commandserver.t --- a/tests/test-commandserver.t +++ b/tests/test-commandserver.t @@ -953,10 +953,9 @@ and the merge should fail (issue5628) *** runcommand up -qC 2 *** runcommand up -qC 1 *** runcommand merge 2 - 1 files updated, 0 files merged, 0 files removed, 0 files unresolved - (branch merge, don't forget to commit) + abort: path 'a/poisoned' traverses symbolic link 'a' + [255] $ ls ../merge-symlink-out - poisoned cache of repo.auditor should be discarded, so matcher would never traverse symlinks: @@ -980,7 +979,8 @@ symlinks: *** runcommand up -qC 0 *** runcommand up -qC 1 *** runcommand files a/poisoned - [1] + abort: path 'a/poisoned' traverses symbolic link 'a' + [255] $ cd ..