dirstate: drop explicit files that shouldn't match (BC) (issue4679)
Before, wctx.walk() could include files excluded by -X pattern, which
disagrees with wctx.matches() and ctx.walk()/matches() behavior. This patch
fixes the problem by testing stat results against the matcher if the matcher
may contain false paths.
I have no idea if the fix should be made before the workaround for case-
insensitive filesystems, but that shouldn't matter since match.anypats()
means 'not match.isexact()'.
This patch also makes narrow and sparse extensions to not exclude explicit
paths on walk() because they appear to depend on the buggy behavior.
More detailed analysis about this issue by Martin von Zweigbergk:
"I think it's just an unintended consequence of how the dirstate walk works,
but I'm not sure. The exception for explicit files also bothered me when I
was working on the matcher code a year or so ago. I actually added the
exception to the matcher code because I thought it was always working like
that (not just for dirstate) in a83a7d27911e (match: handle excludes using
new differencematcher, 2017-05-16). It was only recently that Yuya realized
that it used to be inconsistent and that I probably made it consistently bad
because I didn't realize it was inconsistent to start with, see 821d8a5ab4ff
(match: do not weirdly include explicit files excluded by -X option,
2018-01-16)."
.. bc::
Working-directory commands now respect ``-X PATTERN`` no matter if PATTERN
matches explicitly-specified FILEs. For example, ``hg add foo -X foo`` no
longer add the file ``foo``.
--- a/hgext/narrow/narrowdirstate.py Sun Jan 21 17:04:42 2018 +0900
+++ b/hgext/narrow/narrowdirstate.py Fri Jan 26 19:48:39 2018 +0900
@@ -23,8 +23,11 @@
def walk(orig, self, match, subrepos, unknown, ignored, full=True,
narrowonly=True):
if narrowonly:
- narrowmatch = repo.narrowmatch()
- match = matchmod.intersectmatchers(match, narrowmatch)
+ # hack to not exclude explicitly-specified paths so that they can
+ # be warned later on e.g. dirstate.add()
+ em = matchmod.exact(match._root, match._cwd, match.files())
+ nm = matchmod.unionmatcher([repo.narrowmatch(), em])
+ match = matchmod.intersectmatchers(match, nm)
return orig(self, match, subrepos, unknown, ignored, full)
extensions.wrapfunction(dirstate.dirstate, 'walk', walk)
--- a/hgext/sparse.py Sun Jan 21 17:04:42 2018 +0900
+++ b/hgext/sparse.py Fri Jan 26 19:48:39 2018 +0900
@@ -194,7 +194,11 @@
"""
def walk(orig, self, match, subrepos, unknown, ignored, full=True):
- match = matchmod.intersectmatchers(match, self._sparsematcher)
+ # hack to not exclude explicitly-specified paths so that they can
+ # be warned later on e.g. dirstate.add()
+ em = matchmod.exact(match._root, match._cwd, match.files())
+ sm = matchmod.unionmatcher([self._sparsematcher, em])
+ match = matchmod.intersectmatchers(match, sm)
return orig(self, match, subrepos, unknown, ignored, full)
extensions.wrapfunction(dirstate.dirstate, 'walk', walk)
--- a/mercurial/dirstate.py Sun Jan 21 17:04:42 2018 +0900
+++ b/mercurial/dirstate.py Fri Jan 26 19:48:39 2018 +0900
@@ -787,6 +787,17 @@
else:
badfn(ff, encoding.strtolocal(inst.strerror))
+ # match.files() may contain explicitly-specified paths that shouldn't
+ # be taken; drop them from the list of files found. dirsfound/notfound
+ # aren't filtered here because they will be tested later.
+ if match.anypats():
+ for f in list(results):
+ if f == '.hg' or f in subrepos:
+ # keep sentinel to disable further out-of-repo walks
+ continue
+ if not match(f):
+ del results[f]
+
# Case insensitive filesystems cannot rely on lstat() failing to detect
# a case-only rename. Prune the stat object for any file that does not
# match the case in the filesystem, if there are multiple files that
--- a/tests/test-add.t Sun Jan 21 17:04:42 2018 +0900
+++ b/tests/test-add.t Fri Jan 26 19:48:39 2018 +0900
@@ -146,6 +146,13 @@
M a
? a.orig
+excluded file shouldn't be added even if it is explicitly specified
+
+ $ hg add a.orig -X '*.orig'
+ $ hg st
+ M a
+ ? a.orig
+
Forgotten file can be added back (as either clean or modified)
$ hg forget b
--- a/tests/test-narrow-commit.t Sun Jan 21 17:04:42 2018 +0900
+++ b/tests/test-narrow-commit.t Fri Jan 26 19:48:39 2018 +0900
@@ -55,6 +55,11 @@
$ hg add outside/f3
abort: cannot track 'outside/f3' - it is outside the narrow clone
[255]
+
+But adding a truly excluded file shouldn't count
+
+ $ hg add outside/f3 -X outside/f3
+
$ rm -r outside
Can modify dirstate inside
--- a/tests/test-sparse.t Sun Jan 21 17:04:42 2018 +0900
+++ b/tests/test-sparse.t Fri Jan 26 19:48:39 2018 +0900
@@ -129,6 +129,10 @@
(include file with `hg debugsparse --include <pattern>` or use `hg add -s <file>` to include file directory while adding)
[255]
+But adding a truly excluded file shouldn't count
+
+ $ hg add hide3 -X hide3
+
Verify deleting sparseness while a file has changes fails
$ hg debugsparse --delete 'show*'
--- a/tests/test-walk.t Sun Jan 21 17:04:42 2018 +0900
+++ b/tests/test-walk.t Fri Jan 26 19:48:39 2018 +0900
@@ -304,12 +304,10 @@
f beans/turtle beans/turtle
$ hg debugwalk -Xbeans/black beans/black
matcher: <differencematcher m1=<patternmatcher patterns='(?:beans\\/black(?:/|$))'>, m2=<includematcher includes='(?:beans\\/black(?:/|$))'>>
- f beans/black beans/black exact
$ hg debugwalk -Xbeans/black -Ibeans/black
matcher: <differencematcher m1=<includematcher includes='(?:beans\\/black(?:/|$))'>, m2=<includematcher includes='(?:beans\\/black(?:/|$))'>>
$ hg debugwalk -Xbeans beans/black
matcher: <differencematcher m1=<patternmatcher patterns='(?:beans\\/black(?:/|$))'>, m2=<includematcher includes='(?:beans(?:/|$))'>>
- f beans/black beans/black exact
$ hg debugwalk -Xbeans -Ibeans/black
matcher: <differencematcher m1=<includematcher includes='(?:beans\\/black(?:/|$))'>, m2=<includematcher includes='(?:beans(?:/|$))'>>
$ hg debugwalk 'glob:mammals/../beans/b*'
@@ -345,17 +343,13 @@
[255]
Test explicit paths and excludes:
-(BROKEN: nothing should be included, but wctx.walk() does)
$ hg debugwalk fennel -X fennel
matcher: <differencematcher m1=<patternmatcher patterns='(?:fennel(?:/|$))'>, m2=<includematcher includes='(?:fennel(?:/|$))'>>
- f fennel fennel exact
$ hg debugwalk fennel -X 'f*'
matcher: <differencematcher m1=<patternmatcher patterns='(?:fennel(?:/|$))'>, m2=<includematcher includes='(?:f[^/]*(?:/|$))'>>
- f fennel fennel exact
$ hg debugwalk beans/black -X 'path:beans'
matcher: <differencematcher m1=<patternmatcher patterns='(?:beans\\/black(?:/|$))'>, m2=<includematcher includes='(?:beans(?:/|$))'>>
- f beans/black beans/black exact
$ hg debugwalk -I 'path:beans/black' -X 'path:beans'
matcher: <differencematcher m1=<includematcher includes='(?:beans\\/black(?:/|$))'>, m2=<includematcher includes='(?:beans(?:/|$))'>>