##// END OF EJS Templates
rust-dirstate: add support for nevermatcher...
Raphaël Gomès -
r50247:97dcd690 default
parent child Browse files
Show More
@@ -1,1469 +1,1470 b''
1 # dirstate.py - working directory tracking for mercurial
1 # dirstate.py - working directory tracking for mercurial
2 #
2 #
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8
8
9 import collections
9 import collections
10 import contextlib
10 import contextlib
11 import os
11 import os
12 import stat
12 import stat
13 import uuid
13 import uuid
14
14
15 from .i18n import _
15 from .i18n import _
16 from .pycompat import delattr
16 from .pycompat import delattr
17
17
18 from hgdemandimport import tracing
18 from hgdemandimport import tracing
19
19
20 from . import (
20 from . import (
21 dirstatemap,
21 dirstatemap,
22 encoding,
22 encoding,
23 error,
23 error,
24 match as matchmod,
24 match as matchmod,
25 node,
25 node,
26 pathutil,
26 pathutil,
27 policy,
27 policy,
28 pycompat,
28 pycompat,
29 scmutil,
29 scmutil,
30 sparse,
30 sparse,
31 util,
31 util,
32 )
32 )
33
33
34 from .dirstateutils import (
34 from .dirstateutils import (
35 timestamp,
35 timestamp,
36 )
36 )
37
37
38 from .interfaces import (
38 from .interfaces import (
39 dirstate as intdirstate,
39 dirstate as intdirstate,
40 util as interfaceutil,
40 util as interfaceutil,
41 )
41 )
42
42
43 parsers = policy.importmod('parsers')
43 parsers = policy.importmod('parsers')
44 rustmod = policy.importrust('dirstate')
44 rustmod = policy.importrust('dirstate')
45
45
46 HAS_FAST_DIRSTATE_V2 = rustmod is not None
46 HAS_FAST_DIRSTATE_V2 = rustmod is not None
47
47
48 propertycache = util.propertycache
48 propertycache = util.propertycache
49 filecache = scmutil.filecache
49 filecache = scmutil.filecache
50 _rangemask = dirstatemap.rangemask
50 _rangemask = dirstatemap.rangemask
51
51
52 DirstateItem = dirstatemap.DirstateItem
52 DirstateItem = dirstatemap.DirstateItem
53
53
54
54
55 class repocache(filecache):
55 class repocache(filecache):
56 """filecache for files in .hg/"""
56 """filecache for files in .hg/"""
57
57
58 def join(self, obj, fname):
58 def join(self, obj, fname):
59 return obj._opener.join(fname)
59 return obj._opener.join(fname)
60
60
61
61
62 class rootcache(filecache):
62 class rootcache(filecache):
63 """filecache for files in the repository root"""
63 """filecache for files in the repository root"""
64
64
65 def join(self, obj, fname):
65 def join(self, obj, fname):
66 return obj._join(fname)
66 return obj._join(fname)
67
67
68
68
69 def requires_parents_change(func):
69 def requires_parents_change(func):
70 def wrap(self, *args, **kwargs):
70 def wrap(self, *args, **kwargs):
71 if not self.pendingparentchange():
71 if not self.pendingparentchange():
72 msg = 'calling `%s` outside of a parentchange context'
72 msg = 'calling `%s` outside of a parentchange context'
73 msg %= func.__name__
73 msg %= func.__name__
74 raise error.ProgrammingError(msg)
74 raise error.ProgrammingError(msg)
75 return func(self, *args, **kwargs)
75 return func(self, *args, **kwargs)
76
76
77 return wrap
77 return wrap
78
78
79
79
80 def requires_no_parents_change(func):
80 def requires_no_parents_change(func):
81 def wrap(self, *args, **kwargs):
81 def wrap(self, *args, **kwargs):
82 if self.pendingparentchange():
82 if self.pendingparentchange():
83 msg = 'calling `%s` inside of a parentchange context'
83 msg = 'calling `%s` inside of a parentchange context'
84 msg %= func.__name__
84 msg %= func.__name__
85 raise error.ProgrammingError(msg)
85 raise error.ProgrammingError(msg)
86 return func(self, *args, **kwargs)
86 return func(self, *args, **kwargs)
87
87
88 return wrap
88 return wrap
89
89
90
90
91 @interfaceutil.implementer(intdirstate.idirstate)
91 @interfaceutil.implementer(intdirstate.idirstate)
92 class dirstate:
92 class dirstate:
93 def __init__(
93 def __init__(
94 self,
94 self,
95 opener,
95 opener,
96 ui,
96 ui,
97 root,
97 root,
98 validate,
98 validate,
99 sparsematchfn,
99 sparsematchfn,
100 nodeconstants,
100 nodeconstants,
101 use_dirstate_v2,
101 use_dirstate_v2,
102 use_tracked_hint=False,
102 use_tracked_hint=False,
103 ):
103 ):
104 """Create a new dirstate object.
104 """Create a new dirstate object.
105
105
106 opener is an open()-like callable that can be used to open the
106 opener is an open()-like callable that can be used to open the
107 dirstate file; root is the root of the directory tracked by
107 dirstate file; root is the root of the directory tracked by
108 the dirstate.
108 the dirstate.
109 """
109 """
110 self._use_dirstate_v2 = use_dirstate_v2
110 self._use_dirstate_v2 = use_dirstate_v2
111 self._use_tracked_hint = use_tracked_hint
111 self._use_tracked_hint = use_tracked_hint
112 self._nodeconstants = nodeconstants
112 self._nodeconstants = nodeconstants
113 self._opener = opener
113 self._opener = opener
114 self._validate = validate
114 self._validate = validate
115 self._root = root
115 self._root = root
116 self._sparsematchfn = sparsematchfn
116 self._sparsematchfn = sparsematchfn
117 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
117 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
118 # UNC path pointing to root share (issue4557)
118 # UNC path pointing to root share (issue4557)
119 self._rootdir = pathutil.normasprefix(root)
119 self._rootdir = pathutil.normasprefix(root)
120 # True is any internal state may be different
120 # True is any internal state may be different
121 self._dirty = False
121 self._dirty = False
122 # True if the set of tracked file may be different
122 # True if the set of tracked file may be different
123 self._dirty_tracked_set = False
123 self._dirty_tracked_set = False
124 self._ui = ui
124 self._ui = ui
125 self._filecache = {}
125 self._filecache = {}
126 self._parentwriters = 0
126 self._parentwriters = 0
127 self._filename = b'dirstate'
127 self._filename = b'dirstate'
128 self._filename_th = b'dirstate-tracked-hint'
128 self._filename_th = b'dirstate-tracked-hint'
129 self._pendingfilename = b'%s.pending' % self._filename
129 self._pendingfilename = b'%s.pending' % self._filename
130 self._plchangecallbacks = {}
130 self._plchangecallbacks = {}
131 self._origpl = None
131 self._origpl = None
132 self._mapcls = dirstatemap.dirstatemap
132 self._mapcls = dirstatemap.dirstatemap
133 # Access and cache cwd early, so we don't access it for the first time
133 # Access and cache cwd early, so we don't access it for the first time
134 # after a working-copy update caused it to not exist (accessing it then
134 # after a working-copy update caused it to not exist (accessing it then
135 # raises an exception).
135 # raises an exception).
136 self._cwd
136 self._cwd
137
137
138 def prefetch_parents(self):
138 def prefetch_parents(self):
139 """make sure the parents are loaded
139 """make sure the parents are loaded
140
140
141 Used to avoid a race condition.
141 Used to avoid a race condition.
142 """
142 """
143 self._pl
143 self._pl
144
144
145 @contextlib.contextmanager
145 @contextlib.contextmanager
146 def parentchange(self):
146 def parentchange(self):
147 """Context manager for handling dirstate parents.
147 """Context manager for handling dirstate parents.
148
148
149 If an exception occurs in the scope of the context manager,
149 If an exception occurs in the scope of the context manager,
150 the incoherent dirstate won't be written when wlock is
150 the incoherent dirstate won't be written when wlock is
151 released.
151 released.
152 """
152 """
153 self._parentwriters += 1
153 self._parentwriters += 1
154 yield
154 yield
155 # Typically we want the "undo" step of a context manager in a
155 # Typically we want the "undo" step of a context manager in a
156 # finally block so it happens even when an exception
156 # finally block so it happens even when an exception
157 # occurs. In this case, however, we only want to decrement
157 # occurs. In this case, however, we only want to decrement
158 # parentwriters if the code in the with statement exits
158 # parentwriters if the code in the with statement exits
159 # normally, so we don't have a try/finally here on purpose.
159 # normally, so we don't have a try/finally here on purpose.
160 self._parentwriters -= 1
160 self._parentwriters -= 1
161
161
162 def pendingparentchange(self):
162 def pendingparentchange(self):
163 """Returns true if the dirstate is in the middle of a set of changes
163 """Returns true if the dirstate is in the middle of a set of changes
164 that modify the dirstate parent.
164 that modify the dirstate parent.
165 """
165 """
166 return self._parentwriters > 0
166 return self._parentwriters > 0
167
167
168 @propertycache
168 @propertycache
169 def _map(self):
169 def _map(self):
170 """Return the dirstate contents (see documentation for dirstatemap)."""
170 """Return the dirstate contents (see documentation for dirstatemap)."""
171 self._map = self._mapcls(
171 self._map = self._mapcls(
172 self._ui,
172 self._ui,
173 self._opener,
173 self._opener,
174 self._root,
174 self._root,
175 self._nodeconstants,
175 self._nodeconstants,
176 self._use_dirstate_v2,
176 self._use_dirstate_v2,
177 )
177 )
178 return self._map
178 return self._map
179
179
180 @property
180 @property
181 def _sparsematcher(self):
181 def _sparsematcher(self):
182 """The matcher for the sparse checkout.
182 """The matcher for the sparse checkout.
183
183
184 The working directory may not include every file from a manifest. The
184 The working directory may not include every file from a manifest. The
185 matcher obtained by this property will match a path if it is to be
185 matcher obtained by this property will match a path if it is to be
186 included in the working directory.
186 included in the working directory.
187 """
187 """
188 # TODO there is potential to cache this property. For now, the matcher
188 # TODO there is potential to cache this property. For now, the matcher
189 # is resolved on every access. (But the called function does use a
189 # is resolved on every access. (But the called function does use a
190 # cache to keep the lookup fast.)
190 # cache to keep the lookup fast.)
191 return self._sparsematchfn()
191 return self._sparsematchfn()
192
192
193 @repocache(b'branch')
193 @repocache(b'branch')
194 def _branch(self):
194 def _branch(self):
195 try:
195 try:
196 return self._opener.read(b"branch").strip() or b"default"
196 return self._opener.read(b"branch").strip() or b"default"
197 except FileNotFoundError:
197 except FileNotFoundError:
198 return b"default"
198 return b"default"
199
199
200 @property
200 @property
201 def _pl(self):
201 def _pl(self):
202 return self._map.parents()
202 return self._map.parents()
203
203
204 def hasdir(self, d):
204 def hasdir(self, d):
205 return self._map.hastrackeddir(d)
205 return self._map.hastrackeddir(d)
206
206
207 @rootcache(b'.hgignore')
207 @rootcache(b'.hgignore')
208 def _ignore(self):
208 def _ignore(self):
209 files = self._ignorefiles()
209 files = self._ignorefiles()
210 if not files:
210 if not files:
211 return matchmod.never()
211 return matchmod.never()
212
212
213 pats = [b'include:%s' % f for f in files]
213 pats = [b'include:%s' % f for f in files]
214 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
214 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
215
215
216 @propertycache
216 @propertycache
217 def _slash(self):
217 def _slash(self):
218 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
218 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
219
219
220 @propertycache
220 @propertycache
221 def _checklink(self):
221 def _checklink(self):
222 return util.checklink(self._root)
222 return util.checklink(self._root)
223
223
224 @propertycache
224 @propertycache
225 def _checkexec(self):
225 def _checkexec(self):
226 return bool(util.checkexec(self._root))
226 return bool(util.checkexec(self._root))
227
227
228 @propertycache
228 @propertycache
229 def _checkcase(self):
229 def _checkcase(self):
230 return not util.fscasesensitive(self._join(b'.hg'))
230 return not util.fscasesensitive(self._join(b'.hg'))
231
231
232 def _join(self, f):
232 def _join(self, f):
233 # much faster than os.path.join()
233 # much faster than os.path.join()
234 # it's safe because f is always a relative path
234 # it's safe because f is always a relative path
235 return self._rootdir + f
235 return self._rootdir + f
236
236
237 def flagfunc(self, buildfallback):
237 def flagfunc(self, buildfallback):
238 """build a callable that returns flags associated with a filename
238 """build a callable that returns flags associated with a filename
239
239
240 The information is extracted from three possible layers:
240 The information is extracted from three possible layers:
241 1. the file system if it supports the information
241 1. the file system if it supports the information
242 2. the "fallback" information stored in the dirstate if any
242 2. the "fallback" information stored in the dirstate if any
243 3. a more expensive mechanism inferring the flags from the parents.
243 3. a more expensive mechanism inferring the flags from the parents.
244 """
244 """
245
245
246 # small hack to cache the result of buildfallback()
246 # small hack to cache the result of buildfallback()
247 fallback_func = []
247 fallback_func = []
248
248
249 def get_flags(x):
249 def get_flags(x):
250 entry = None
250 entry = None
251 fallback_value = None
251 fallback_value = None
252 try:
252 try:
253 st = os.lstat(self._join(x))
253 st = os.lstat(self._join(x))
254 except OSError:
254 except OSError:
255 return b''
255 return b''
256
256
257 if self._checklink:
257 if self._checklink:
258 if util.statislink(st):
258 if util.statislink(st):
259 return b'l'
259 return b'l'
260 else:
260 else:
261 entry = self.get_entry(x)
261 entry = self.get_entry(x)
262 if entry.has_fallback_symlink:
262 if entry.has_fallback_symlink:
263 if entry.fallback_symlink:
263 if entry.fallback_symlink:
264 return b'l'
264 return b'l'
265 else:
265 else:
266 if not fallback_func:
266 if not fallback_func:
267 fallback_func.append(buildfallback())
267 fallback_func.append(buildfallback())
268 fallback_value = fallback_func[0](x)
268 fallback_value = fallback_func[0](x)
269 if b'l' in fallback_value:
269 if b'l' in fallback_value:
270 return b'l'
270 return b'l'
271
271
272 if self._checkexec:
272 if self._checkexec:
273 if util.statisexec(st):
273 if util.statisexec(st):
274 return b'x'
274 return b'x'
275 else:
275 else:
276 if entry is None:
276 if entry is None:
277 entry = self.get_entry(x)
277 entry = self.get_entry(x)
278 if entry.has_fallback_exec:
278 if entry.has_fallback_exec:
279 if entry.fallback_exec:
279 if entry.fallback_exec:
280 return b'x'
280 return b'x'
281 else:
281 else:
282 if fallback_value is None:
282 if fallback_value is None:
283 if not fallback_func:
283 if not fallback_func:
284 fallback_func.append(buildfallback())
284 fallback_func.append(buildfallback())
285 fallback_value = fallback_func[0](x)
285 fallback_value = fallback_func[0](x)
286 if b'x' in fallback_value:
286 if b'x' in fallback_value:
287 return b'x'
287 return b'x'
288 return b''
288 return b''
289
289
290 return get_flags
290 return get_flags
291
291
292 @propertycache
292 @propertycache
293 def _cwd(self):
293 def _cwd(self):
294 # internal config: ui.forcecwd
294 # internal config: ui.forcecwd
295 forcecwd = self._ui.config(b'ui', b'forcecwd')
295 forcecwd = self._ui.config(b'ui', b'forcecwd')
296 if forcecwd:
296 if forcecwd:
297 return forcecwd
297 return forcecwd
298 return encoding.getcwd()
298 return encoding.getcwd()
299
299
300 def getcwd(self):
300 def getcwd(self):
301 """Return the path from which a canonical path is calculated.
301 """Return the path from which a canonical path is calculated.
302
302
303 This path should be used to resolve file patterns or to convert
303 This path should be used to resolve file patterns or to convert
304 canonical paths back to file paths for display. It shouldn't be
304 canonical paths back to file paths for display. It shouldn't be
305 used to get real file paths. Use vfs functions instead.
305 used to get real file paths. Use vfs functions instead.
306 """
306 """
307 cwd = self._cwd
307 cwd = self._cwd
308 if cwd == self._root:
308 if cwd == self._root:
309 return b''
309 return b''
310 # self._root ends with a path separator if self._root is '/' or 'C:\'
310 # self._root ends with a path separator if self._root is '/' or 'C:\'
311 rootsep = self._root
311 rootsep = self._root
312 if not util.endswithsep(rootsep):
312 if not util.endswithsep(rootsep):
313 rootsep += pycompat.ossep
313 rootsep += pycompat.ossep
314 if cwd.startswith(rootsep):
314 if cwd.startswith(rootsep):
315 return cwd[len(rootsep) :]
315 return cwd[len(rootsep) :]
316 else:
316 else:
317 # we're outside the repo. return an absolute path.
317 # we're outside the repo. return an absolute path.
318 return cwd
318 return cwd
319
319
320 def pathto(self, f, cwd=None):
320 def pathto(self, f, cwd=None):
321 if cwd is None:
321 if cwd is None:
322 cwd = self.getcwd()
322 cwd = self.getcwd()
323 path = util.pathto(self._root, cwd, f)
323 path = util.pathto(self._root, cwd, f)
324 if self._slash:
324 if self._slash:
325 return util.pconvert(path)
325 return util.pconvert(path)
326 return path
326 return path
327
327
328 def get_entry(self, path):
328 def get_entry(self, path):
329 """return a DirstateItem for the associated path"""
329 """return a DirstateItem for the associated path"""
330 entry = self._map.get(path)
330 entry = self._map.get(path)
331 if entry is None:
331 if entry is None:
332 return DirstateItem()
332 return DirstateItem()
333 return entry
333 return entry
334
334
335 def __contains__(self, key):
335 def __contains__(self, key):
336 return key in self._map
336 return key in self._map
337
337
338 def __iter__(self):
338 def __iter__(self):
339 return iter(sorted(self._map))
339 return iter(sorted(self._map))
340
340
341 def items(self):
341 def items(self):
342 return self._map.items()
342 return self._map.items()
343
343
344 iteritems = items
344 iteritems = items
345
345
346 def parents(self):
346 def parents(self):
347 return [self._validate(p) for p in self._pl]
347 return [self._validate(p) for p in self._pl]
348
348
349 def p1(self):
349 def p1(self):
350 return self._validate(self._pl[0])
350 return self._validate(self._pl[0])
351
351
352 def p2(self):
352 def p2(self):
353 return self._validate(self._pl[1])
353 return self._validate(self._pl[1])
354
354
355 @property
355 @property
356 def in_merge(self):
356 def in_merge(self):
357 """True if a merge is in progress"""
357 """True if a merge is in progress"""
358 return self._pl[1] != self._nodeconstants.nullid
358 return self._pl[1] != self._nodeconstants.nullid
359
359
360 def branch(self):
360 def branch(self):
361 return encoding.tolocal(self._branch)
361 return encoding.tolocal(self._branch)
362
362
363 def setparents(self, p1, p2=None):
363 def setparents(self, p1, p2=None):
364 """Set dirstate parents to p1 and p2.
364 """Set dirstate parents to p1 and p2.
365
365
366 When moving from two parents to one, "merged" entries a
366 When moving from two parents to one, "merged" entries a
367 adjusted to normal and previous copy records discarded and
367 adjusted to normal and previous copy records discarded and
368 returned by the call.
368 returned by the call.
369
369
370 See localrepo.setparents()
370 See localrepo.setparents()
371 """
371 """
372 if p2 is None:
372 if p2 is None:
373 p2 = self._nodeconstants.nullid
373 p2 = self._nodeconstants.nullid
374 if self._parentwriters == 0:
374 if self._parentwriters == 0:
375 raise ValueError(
375 raise ValueError(
376 b"cannot set dirstate parent outside of "
376 b"cannot set dirstate parent outside of "
377 b"dirstate.parentchange context manager"
377 b"dirstate.parentchange context manager"
378 )
378 )
379
379
380 self._dirty = True
380 self._dirty = True
381 oldp2 = self._pl[1]
381 oldp2 = self._pl[1]
382 if self._origpl is None:
382 if self._origpl is None:
383 self._origpl = self._pl
383 self._origpl = self._pl
384 nullid = self._nodeconstants.nullid
384 nullid = self._nodeconstants.nullid
385 # True if we need to fold p2 related state back to a linear case
385 # True if we need to fold p2 related state back to a linear case
386 fold_p2 = oldp2 != nullid and p2 == nullid
386 fold_p2 = oldp2 != nullid and p2 == nullid
387 return self._map.setparents(p1, p2, fold_p2=fold_p2)
387 return self._map.setparents(p1, p2, fold_p2=fold_p2)
388
388
389 def setbranch(self, branch):
389 def setbranch(self, branch):
390 self.__class__._branch.set(self, encoding.fromlocal(branch))
390 self.__class__._branch.set(self, encoding.fromlocal(branch))
391 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
391 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
392 try:
392 try:
393 f.write(self._branch + b'\n')
393 f.write(self._branch + b'\n')
394 f.close()
394 f.close()
395
395
396 # make sure filecache has the correct stat info for _branch after
396 # make sure filecache has the correct stat info for _branch after
397 # replacing the underlying file
397 # replacing the underlying file
398 ce = self._filecache[b'_branch']
398 ce = self._filecache[b'_branch']
399 if ce:
399 if ce:
400 ce.refresh()
400 ce.refresh()
401 except: # re-raises
401 except: # re-raises
402 f.discard()
402 f.discard()
403 raise
403 raise
404
404
405 def invalidate(self):
405 def invalidate(self):
406 """Causes the next access to reread the dirstate.
406 """Causes the next access to reread the dirstate.
407
407
408 This is different from localrepo.invalidatedirstate() because it always
408 This is different from localrepo.invalidatedirstate() because it always
409 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
409 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
410 check whether the dirstate has changed before rereading it."""
410 check whether the dirstate has changed before rereading it."""
411
411
412 for a in ("_map", "_branch", "_ignore"):
412 for a in ("_map", "_branch", "_ignore"):
413 if a in self.__dict__:
413 if a in self.__dict__:
414 delattr(self, a)
414 delattr(self, a)
415 self._dirty = False
415 self._dirty = False
416 self._dirty_tracked_set = False
416 self._dirty_tracked_set = False
417 self._parentwriters = 0
417 self._parentwriters = 0
418 self._origpl = None
418 self._origpl = None
419
419
420 def copy(self, source, dest):
420 def copy(self, source, dest):
421 """Mark dest as a copy of source. Unmark dest if source is None."""
421 """Mark dest as a copy of source. Unmark dest if source is None."""
422 if source == dest:
422 if source == dest:
423 return
423 return
424 self._dirty = True
424 self._dirty = True
425 if source is not None:
425 if source is not None:
426 self._map.copymap[dest] = source
426 self._map.copymap[dest] = source
427 else:
427 else:
428 self._map.copymap.pop(dest, None)
428 self._map.copymap.pop(dest, None)
429
429
430 def copied(self, file):
430 def copied(self, file):
431 return self._map.copymap.get(file, None)
431 return self._map.copymap.get(file, None)
432
432
433 def copies(self):
433 def copies(self):
434 return self._map.copymap
434 return self._map.copymap
435
435
436 @requires_no_parents_change
436 @requires_no_parents_change
437 def set_tracked(self, filename, reset_copy=False):
437 def set_tracked(self, filename, reset_copy=False):
438 """a "public" method for generic code to mark a file as tracked
438 """a "public" method for generic code to mark a file as tracked
439
439
440 This function is to be called outside of "update/merge" case. For
440 This function is to be called outside of "update/merge" case. For
441 example by a command like `hg add X`.
441 example by a command like `hg add X`.
442
442
443 if reset_copy is set, any existing copy information will be dropped.
443 if reset_copy is set, any existing copy information will be dropped.
444
444
445 return True the file was previously untracked, False otherwise.
445 return True the file was previously untracked, False otherwise.
446 """
446 """
447 self._dirty = True
447 self._dirty = True
448 entry = self._map.get(filename)
448 entry = self._map.get(filename)
449 if entry is None or not entry.tracked:
449 if entry is None or not entry.tracked:
450 self._check_new_tracked_filename(filename)
450 self._check_new_tracked_filename(filename)
451 pre_tracked = self._map.set_tracked(filename)
451 pre_tracked = self._map.set_tracked(filename)
452 if reset_copy:
452 if reset_copy:
453 self._map.copymap.pop(filename, None)
453 self._map.copymap.pop(filename, None)
454 if pre_tracked:
454 if pre_tracked:
455 self._dirty_tracked_set = True
455 self._dirty_tracked_set = True
456 return pre_tracked
456 return pre_tracked
457
457
458 @requires_no_parents_change
458 @requires_no_parents_change
459 def set_untracked(self, filename):
459 def set_untracked(self, filename):
460 """a "public" method for generic code to mark a file as untracked
460 """a "public" method for generic code to mark a file as untracked
461
461
462 This function is to be called outside of "update/merge" case. For
462 This function is to be called outside of "update/merge" case. For
463 example by a command like `hg remove X`.
463 example by a command like `hg remove X`.
464
464
465 return True the file was previously tracked, False otherwise.
465 return True the file was previously tracked, False otherwise.
466 """
466 """
467 ret = self._map.set_untracked(filename)
467 ret = self._map.set_untracked(filename)
468 if ret:
468 if ret:
469 self._dirty = True
469 self._dirty = True
470 self._dirty_tracked_set = True
470 self._dirty_tracked_set = True
471 return ret
471 return ret
472
472
473 @requires_no_parents_change
473 @requires_no_parents_change
474 def set_clean(self, filename, parentfiledata):
474 def set_clean(self, filename, parentfiledata):
475 """record that the current state of the file on disk is known to be clean"""
475 """record that the current state of the file on disk is known to be clean"""
476 self._dirty = True
476 self._dirty = True
477 if not self._map[filename].tracked:
477 if not self._map[filename].tracked:
478 self._check_new_tracked_filename(filename)
478 self._check_new_tracked_filename(filename)
479 (mode, size, mtime) = parentfiledata
479 (mode, size, mtime) = parentfiledata
480 self._map.set_clean(filename, mode, size, mtime)
480 self._map.set_clean(filename, mode, size, mtime)
481
481
482 @requires_no_parents_change
482 @requires_no_parents_change
483 def set_possibly_dirty(self, filename):
483 def set_possibly_dirty(self, filename):
484 """record that the current state of the file on disk is unknown"""
484 """record that the current state of the file on disk is unknown"""
485 self._dirty = True
485 self._dirty = True
486 self._map.set_possibly_dirty(filename)
486 self._map.set_possibly_dirty(filename)
487
487
488 @requires_parents_change
488 @requires_parents_change
489 def update_file_p1(
489 def update_file_p1(
490 self,
490 self,
491 filename,
491 filename,
492 p1_tracked,
492 p1_tracked,
493 ):
493 ):
494 """Set a file as tracked in the parent (or not)
494 """Set a file as tracked in the parent (or not)
495
495
496 This is to be called when adjust the dirstate to a new parent after an history
496 This is to be called when adjust the dirstate to a new parent after an history
497 rewriting operation.
497 rewriting operation.
498
498
499 It should not be called during a merge (p2 != nullid) and only within
499 It should not be called during a merge (p2 != nullid) and only within
500 a `with dirstate.parentchange():` context.
500 a `with dirstate.parentchange():` context.
501 """
501 """
502 if self.in_merge:
502 if self.in_merge:
503 msg = b'update_file_reference should not be called when merging'
503 msg = b'update_file_reference should not be called when merging'
504 raise error.ProgrammingError(msg)
504 raise error.ProgrammingError(msg)
505 entry = self._map.get(filename)
505 entry = self._map.get(filename)
506 if entry is None:
506 if entry is None:
507 wc_tracked = False
507 wc_tracked = False
508 else:
508 else:
509 wc_tracked = entry.tracked
509 wc_tracked = entry.tracked
510 if not (p1_tracked or wc_tracked):
510 if not (p1_tracked or wc_tracked):
511 # the file is no longer relevant to anyone
511 # the file is no longer relevant to anyone
512 if self._map.get(filename) is not None:
512 if self._map.get(filename) is not None:
513 self._map.reset_state(filename)
513 self._map.reset_state(filename)
514 self._dirty = True
514 self._dirty = True
515 elif (not p1_tracked) and wc_tracked:
515 elif (not p1_tracked) and wc_tracked:
516 if entry is not None and entry.added:
516 if entry is not None and entry.added:
517 return # avoid dropping copy information (maybe?)
517 return # avoid dropping copy information (maybe?)
518
518
519 self._map.reset_state(
519 self._map.reset_state(
520 filename,
520 filename,
521 wc_tracked,
521 wc_tracked,
522 p1_tracked,
522 p1_tracked,
523 # the underlying reference might have changed, we will have to
523 # the underlying reference might have changed, we will have to
524 # check it.
524 # check it.
525 has_meaningful_mtime=False,
525 has_meaningful_mtime=False,
526 )
526 )
527
527
528 @requires_parents_change
528 @requires_parents_change
529 def update_file(
529 def update_file(
530 self,
530 self,
531 filename,
531 filename,
532 wc_tracked,
532 wc_tracked,
533 p1_tracked,
533 p1_tracked,
534 p2_info=False,
534 p2_info=False,
535 possibly_dirty=False,
535 possibly_dirty=False,
536 parentfiledata=None,
536 parentfiledata=None,
537 ):
537 ):
538 """update the information about a file in the dirstate
538 """update the information about a file in the dirstate
539
539
540 This is to be called when the direstates parent changes to keep track
540 This is to be called when the direstates parent changes to keep track
541 of what is the file situation in regards to the working copy and its parent.
541 of what is the file situation in regards to the working copy and its parent.
542
542
543 This function must be called within a `dirstate.parentchange` context.
543 This function must be called within a `dirstate.parentchange` context.
544
544
545 note: the API is at an early stage and we might need to adjust it
545 note: the API is at an early stage and we might need to adjust it
546 depending of what information ends up being relevant and useful to
546 depending of what information ends up being relevant and useful to
547 other processing.
547 other processing.
548 """
548 """
549
549
550 # note: I do not think we need to double check name clash here since we
550 # note: I do not think we need to double check name clash here since we
551 # are in a update/merge case that should already have taken care of
551 # are in a update/merge case that should already have taken care of
552 # this. The test agrees
552 # this. The test agrees
553
553
554 self._dirty = True
554 self._dirty = True
555 old_entry = self._map.get(filename)
555 old_entry = self._map.get(filename)
556 if old_entry is None:
556 if old_entry is None:
557 prev_tracked = False
557 prev_tracked = False
558 else:
558 else:
559 prev_tracked = old_entry.tracked
559 prev_tracked = old_entry.tracked
560 if prev_tracked != wc_tracked:
560 if prev_tracked != wc_tracked:
561 self._dirty_tracked_set = True
561 self._dirty_tracked_set = True
562
562
563 self._map.reset_state(
563 self._map.reset_state(
564 filename,
564 filename,
565 wc_tracked,
565 wc_tracked,
566 p1_tracked,
566 p1_tracked,
567 p2_info=p2_info,
567 p2_info=p2_info,
568 has_meaningful_mtime=not possibly_dirty,
568 has_meaningful_mtime=not possibly_dirty,
569 parentfiledata=parentfiledata,
569 parentfiledata=parentfiledata,
570 )
570 )
571
571
572 def _check_new_tracked_filename(self, filename):
572 def _check_new_tracked_filename(self, filename):
573 scmutil.checkfilename(filename)
573 scmutil.checkfilename(filename)
574 if self._map.hastrackeddir(filename):
574 if self._map.hastrackeddir(filename):
575 msg = _(b'directory %r already in dirstate')
575 msg = _(b'directory %r already in dirstate')
576 msg %= pycompat.bytestr(filename)
576 msg %= pycompat.bytestr(filename)
577 raise error.Abort(msg)
577 raise error.Abort(msg)
578 # shadows
578 # shadows
579 for d in pathutil.finddirs(filename):
579 for d in pathutil.finddirs(filename):
580 if self._map.hastrackeddir(d):
580 if self._map.hastrackeddir(d):
581 break
581 break
582 entry = self._map.get(d)
582 entry = self._map.get(d)
583 if entry is not None and not entry.removed:
583 if entry is not None and not entry.removed:
584 msg = _(b'file %r in dirstate clashes with %r')
584 msg = _(b'file %r in dirstate clashes with %r')
585 msg %= (pycompat.bytestr(d), pycompat.bytestr(filename))
585 msg %= (pycompat.bytestr(d), pycompat.bytestr(filename))
586 raise error.Abort(msg)
586 raise error.Abort(msg)
587
587
588 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
588 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
589 if exists is None:
589 if exists is None:
590 exists = os.path.lexists(os.path.join(self._root, path))
590 exists = os.path.lexists(os.path.join(self._root, path))
591 if not exists:
591 if not exists:
592 # Maybe a path component exists
592 # Maybe a path component exists
593 if not ignoremissing and b'/' in path:
593 if not ignoremissing and b'/' in path:
594 d, f = path.rsplit(b'/', 1)
594 d, f = path.rsplit(b'/', 1)
595 d = self._normalize(d, False, ignoremissing, None)
595 d = self._normalize(d, False, ignoremissing, None)
596 folded = d + b"/" + f
596 folded = d + b"/" + f
597 else:
597 else:
598 # No path components, preserve original case
598 # No path components, preserve original case
599 folded = path
599 folded = path
600 else:
600 else:
601 # recursively normalize leading directory components
601 # recursively normalize leading directory components
602 # against dirstate
602 # against dirstate
603 if b'/' in normed:
603 if b'/' in normed:
604 d, f = normed.rsplit(b'/', 1)
604 d, f = normed.rsplit(b'/', 1)
605 d = self._normalize(d, False, ignoremissing, True)
605 d = self._normalize(d, False, ignoremissing, True)
606 r = self._root + b"/" + d
606 r = self._root + b"/" + d
607 folded = d + b"/" + util.fspath(f, r)
607 folded = d + b"/" + util.fspath(f, r)
608 else:
608 else:
609 folded = util.fspath(normed, self._root)
609 folded = util.fspath(normed, self._root)
610 storemap[normed] = folded
610 storemap[normed] = folded
611
611
612 return folded
612 return folded
613
613
614 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
614 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
615 normed = util.normcase(path)
615 normed = util.normcase(path)
616 folded = self._map.filefoldmap.get(normed, None)
616 folded = self._map.filefoldmap.get(normed, None)
617 if folded is None:
617 if folded is None:
618 if isknown:
618 if isknown:
619 folded = path
619 folded = path
620 else:
620 else:
621 folded = self._discoverpath(
621 folded = self._discoverpath(
622 path, normed, ignoremissing, exists, self._map.filefoldmap
622 path, normed, ignoremissing, exists, self._map.filefoldmap
623 )
623 )
624 return folded
624 return folded
625
625
626 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
626 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
627 normed = util.normcase(path)
627 normed = util.normcase(path)
628 folded = self._map.filefoldmap.get(normed, None)
628 folded = self._map.filefoldmap.get(normed, None)
629 if folded is None:
629 if folded is None:
630 folded = self._map.dirfoldmap.get(normed, None)
630 folded = self._map.dirfoldmap.get(normed, None)
631 if folded is None:
631 if folded is None:
632 if isknown:
632 if isknown:
633 folded = path
633 folded = path
634 else:
634 else:
635 # store discovered result in dirfoldmap so that future
635 # store discovered result in dirfoldmap so that future
636 # normalizefile calls don't start matching directories
636 # normalizefile calls don't start matching directories
637 folded = self._discoverpath(
637 folded = self._discoverpath(
638 path, normed, ignoremissing, exists, self._map.dirfoldmap
638 path, normed, ignoremissing, exists, self._map.dirfoldmap
639 )
639 )
640 return folded
640 return folded
641
641
642 def normalize(self, path, isknown=False, ignoremissing=False):
642 def normalize(self, path, isknown=False, ignoremissing=False):
643 """
643 """
644 normalize the case of a pathname when on a casefolding filesystem
644 normalize the case of a pathname when on a casefolding filesystem
645
645
646 isknown specifies whether the filename came from walking the
646 isknown specifies whether the filename came from walking the
647 disk, to avoid extra filesystem access.
647 disk, to avoid extra filesystem access.
648
648
649 If ignoremissing is True, missing path are returned
649 If ignoremissing is True, missing path are returned
650 unchanged. Otherwise, we try harder to normalize possibly
650 unchanged. Otherwise, we try harder to normalize possibly
651 existing path components.
651 existing path components.
652
652
653 The normalized case is determined based on the following precedence:
653 The normalized case is determined based on the following precedence:
654
654
655 - version of name already stored in the dirstate
655 - version of name already stored in the dirstate
656 - version of name stored on disk
656 - version of name stored on disk
657 - version provided via command arguments
657 - version provided via command arguments
658 """
658 """
659
659
660 if self._checkcase:
660 if self._checkcase:
661 return self._normalize(path, isknown, ignoremissing)
661 return self._normalize(path, isknown, ignoremissing)
662 return path
662 return path
663
663
664 def clear(self):
664 def clear(self):
665 self._map.clear()
665 self._map.clear()
666 self._dirty = True
666 self._dirty = True
667
667
668 def rebuild(self, parent, allfiles, changedfiles=None):
668 def rebuild(self, parent, allfiles, changedfiles=None):
669 if changedfiles is None:
669 if changedfiles is None:
670 # Rebuild entire dirstate
670 # Rebuild entire dirstate
671 to_lookup = allfiles
671 to_lookup = allfiles
672 to_drop = []
672 to_drop = []
673 self.clear()
673 self.clear()
674 elif len(changedfiles) < 10:
674 elif len(changedfiles) < 10:
675 # Avoid turning allfiles into a set, which can be expensive if it's
675 # Avoid turning allfiles into a set, which can be expensive if it's
676 # large.
676 # large.
677 to_lookup = []
677 to_lookup = []
678 to_drop = []
678 to_drop = []
679 for f in changedfiles:
679 for f in changedfiles:
680 if f in allfiles:
680 if f in allfiles:
681 to_lookup.append(f)
681 to_lookup.append(f)
682 else:
682 else:
683 to_drop.append(f)
683 to_drop.append(f)
684 else:
684 else:
685 changedfilesset = set(changedfiles)
685 changedfilesset = set(changedfiles)
686 to_lookup = changedfilesset & set(allfiles)
686 to_lookup = changedfilesset & set(allfiles)
687 to_drop = changedfilesset - to_lookup
687 to_drop = changedfilesset - to_lookup
688
688
689 if self._origpl is None:
689 if self._origpl is None:
690 self._origpl = self._pl
690 self._origpl = self._pl
691 self._map.setparents(parent, self._nodeconstants.nullid)
691 self._map.setparents(parent, self._nodeconstants.nullid)
692
692
693 for f in to_lookup:
693 for f in to_lookup:
694
694
695 if self.in_merge:
695 if self.in_merge:
696 self.set_tracked(f)
696 self.set_tracked(f)
697 else:
697 else:
698 self._map.reset_state(
698 self._map.reset_state(
699 f,
699 f,
700 wc_tracked=True,
700 wc_tracked=True,
701 p1_tracked=True,
701 p1_tracked=True,
702 )
702 )
703 for f in to_drop:
703 for f in to_drop:
704 self._map.reset_state(f)
704 self._map.reset_state(f)
705
705
706 self._dirty = True
706 self._dirty = True
707
707
708 def identity(self):
708 def identity(self):
709 """Return identity of dirstate itself to detect changing in storage
709 """Return identity of dirstate itself to detect changing in storage
710
710
711 If identity of previous dirstate is equal to this, writing
711 If identity of previous dirstate is equal to this, writing
712 changes based on the former dirstate out can keep consistency.
712 changes based on the former dirstate out can keep consistency.
713 """
713 """
714 return self._map.identity
714 return self._map.identity
715
715
716 def write(self, tr):
716 def write(self, tr):
717 if not self._dirty:
717 if not self._dirty:
718 return
718 return
719
719
720 write_key = self._use_tracked_hint and self._dirty_tracked_set
720 write_key = self._use_tracked_hint and self._dirty_tracked_set
721 if tr:
721 if tr:
722 # delay writing in-memory changes out
722 # delay writing in-memory changes out
723 tr.addfilegenerator(
723 tr.addfilegenerator(
724 b'dirstate-1-main',
724 b'dirstate-1-main',
725 (self._filename,),
725 (self._filename,),
726 lambda f: self._writedirstate(tr, f),
726 lambda f: self._writedirstate(tr, f),
727 location=b'plain',
727 location=b'plain',
728 post_finalize=True,
728 post_finalize=True,
729 )
729 )
730 if write_key:
730 if write_key:
731 tr.addfilegenerator(
731 tr.addfilegenerator(
732 b'dirstate-2-key-post',
732 b'dirstate-2-key-post',
733 (self._filename_th,),
733 (self._filename_th,),
734 lambda f: self._write_tracked_hint(tr, f),
734 lambda f: self._write_tracked_hint(tr, f),
735 location=b'plain',
735 location=b'plain',
736 post_finalize=True,
736 post_finalize=True,
737 )
737 )
738 return
738 return
739
739
740 file = lambda f: self._opener(f, b"w", atomictemp=True, checkambig=True)
740 file = lambda f: self._opener(f, b"w", atomictemp=True, checkambig=True)
741 with file(self._filename) as f:
741 with file(self._filename) as f:
742 self._writedirstate(tr, f)
742 self._writedirstate(tr, f)
743 if write_key:
743 if write_key:
744 # we update the key-file after writing to make sure reader have a
744 # we update the key-file after writing to make sure reader have a
745 # key that match the newly written content
745 # key that match the newly written content
746 with file(self._filename_th) as f:
746 with file(self._filename_th) as f:
747 self._write_tracked_hint(tr, f)
747 self._write_tracked_hint(tr, f)
748
748
749 def delete_tracked_hint(self):
749 def delete_tracked_hint(self):
750 """remove the tracked_hint file
750 """remove the tracked_hint file
751
751
752 To be used by format downgrades operation"""
752 To be used by format downgrades operation"""
753 self._opener.unlink(self._filename_th)
753 self._opener.unlink(self._filename_th)
754 self._use_tracked_hint = False
754 self._use_tracked_hint = False
755
755
756 def addparentchangecallback(self, category, callback):
756 def addparentchangecallback(self, category, callback):
757 """add a callback to be called when the wd parents are changed
757 """add a callback to be called when the wd parents are changed
758
758
759 Callback will be called with the following arguments:
759 Callback will be called with the following arguments:
760 dirstate, (oldp1, oldp2), (newp1, newp2)
760 dirstate, (oldp1, oldp2), (newp1, newp2)
761
761
762 Category is a unique identifier to allow overwriting an old callback
762 Category is a unique identifier to allow overwriting an old callback
763 with a newer callback.
763 with a newer callback.
764 """
764 """
765 self._plchangecallbacks[category] = callback
765 self._plchangecallbacks[category] = callback
766
766
767 def _writedirstate(self, tr, st):
767 def _writedirstate(self, tr, st):
768 # notify callbacks about parents change
768 # notify callbacks about parents change
769 if self._origpl is not None and self._origpl != self._pl:
769 if self._origpl is not None and self._origpl != self._pl:
770 for c, callback in sorted(self._plchangecallbacks.items()):
770 for c, callback in sorted(self._plchangecallbacks.items()):
771 callback(self, self._origpl, self._pl)
771 callback(self, self._origpl, self._pl)
772 self._origpl = None
772 self._origpl = None
773 self._map.write(tr, st)
773 self._map.write(tr, st)
774 self._dirty = False
774 self._dirty = False
775 self._dirty_tracked_set = False
775 self._dirty_tracked_set = False
776
776
777 def _write_tracked_hint(self, tr, f):
777 def _write_tracked_hint(self, tr, f):
778 key = node.hex(uuid.uuid4().bytes)
778 key = node.hex(uuid.uuid4().bytes)
779 f.write(b"1\n%s\n" % key) # 1 is the format version
779 f.write(b"1\n%s\n" % key) # 1 is the format version
780
780
781 def _dirignore(self, f):
781 def _dirignore(self, f):
782 if self._ignore(f):
782 if self._ignore(f):
783 return True
783 return True
784 for p in pathutil.finddirs(f):
784 for p in pathutil.finddirs(f):
785 if self._ignore(p):
785 if self._ignore(p):
786 return True
786 return True
787 return False
787 return False
788
788
789 def _ignorefiles(self):
789 def _ignorefiles(self):
790 files = []
790 files = []
791 if os.path.exists(self._join(b'.hgignore')):
791 if os.path.exists(self._join(b'.hgignore')):
792 files.append(self._join(b'.hgignore'))
792 files.append(self._join(b'.hgignore'))
793 for name, path in self._ui.configitems(b"ui"):
793 for name, path in self._ui.configitems(b"ui"):
794 if name == b'ignore' or name.startswith(b'ignore.'):
794 if name == b'ignore' or name.startswith(b'ignore.'):
795 # we need to use os.path.join here rather than self._join
795 # we need to use os.path.join here rather than self._join
796 # because path is arbitrary and user-specified
796 # because path is arbitrary and user-specified
797 files.append(os.path.join(self._rootdir, util.expandpath(path)))
797 files.append(os.path.join(self._rootdir, util.expandpath(path)))
798 return files
798 return files
799
799
800 def _ignorefileandline(self, f):
800 def _ignorefileandline(self, f):
801 files = collections.deque(self._ignorefiles())
801 files = collections.deque(self._ignorefiles())
802 visited = set()
802 visited = set()
803 while files:
803 while files:
804 i = files.popleft()
804 i = files.popleft()
805 patterns = matchmod.readpatternfile(
805 patterns = matchmod.readpatternfile(
806 i, self._ui.warn, sourceinfo=True
806 i, self._ui.warn, sourceinfo=True
807 )
807 )
808 for pattern, lineno, line in patterns:
808 for pattern, lineno, line in patterns:
809 kind, p = matchmod._patsplit(pattern, b'glob')
809 kind, p = matchmod._patsplit(pattern, b'glob')
810 if kind == b"subinclude":
810 if kind == b"subinclude":
811 if p not in visited:
811 if p not in visited:
812 files.append(p)
812 files.append(p)
813 continue
813 continue
814 m = matchmod.match(
814 m = matchmod.match(
815 self._root, b'', [], [pattern], warn=self._ui.warn
815 self._root, b'', [], [pattern], warn=self._ui.warn
816 )
816 )
817 if m(f):
817 if m(f):
818 return (i, lineno, line)
818 return (i, lineno, line)
819 visited.add(i)
819 visited.add(i)
820 return (None, -1, b"")
820 return (None, -1, b"")
821
821
822 def _walkexplicit(self, match, subrepos):
822 def _walkexplicit(self, match, subrepos):
823 """Get stat data about the files explicitly specified by match.
823 """Get stat data about the files explicitly specified by match.
824
824
825 Return a triple (results, dirsfound, dirsnotfound).
825 Return a triple (results, dirsfound, dirsnotfound).
826 - results is a mapping from filename to stat result. It also contains
826 - results is a mapping from filename to stat result. It also contains
827 listings mapping subrepos and .hg to None.
827 listings mapping subrepos and .hg to None.
828 - dirsfound is a list of files found to be directories.
828 - dirsfound is a list of files found to be directories.
829 - dirsnotfound is a list of files that the dirstate thinks are
829 - dirsnotfound is a list of files that the dirstate thinks are
830 directories and that were not found."""
830 directories and that were not found."""
831
831
832 def badtype(mode):
832 def badtype(mode):
833 kind = _(b'unknown')
833 kind = _(b'unknown')
834 if stat.S_ISCHR(mode):
834 if stat.S_ISCHR(mode):
835 kind = _(b'character device')
835 kind = _(b'character device')
836 elif stat.S_ISBLK(mode):
836 elif stat.S_ISBLK(mode):
837 kind = _(b'block device')
837 kind = _(b'block device')
838 elif stat.S_ISFIFO(mode):
838 elif stat.S_ISFIFO(mode):
839 kind = _(b'fifo')
839 kind = _(b'fifo')
840 elif stat.S_ISSOCK(mode):
840 elif stat.S_ISSOCK(mode):
841 kind = _(b'socket')
841 kind = _(b'socket')
842 elif stat.S_ISDIR(mode):
842 elif stat.S_ISDIR(mode):
843 kind = _(b'directory')
843 kind = _(b'directory')
844 return _(b'unsupported file type (type is %s)') % kind
844 return _(b'unsupported file type (type is %s)') % kind
845
845
846 badfn = match.bad
846 badfn = match.bad
847 dmap = self._map
847 dmap = self._map
848 lstat = os.lstat
848 lstat = os.lstat
849 getkind = stat.S_IFMT
849 getkind = stat.S_IFMT
850 dirkind = stat.S_IFDIR
850 dirkind = stat.S_IFDIR
851 regkind = stat.S_IFREG
851 regkind = stat.S_IFREG
852 lnkkind = stat.S_IFLNK
852 lnkkind = stat.S_IFLNK
853 join = self._join
853 join = self._join
854 dirsfound = []
854 dirsfound = []
855 foundadd = dirsfound.append
855 foundadd = dirsfound.append
856 dirsnotfound = []
856 dirsnotfound = []
857 notfoundadd = dirsnotfound.append
857 notfoundadd = dirsnotfound.append
858
858
859 if not match.isexact() and self._checkcase:
859 if not match.isexact() and self._checkcase:
860 normalize = self._normalize
860 normalize = self._normalize
861 else:
861 else:
862 normalize = None
862 normalize = None
863
863
864 files = sorted(match.files())
864 files = sorted(match.files())
865 subrepos.sort()
865 subrepos.sort()
866 i, j = 0, 0
866 i, j = 0, 0
867 while i < len(files) and j < len(subrepos):
867 while i < len(files) and j < len(subrepos):
868 subpath = subrepos[j] + b"/"
868 subpath = subrepos[j] + b"/"
869 if files[i] < subpath:
869 if files[i] < subpath:
870 i += 1
870 i += 1
871 continue
871 continue
872 while i < len(files) and files[i].startswith(subpath):
872 while i < len(files) and files[i].startswith(subpath):
873 del files[i]
873 del files[i]
874 j += 1
874 j += 1
875
875
876 if not files or b'' in files:
876 if not files or b'' in files:
877 files = [b'']
877 files = [b'']
878 # constructing the foldmap is expensive, so don't do it for the
878 # constructing the foldmap is expensive, so don't do it for the
879 # common case where files is ['']
879 # common case where files is ['']
880 normalize = None
880 normalize = None
881 results = dict.fromkeys(subrepos)
881 results = dict.fromkeys(subrepos)
882 results[b'.hg'] = None
882 results[b'.hg'] = None
883
883
884 for ff in files:
884 for ff in files:
885 if normalize:
885 if normalize:
886 nf = normalize(ff, False, True)
886 nf = normalize(ff, False, True)
887 else:
887 else:
888 nf = ff
888 nf = ff
889 if nf in results:
889 if nf in results:
890 continue
890 continue
891
891
892 try:
892 try:
893 st = lstat(join(nf))
893 st = lstat(join(nf))
894 kind = getkind(st.st_mode)
894 kind = getkind(st.st_mode)
895 if kind == dirkind:
895 if kind == dirkind:
896 if nf in dmap:
896 if nf in dmap:
897 # file replaced by dir on disk but still in dirstate
897 # file replaced by dir on disk but still in dirstate
898 results[nf] = None
898 results[nf] = None
899 foundadd((nf, ff))
899 foundadd((nf, ff))
900 elif kind == regkind or kind == lnkkind:
900 elif kind == regkind or kind == lnkkind:
901 results[nf] = st
901 results[nf] = st
902 else:
902 else:
903 badfn(ff, badtype(kind))
903 badfn(ff, badtype(kind))
904 if nf in dmap:
904 if nf in dmap:
905 results[nf] = None
905 results[nf] = None
906 except OSError as inst: # nf not found on disk - it is dirstate only
906 except OSError as inst: # nf not found on disk - it is dirstate only
907 if nf in dmap: # does it exactly match a missing file?
907 if nf in dmap: # does it exactly match a missing file?
908 results[nf] = None
908 results[nf] = None
909 else: # does it match a missing directory?
909 else: # does it match a missing directory?
910 if self._map.hasdir(nf):
910 if self._map.hasdir(nf):
911 notfoundadd(nf)
911 notfoundadd(nf)
912 else:
912 else:
913 badfn(ff, encoding.strtolocal(inst.strerror))
913 badfn(ff, encoding.strtolocal(inst.strerror))
914
914
915 # match.files() may contain explicitly-specified paths that shouldn't
915 # match.files() may contain explicitly-specified paths that shouldn't
916 # be taken; drop them from the list of files found. dirsfound/notfound
916 # be taken; drop them from the list of files found. dirsfound/notfound
917 # aren't filtered here because they will be tested later.
917 # aren't filtered here because they will be tested later.
918 if match.anypats():
918 if match.anypats():
919 for f in list(results):
919 for f in list(results):
920 if f == b'.hg' or f in subrepos:
920 if f == b'.hg' or f in subrepos:
921 # keep sentinel to disable further out-of-repo walks
921 # keep sentinel to disable further out-of-repo walks
922 continue
922 continue
923 if not match(f):
923 if not match(f):
924 del results[f]
924 del results[f]
925
925
926 # Case insensitive filesystems cannot rely on lstat() failing to detect
926 # Case insensitive filesystems cannot rely on lstat() failing to detect
927 # a case-only rename. Prune the stat object for any file that does not
927 # a case-only rename. Prune the stat object for any file that does not
928 # match the case in the filesystem, if there are multiple files that
928 # match the case in the filesystem, if there are multiple files that
929 # normalize to the same path.
929 # normalize to the same path.
930 if match.isexact() and self._checkcase:
930 if match.isexact() and self._checkcase:
931 normed = {}
931 normed = {}
932
932
933 for f, st in results.items():
933 for f, st in results.items():
934 if st is None:
934 if st is None:
935 continue
935 continue
936
936
937 nc = util.normcase(f)
937 nc = util.normcase(f)
938 paths = normed.get(nc)
938 paths = normed.get(nc)
939
939
940 if paths is None:
940 if paths is None:
941 paths = set()
941 paths = set()
942 normed[nc] = paths
942 normed[nc] = paths
943
943
944 paths.add(f)
944 paths.add(f)
945
945
946 for norm, paths in normed.items():
946 for norm, paths in normed.items():
947 if len(paths) > 1:
947 if len(paths) > 1:
948 for path in paths:
948 for path in paths:
949 folded = self._discoverpath(
949 folded = self._discoverpath(
950 path, norm, True, None, self._map.dirfoldmap
950 path, norm, True, None, self._map.dirfoldmap
951 )
951 )
952 if path != folded:
952 if path != folded:
953 results[path] = None
953 results[path] = None
954
954
955 return results, dirsfound, dirsnotfound
955 return results, dirsfound, dirsnotfound
956
956
957 def walk(self, match, subrepos, unknown, ignored, full=True):
957 def walk(self, match, subrepos, unknown, ignored, full=True):
958 """
958 """
959 Walk recursively through the directory tree, finding all files
959 Walk recursively through the directory tree, finding all files
960 matched by match.
960 matched by match.
961
961
962 If full is False, maybe skip some known-clean files.
962 If full is False, maybe skip some known-clean files.
963
963
964 Return a dict mapping filename to stat-like object (either
964 Return a dict mapping filename to stat-like object (either
965 mercurial.osutil.stat instance or return value of os.stat()).
965 mercurial.osutil.stat instance or return value of os.stat()).
966
966
967 """
967 """
968 # full is a flag that extensions that hook into walk can use -- this
968 # full is a flag that extensions that hook into walk can use -- this
969 # implementation doesn't use it at all. This satisfies the contract
969 # implementation doesn't use it at all. This satisfies the contract
970 # because we only guarantee a "maybe".
970 # because we only guarantee a "maybe".
971
971
972 if ignored:
972 if ignored:
973 ignore = util.never
973 ignore = util.never
974 dirignore = util.never
974 dirignore = util.never
975 elif unknown:
975 elif unknown:
976 ignore = self._ignore
976 ignore = self._ignore
977 dirignore = self._dirignore
977 dirignore = self._dirignore
978 else:
978 else:
979 # if not unknown and not ignored, drop dir recursion and step 2
979 # if not unknown and not ignored, drop dir recursion and step 2
980 ignore = util.always
980 ignore = util.always
981 dirignore = util.always
981 dirignore = util.always
982
982
983 matchfn = match.matchfn
983 matchfn = match.matchfn
984 matchalways = match.always()
984 matchalways = match.always()
985 matchtdir = match.traversedir
985 matchtdir = match.traversedir
986 dmap = self._map
986 dmap = self._map
987 listdir = util.listdir
987 listdir = util.listdir
988 lstat = os.lstat
988 lstat = os.lstat
989 dirkind = stat.S_IFDIR
989 dirkind = stat.S_IFDIR
990 regkind = stat.S_IFREG
990 regkind = stat.S_IFREG
991 lnkkind = stat.S_IFLNK
991 lnkkind = stat.S_IFLNK
992 join = self._join
992 join = self._join
993
993
994 exact = skipstep3 = False
994 exact = skipstep3 = False
995 if match.isexact(): # match.exact
995 if match.isexact(): # match.exact
996 exact = True
996 exact = True
997 dirignore = util.always # skip step 2
997 dirignore = util.always # skip step 2
998 elif match.prefix(): # match.match, no patterns
998 elif match.prefix(): # match.match, no patterns
999 skipstep3 = True
999 skipstep3 = True
1000
1000
1001 if not exact and self._checkcase:
1001 if not exact and self._checkcase:
1002 normalize = self._normalize
1002 normalize = self._normalize
1003 normalizefile = self._normalizefile
1003 normalizefile = self._normalizefile
1004 skipstep3 = False
1004 skipstep3 = False
1005 else:
1005 else:
1006 normalize = self._normalize
1006 normalize = self._normalize
1007 normalizefile = None
1007 normalizefile = None
1008
1008
1009 # step 1: find all explicit files
1009 # step 1: find all explicit files
1010 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1010 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1011 if matchtdir:
1011 if matchtdir:
1012 for d in work:
1012 for d in work:
1013 matchtdir(d[0])
1013 matchtdir(d[0])
1014 for d in dirsnotfound:
1014 for d in dirsnotfound:
1015 matchtdir(d)
1015 matchtdir(d)
1016
1016
1017 skipstep3 = skipstep3 and not (work or dirsnotfound)
1017 skipstep3 = skipstep3 and not (work or dirsnotfound)
1018 work = [d for d in work if not dirignore(d[0])]
1018 work = [d for d in work if not dirignore(d[0])]
1019
1019
1020 # step 2: visit subdirectories
1020 # step 2: visit subdirectories
1021 def traverse(work, alreadynormed):
1021 def traverse(work, alreadynormed):
1022 wadd = work.append
1022 wadd = work.append
1023 while work:
1023 while work:
1024 tracing.counter('dirstate.walk work', len(work))
1024 tracing.counter('dirstate.walk work', len(work))
1025 nd = work.pop()
1025 nd = work.pop()
1026 visitentries = match.visitchildrenset(nd)
1026 visitentries = match.visitchildrenset(nd)
1027 if not visitentries:
1027 if not visitentries:
1028 continue
1028 continue
1029 if visitentries == b'this' or visitentries == b'all':
1029 if visitentries == b'this' or visitentries == b'all':
1030 visitentries = None
1030 visitentries = None
1031 skip = None
1031 skip = None
1032 if nd != b'':
1032 if nd != b'':
1033 skip = b'.hg'
1033 skip = b'.hg'
1034 try:
1034 try:
1035 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1035 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1036 entries = listdir(join(nd), stat=True, skip=skip)
1036 entries = listdir(join(nd), stat=True, skip=skip)
1037 except (PermissionError, FileNotFoundError) as inst:
1037 except (PermissionError, FileNotFoundError) as inst:
1038 match.bad(
1038 match.bad(
1039 self.pathto(nd), encoding.strtolocal(inst.strerror)
1039 self.pathto(nd), encoding.strtolocal(inst.strerror)
1040 )
1040 )
1041 continue
1041 continue
1042 for f, kind, st in entries:
1042 for f, kind, st in entries:
1043 # Some matchers may return files in the visitentries set,
1043 # Some matchers may return files in the visitentries set,
1044 # instead of 'this', if the matcher explicitly mentions them
1044 # instead of 'this', if the matcher explicitly mentions them
1045 # and is not an exactmatcher. This is acceptable; we do not
1045 # and is not an exactmatcher. This is acceptable; we do not
1046 # make any hard assumptions about file-or-directory below
1046 # make any hard assumptions about file-or-directory below
1047 # based on the presence of `f` in visitentries. If
1047 # based on the presence of `f` in visitentries. If
1048 # visitchildrenset returned a set, we can always skip the
1048 # visitchildrenset returned a set, we can always skip the
1049 # entries *not* in the set it provided regardless of whether
1049 # entries *not* in the set it provided regardless of whether
1050 # they're actually a file or a directory.
1050 # they're actually a file or a directory.
1051 if visitentries and f not in visitentries:
1051 if visitentries and f not in visitentries:
1052 continue
1052 continue
1053 if normalizefile:
1053 if normalizefile:
1054 # even though f might be a directory, we're only
1054 # even though f might be a directory, we're only
1055 # interested in comparing it to files currently in the
1055 # interested in comparing it to files currently in the
1056 # dmap -- therefore normalizefile is enough
1056 # dmap -- therefore normalizefile is enough
1057 nf = normalizefile(
1057 nf = normalizefile(
1058 nd and (nd + b"/" + f) or f, True, True
1058 nd and (nd + b"/" + f) or f, True, True
1059 )
1059 )
1060 else:
1060 else:
1061 nf = nd and (nd + b"/" + f) or f
1061 nf = nd and (nd + b"/" + f) or f
1062 if nf not in results:
1062 if nf not in results:
1063 if kind == dirkind:
1063 if kind == dirkind:
1064 if not ignore(nf):
1064 if not ignore(nf):
1065 if matchtdir:
1065 if matchtdir:
1066 matchtdir(nf)
1066 matchtdir(nf)
1067 wadd(nf)
1067 wadd(nf)
1068 if nf in dmap and (matchalways or matchfn(nf)):
1068 if nf in dmap and (matchalways or matchfn(nf)):
1069 results[nf] = None
1069 results[nf] = None
1070 elif kind == regkind or kind == lnkkind:
1070 elif kind == regkind or kind == lnkkind:
1071 if nf in dmap:
1071 if nf in dmap:
1072 if matchalways or matchfn(nf):
1072 if matchalways or matchfn(nf):
1073 results[nf] = st
1073 results[nf] = st
1074 elif (matchalways or matchfn(nf)) and not ignore(
1074 elif (matchalways or matchfn(nf)) and not ignore(
1075 nf
1075 nf
1076 ):
1076 ):
1077 # unknown file -- normalize if necessary
1077 # unknown file -- normalize if necessary
1078 if not alreadynormed:
1078 if not alreadynormed:
1079 nf = normalize(nf, False, True)
1079 nf = normalize(nf, False, True)
1080 results[nf] = st
1080 results[nf] = st
1081 elif nf in dmap and (matchalways or matchfn(nf)):
1081 elif nf in dmap and (matchalways or matchfn(nf)):
1082 results[nf] = None
1082 results[nf] = None
1083
1083
1084 for nd, d in work:
1084 for nd, d in work:
1085 # alreadynormed means that processwork doesn't have to do any
1085 # alreadynormed means that processwork doesn't have to do any
1086 # expensive directory normalization
1086 # expensive directory normalization
1087 alreadynormed = not normalize or nd == d
1087 alreadynormed = not normalize or nd == d
1088 traverse([d], alreadynormed)
1088 traverse([d], alreadynormed)
1089
1089
1090 for s in subrepos:
1090 for s in subrepos:
1091 del results[s]
1091 del results[s]
1092 del results[b'.hg']
1092 del results[b'.hg']
1093
1093
1094 # step 3: visit remaining files from dmap
1094 # step 3: visit remaining files from dmap
1095 if not skipstep3 and not exact:
1095 if not skipstep3 and not exact:
1096 # If a dmap file is not in results yet, it was either
1096 # If a dmap file is not in results yet, it was either
1097 # a) not matching matchfn b) ignored, c) missing, or d) under a
1097 # a) not matching matchfn b) ignored, c) missing, or d) under a
1098 # symlink directory.
1098 # symlink directory.
1099 if not results and matchalways:
1099 if not results and matchalways:
1100 visit = [f for f in dmap]
1100 visit = [f for f in dmap]
1101 else:
1101 else:
1102 visit = [f for f in dmap if f not in results and matchfn(f)]
1102 visit = [f for f in dmap if f not in results and matchfn(f)]
1103 visit.sort()
1103 visit.sort()
1104
1104
1105 if unknown:
1105 if unknown:
1106 # unknown == True means we walked all dirs under the roots
1106 # unknown == True means we walked all dirs under the roots
1107 # that wasn't ignored, and everything that matched was stat'ed
1107 # that wasn't ignored, and everything that matched was stat'ed
1108 # and is already in results.
1108 # and is already in results.
1109 # The rest must thus be ignored or under a symlink.
1109 # The rest must thus be ignored or under a symlink.
1110 audit_path = pathutil.pathauditor(self._root, cached=True)
1110 audit_path = pathutil.pathauditor(self._root, cached=True)
1111
1111
1112 for nf in iter(visit):
1112 for nf in iter(visit):
1113 # If a stat for the same file was already added with a
1113 # If a stat for the same file was already added with a
1114 # different case, don't add one for this, since that would
1114 # different case, don't add one for this, since that would
1115 # make it appear as if the file exists under both names
1115 # make it appear as if the file exists under both names
1116 # on disk.
1116 # on disk.
1117 if (
1117 if (
1118 normalizefile
1118 normalizefile
1119 and normalizefile(nf, True, True) in results
1119 and normalizefile(nf, True, True) in results
1120 ):
1120 ):
1121 results[nf] = None
1121 results[nf] = None
1122 # Report ignored items in the dmap as long as they are not
1122 # Report ignored items in the dmap as long as they are not
1123 # under a symlink directory.
1123 # under a symlink directory.
1124 elif audit_path.check(nf):
1124 elif audit_path.check(nf):
1125 try:
1125 try:
1126 results[nf] = lstat(join(nf))
1126 results[nf] = lstat(join(nf))
1127 # file was just ignored, no links, and exists
1127 # file was just ignored, no links, and exists
1128 except OSError:
1128 except OSError:
1129 # file doesn't exist
1129 # file doesn't exist
1130 results[nf] = None
1130 results[nf] = None
1131 else:
1131 else:
1132 # It's either missing or under a symlink directory
1132 # It's either missing or under a symlink directory
1133 # which we in this case report as missing
1133 # which we in this case report as missing
1134 results[nf] = None
1134 results[nf] = None
1135 else:
1135 else:
1136 # We may not have walked the full directory tree above,
1136 # We may not have walked the full directory tree above,
1137 # so stat and check everything we missed.
1137 # so stat and check everything we missed.
1138 iv = iter(visit)
1138 iv = iter(visit)
1139 for st in util.statfiles([join(i) for i in visit]):
1139 for st in util.statfiles([join(i) for i in visit]):
1140 results[next(iv)] = st
1140 results[next(iv)] = st
1141 return results
1141 return results
1142
1142
1143 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1143 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1144 # Force Rayon (Rust parallelism library) to respect the number of
1144 # Force Rayon (Rust parallelism library) to respect the number of
1145 # workers. This is a temporary workaround until Rust code knows
1145 # workers. This is a temporary workaround until Rust code knows
1146 # how to read the config file.
1146 # how to read the config file.
1147 numcpus = self._ui.configint(b"worker", b"numcpus")
1147 numcpus = self._ui.configint(b"worker", b"numcpus")
1148 if numcpus is not None:
1148 if numcpus is not None:
1149 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1149 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1150
1150
1151 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1151 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1152 if not workers_enabled:
1152 if not workers_enabled:
1153 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1153 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1154
1154
1155 (
1155 (
1156 lookup,
1156 lookup,
1157 modified,
1157 modified,
1158 added,
1158 added,
1159 removed,
1159 removed,
1160 deleted,
1160 deleted,
1161 clean,
1161 clean,
1162 ignored,
1162 ignored,
1163 unknown,
1163 unknown,
1164 warnings,
1164 warnings,
1165 bad,
1165 bad,
1166 traversed,
1166 traversed,
1167 dirty,
1167 dirty,
1168 ) = rustmod.status(
1168 ) = rustmod.status(
1169 self._map._map,
1169 self._map._map,
1170 matcher,
1170 matcher,
1171 self._rootdir,
1171 self._rootdir,
1172 self._ignorefiles(),
1172 self._ignorefiles(),
1173 self._checkexec,
1173 self._checkexec,
1174 bool(list_clean),
1174 bool(list_clean),
1175 bool(list_ignored),
1175 bool(list_ignored),
1176 bool(list_unknown),
1176 bool(list_unknown),
1177 bool(matcher.traversedir),
1177 bool(matcher.traversedir),
1178 )
1178 )
1179
1179
1180 self._dirty |= dirty
1180 self._dirty |= dirty
1181
1181
1182 if matcher.traversedir:
1182 if matcher.traversedir:
1183 for dir in traversed:
1183 for dir in traversed:
1184 matcher.traversedir(dir)
1184 matcher.traversedir(dir)
1185
1185
1186 if self._ui.warn:
1186 if self._ui.warn:
1187 for item in warnings:
1187 for item in warnings:
1188 if isinstance(item, tuple):
1188 if isinstance(item, tuple):
1189 file_path, syntax = item
1189 file_path, syntax = item
1190 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1190 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1191 file_path,
1191 file_path,
1192 syntax,
1192 syntax,
1193 )
1193 )
1194 self._ui.warn(msg)
1194 self._ui.warn(msg)
1195 else:
1195 else:
1196 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1196 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1197 self._ui.warn(
1197 self._ui.warn(
1198 msg
1198 msg
1199 % (
1199 % (
1200 pathutil.canonpath(
1200 pathutil.canonpath(
1201 self._rootdir, self._rootdir, item
1201 self._rootdir, self._rootdir, item
1202 ),
1202 ),
1203 b"No such file or directory",
1203 b"No such file or directory",
1204 )
1204 )
1205 )
1205 )
1206
1206
1207 for (fn, message) in bad:
1207 for (fn, message) in bad:
1208 matcher.bad(fn, encoding.strtolocal(message))
1208 matcher.bad(fn, encoding.strtolocal(message))
1209
1209
1210 status = scmutil.status(
1210 status = scmutil.status(
1211 modified=modified,
1211 modified=modified,
1212 added=added,
1212 added=added,
1213 removed=removed,
1213 removed=removed,
1214 deleted=deleted,
1214 deleted=deleted,
1215 unknown=unknown,
1215 unknown=unknown,
1216 ignored=ignored,
1216 ignored=ignored,
1217 clean=clean,
1217 clean=clean,
1218 )
1218 )
1219 return (lookup, status)
1219 return (lookup, status)
1220
1220
1221 def status(self, match, subrepos, ignored, clean, unknown):
1221 def status(self, match, subrepos, ignored, clean, unknown):
1222 """Determine the status of the working copy relative to the
1222 """Determine the status of the working copy relative to the
1223 dirstate and return a pair of (unsure, status), where status is of type
1223 dirstate and return a pair of (unsure, status), where status is of type
1224 scmutil.status and:
1224 scmutil.status and:
1225
1225
1226 unsure:
1226 unsure:
1227 files that might have been modified since the dirstate was
1227 files that might have been modified since the dirstate was
1228 written, but need to be read to be sure (size is the same
1228 written, but need to be read to be sure (size is the same
1229 but mtime differs)
1229 but mtime differs)
1230 status.modified:
1230 status.modified:
1231 files that have definitely been modified since the dirstate
1231 files that have definitely been modified since the dirstate
1232 was written (different size or mode)
1232 was written (different size or mode)
1233 status.clean:
1233 status.clean:
1234 files that have definitely not been modified since the
1234 files that have definitely not been modified since the
1235 dirstate was written
1235 dirstate was written
1236 """
1236 """
1237 listignored, listclean, listunknown = ignored, clean, unknown
1237 listignored, listclean, listunknown = ignored, clean, unknown
1238 lookup, modified, added, unknown, ignored = [], [], [], [], []
1238 lookup, modified, added, unknown, ignored = [], [], [], [], []
1239 removed, deleted, clean = [], [], []
1239 removed, deleted, clean = [], [], []
1240
1240
1241 dmap = self._map
1241 dmap = self._map
1242 dmap.preload()
1242 dmap.preload()
1243
1243
1244 use_rust = True
1244 use_rust = True
1245
1245
1246 allowed_matchers = (
1246 allowed_matchers = (
1247 matchmod.alwaysmatcher,
1247 matchmod.alwaysmatcher,
1248 matchmod.exactmatcher,
1248 matchmod.exactmatcher,
1249 matchmod.includematcher,
1249 matchmod.includematcher,
1250 matchmod.intersectionmatcher,
1250 matchmod.intersectionmatcher,
1251 matchmod.nevermatcher,
1251 matchmod.unionmatcher,
1252 matchmod.unionmatcher,
1252 )
1253 )
1253
1254
1254 if rustmod is None:
1255 if rustmod is None:
1255 use_rust = False
1256 use_rust = False
1256 elif self._checkcase:
1257 elif self._checkcase:
1257 # Case-insensitive filesystems are not handled yet
1258 # Case-insensitive filesystems are not handled yet
1258 use_rust = False
1259 use_rust = False
1259 elif subrepos:
1260 elif subrepos:
1260 use_rust = False
1261 use_rust = False
1261 elif sparse.enabled:
1262 elif sparse.enabled:
1262 use_rust = False
1263 use_rust = False
1263 elif not isinstance(match, allowed_matchers):
1264 elif not isinstance(match, allowed_matchers):
1264 # Some matchers have yet to be implemented
1265 # Some matchers have yet to be implemented
1265 use_rust = False
1266 use_rust = False
1266
1267
1267 # Get the time from the filesystem so we can disambiguate files that
1268 # Get the time from the filesystem so we can disambiguate files that
1268 # appear modified in the present or future.
1269 # appear modified in the present or future.
1269 try:
1270 try:
1270 mtime_boundary = timestamp.get_fs_now(self._opener)
1271 mtime_boundary = timestamp.get_fs_now(self._opener)
1271 except OSError:
1272 except OSError:
1272 # In largefiles or readonly context
1273 # In largefiles or readonly context
1273 mtime_boundary = None
1274 mtime_boundary = None
1274
1275
1275 if use_rust:
1276 if use_rust:
1276 try:
1277 try:
1277 res = self._rust_status(
1278 res = self._rust_status(
1278 match, listclean, listignored, listunknown
1279 match, listclean, listignored, listunknown
1279 )
1280 )
1280 return res + (mtime_boundary,)
1281 return res + (mtime_boundary,)
1281 except rustmod.FallbackError:
1282 except rustmod.FallbackError:
1282 pass
1283 pass
1283
1284
1284 def noop(f):
1285 def noop(f):
1285 pass
1286 pass
1286
1287
1287 dcontains = dmap.__contains__
1288 dcontains = dmap.__contains__
1288 dget = dmap.__getitem__
1289 dget = dmap.__getitem__
1289 ladd = lookup.append # aka "unsure"
1290 ladd = lookup.append # aka "unsure"
1290 madd = modified.append
1291 madd = modified.append
1291 aadd = added.append
1292 aadd = added.append
1292 uadd = unknown.append if listunknown else noop
1293 uadd = unknown.append if listunknown else noop
1293 iadd = ignored.append if listignored else noop
1294 iadd = ignored.append if listignored else noop
1294 radd = removed.append
1295 radd = removed.append
1295 dadd = deleted.append
1296 dadd = deleted.append
1296 cadd = clean.append if listclean else noop
1297 cadd = clean.append if listclean else noop
1297 mexact = match.exact
1298 mexact = match.exact
1298 dirignore = self._dirignore
1299 dirignore = self._dirignore
1299 checkexec = self._checkexec
1300 checkexec = self._checkexec
1300 checklink = self._checklink
1301 checklink = self._checklink
1301 copymap = self._map.copymap
1302 copymap = self._map.copymap
1302
1303
1303 # We need to do full walks when either
1304 # We need to do full walks when either
1304 # - we're listing all clean files, or
1305 # - we're listing all clean files, or
1305 # - match.traversedir does something, because match.traversedir should
1306 # - match.traversedir does something, because match.traversedir should
1306 # be called for every dir in the working dir
1307 # be called for every dir in the working dir
1307 full = listclean or match.traversedir is not None
1308 full = listclean or match.traversedir is not None
1308 for fn, st in self.walk(
1309 for fn, st in self.walk(
1309 match, subrepos, listunknown, listignored, full=full
1310 match, subrepos, listunknown, listignored, full=full
1310 ).items():
1311 ).items():
1311 if not dcontains(fn):
1312 if not dcontains(fn):
1312 if (listignored or mexact(fn)) and dirignore(fn):
1313 if (listignored or mexact(fn)) and dirignore(fn):
1313 if listignored:
1314 if listignored:
1314 iadd(fn)
1315 iadd(fn)
1315 else:
1316 else:
1316 uadd(fn)
1317 uadd(fn)
1317 continue
1318 continue
1318
1319
1319 t = dget(fn)
1320 t = dget(fn)
1320 mode = t.mode
1321 mode = t.mode
1321 size = t.size
1322 size = t.size
1322
1323
1323 if not st and t.tracked:
1324 if not st and t.tracked:
1324 dadd(fn)
1325 dadd(fn)
1325 elif t.p2_info:
1326 elif t.p2_info:
1326 madd(fn)
1327 madd(fn)
1327 elif t.added:
1328 elif t.added:
1328 aadd(fn)
1329 aadd(fn)
1329 elif t.removed:
1330 elif t.removed:
1330 radd(fn)
1331 radd(fn)
1331 elif t.tracked:
1332 elif t.tracked:
1332 if not checklink and t.has_fallback_symlink:
1333 if not checklink and t.has_fallback_symlink:
1333 # If the file system does not support symlink, the mode
1334 # If the file system does not support symlink, the mode
1334 # might not be correctly stored in the dirstate, so do not
1335 # might not be correctly stored in the dirstate, so do not
1335 # trust it.
1336 # trust it.
1336 ladd(fn)
1337 ladd(fn)
1337 elif not checkexec and t.has_fallback_exec:
1338 elif not checkexec and t.has_fallback_exec:
1338 # If the file system does not support exec bits, the mode
1339 # If the file system does not support exec bits, the mode
1339 # might not be correctly stored in the dirstate, so do not
1340 # might not be correctly stored in the dirstate, so do not
1340 # trust it.
1341 # trust it.
1341 ladd(fn)
1342 ladd(fn)
1342 elif (
1343 elif (
1343 size >= 0
1344 size >= 0
1344 and (
1345 and (
1345 (size != st.st_size and size != st.st_size & _rangemask)
1346 (size != st.st_size and size != st.st_size & _rangemask)
1346 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1347 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1347 )
1348 )
1348 or fn in copymap
1349 or fn in copymap
1349 ):
1350 ):
1350 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1351 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1351 # issue6456: Size returned may be longer due to
1352 # issue6456: Size returned may be longer due to
1352 # encryption on EXT-4 fscrypt, undecided.
1353 # encryption on EXT-4 fscrypt, undecided.
1353 ladd(fn)
1354 ladd(fn)
1354 else:
1355 else:
1355 madd(fn)
1356 madd(fn)
1356 elif not t.mtime_likely_equal_to(timestamp.mtime_of(st)):
1357 elif not t.mtime_likely_equal_to(timestamp.mtime_of(st)):
1357 # There might be a change in the future if for example the
1358 # There might be a change in the future if for example the
1358 # internal clock is off, but this is a case where the issues
1359 # internal clock is off, but this is a case where the issues
1359 # the user would face would be a lot worse and there is
1360 # the user would face would be a lot worse and there is
1360 # nothing we can really do.
1361 # nothing we can really do.
1361 ladd(fn)
1362 ladd(fn)
1362 elif listclean:
1363 elif listclean:
1363 cadd(fn)
1364 cadd(fn)
1364 status = scmutil.status(
1365 status = scmutil.status(
1365 modified, added, removed, deleted, unknown, ignored, clean
1366 modified, added, removed, deleted, unknown, ignored, clean
1366 )
1367 )
1367 return (lookup, status, mtime_boundary)
1368 return (lookup, status, mtime_boundary)
1368
1369
1369 def matches(self, match):
1370 def matches(self, match):
1370 """
1371 """
1371 return files in the dirstate (in whatever state) filtered by match
1372 return files in the dirstate (in whatever state) filtered by match
1372 """
1373 """
1373 dmap = self._map
1374 dmap = self._map
1374 if rustmod is not None:
1375 if rustmod is not None:
1375 dmap = self._map._map
1376 dmap = self._map._map
1376
1377
1377 if match.always():
1378 if match.always():
1378 return dmap.keys()
1379 return dmap.keys()
1379 files = match.files()
1380 files = match.files()
1380 if match.isexact():
1381 if match.isexact():
1381 # fast path -- filter the other way around, since typically files is
1382 # fast path -- filter the other way around, since typically files is
1382 # much smaller than dmap
1383 # much smaller than dmap
1383 return [f for f in files if f in dmap]
1384 return [f for f in files if f in dmap]
1384 if match.prefix() and all(fn in dmap for fn in files):
1385 if match.prefix() and all(fn in dmap for fn in files):
1385 # fast path -- all the values are known to be files, so just return
1386 # fast path -- all the values are known to be files, so just return
1386 # that
1387 # that
1387 return list(files)
1388 return list(files)
1388 return [f for f in dmap if match(f)]
1389 return [f for f in dmap if match(f)]
1389
1390
1390 def _actualfilename(self, tr):
1391 def _actualfilename(self, tr):
1391 if tr:
1392 if tr:
1392 return self._pendingfilename
1393 return self._pendingfilename
1393 else:
1394 else:
1394 return self._filename
1395 return self._filename
1395
1396
1396 def savebackup(self, tr, backupname):
1397 def savebackup(self, tr, backupname):
1397 '''Save current dirstate into backup file'''
1398 '''Save current dirstate into backup file'''
1398 filename = self._actualfilename(tr)
1399 filename = self._actualfilename(tr)
1399 assert backupname != filename
1400 assert backupname != filename
1400
1401
1401 # use '_writedirstate' instead of 'write' to write changes certainly,
1402 # use '_writedirstate' instead of 'write' to write changes certainly,
1402 # because the latter omits writing out if transaction is running.
1403 # because the latter omits writing out if transaction is running.
1403 # output file will be used to create backup of dirstate at this point.
1404 # output file will be used to create backup of dirstate at this point.
1404 if self._dirty or not self._opener.exists(filename):
1405 if self._dirty or not self._opener.exists(filename):
1405 self._writedirstate(
1406 self._writedirstate(
1406 tr,
1407 tr,
1407 self._opener(filename, b"w", atomictemp=True, checkambig=True),
1408 self._opener(filename, b"w", atomictemp=True, checkambig=True),
1408 )
1409 )
1409
1410
1410 if tr:
1411 if tr:
1411 # ensure that subsequent tr.writepending returns True for
1412 # ensure that subsequent tr.writepending returns True for
1412 # changes written out above, even if dirstate is never
1413 # changes written out above, even if dirstate is never
1413 # changed after this
1414 # changed after this
1414 tr.addfilegenerator(
1415 tr.addfilegenerator(
1415 b'dirstate-1-main',
1416 b'dirstate-1-main',
1416 (self._filename,),
1417 (self._filename,),
1417 lambda f: self._writedirstate(tr, f),
1418 lambda f: self._writedirstate(tr, f),
1418 location=b'plain',
1419 location=b'plain',
1419 post_finalize=True,
1420 post_finalize=True,
1420 )
1421 )
1421
1422
1422 # ensure that pending file written above is unlinked at
1423 # ensure that pending file written above is unlinked at
1423 # failure, even if tr.writepending isn't invoked until the
1424 # failure, even if tr.writepending isn't invoked until the
1424 # end of this transaction
1425 # end of this transaction
1425 tr.registertmp(filename, location=b'plain')
1426 tr.registertmp(filename, location=b'plain')
1426
1427
1427 self._opener.tryunlink(backupname)
1428 self._opener.tryunlink(backupname)
1428 # hardlink backup is okay because _writedirstate is always called
1429 # hardlink backup is okay because _writedirstate is always called
1429 # with an "atomictemp=True" file.
1430 # with an "atomictemp=True" file.
1430 util.copyfile(
1431 util.copyfile(
1431 self._opener.join(filename),
1432 self._opener.join(filename),
1432 self._opener.join(backupname),
1433 self._opener.join(backupname),
1433 hardlink=True,
1434 hardlink=True,
1434 )
1435 )
1435
1436
1436 def restorebackup(self, tr, backupname):
1437 def restorebackup(self, tr, backupname):
1437 '''Restore dirstate by backup file'''
1438 '''Restore dirstate by backup file'''
1438 # this "invalidate()" prevents "wlock.release()" from writing
1439 # this "invalidate()" prevents "wlock.release()" from writing
1439 # changes of dirstate out after restoring from backup file
1440 # changes of dirstate out after restoring from backup file
1440 self.invalidate()
1441 self.invalidate()
1441 filename = self._actualfilename(tr)
1442 filename = self._actualfilename(tr)
1442 o = self._opener
1443 o = self._opener
1443 if util.samefile(o.join(backupname), o.join(filename)):
1444 if util.samefile(o.join(backupname), o.join(filename)):
1444 o.unlink(backupname)
1445 o.unlink(backupname)
1445 else:
1446 else:
1446 o.rename(backupname, filename, checkambig=True)
1447 o.rename(backupname, filename, checkambig=True)
1447
1448
1448 def clearbackup(self, tr, backupname):
1449 def clearbackup(self, tr, backupname):
1449 '''Clear backup file'''
1450 '''Clear backup file'''
1450 self._opener.unlink(backupname)
1451 self._opener.unlink(backupname)
1451
1452
1452 def verify(self, m1, m2):
1453 def verify(self, m1, m2):
1453 """check the dirstate content again the parent manifest and yield errors"""
1454 """check the dirstate content again the parent manifest and yield errors"""
1454 missing_from_p1 = b"%s in state %s, but not in manifest1\n"
1455 missing_from_p1 = b"%s in state %s, but not in manifest1\n"
1455 unexpected_in_p1 = b"%s in state %s, but also in manifest1\n"
1456 unexpected_in_p1 = b"%s in state %s, but also in manifest1\n"
1456 missing_from_ps = b"%s in state %s, but not in either manifest\n"
1457 missing_from_ps = b"%s in state %s, but not in either manifest\n"
1457 missing_from_ds = b"%s in manifest1, but listed as state %s\n"
1458 missing_from_ds = b"%s in manifest1, but listed as state %s\n"
1458 for f, entry in self.items():
1459 for f, entry in self.items():
1459 state = entry.state
1460 state = entry.state
1460 if state in b"nr" and f not in m1:
1461 if state in b"nr" and f not in m1:
1461 yield (missing_from_p1, f, state)
1462 yield (missing_from_p1, f, state)
1462 if state in b"a" and f in m1:
1463 if state in b"a" and f in m1:
1463 yield (unexpected_in_p1, f, state)
1464 yield (unexpected_in_p1, f, state)
1464 if state in b"m" and f not in m1 and f not in m2:
1465 if state in b"m" and f not in m1 and f not in m2:
1465 yield (missing_from_ps, f, state)
1466 yield (missing_from_ps, f, state)
1466 for f in m1:
1467 for f in m1:
1467 state = self.get_entry(f).state
1468 state = self.get_entry(f).state
1468 if state not in b"nrm":
1469 if state not in b"nrm":
1469 yield (missing_from_ds, f, state)
1470 yield (missing_from_ds, f, state)
@@ -1,1442 +1,1467 b''
1 // matchers.rs
1 // matchers.rs
2 //
2 //
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 //! Structs and types for matching files and directories.
8 //! Structs and types for matching files and directories.
9
9
10 use crate::{
10 use crate::{
11 dirstate::dirs_multiset::DirsChildrenMultiset,
11 dirstate::dirs_multiset::DirsChildrenMultiset,
12 filepatterns::{
12 filepatterns::{
13 build_single_regex, filter_subincludes, get_patterns_from_file,
13 build_single_regex, filter_subincludes, get_patterns_from_file,
14 PatternFileWarning, PatternResult,
14 PatternFileWarning, PatternResult,
15 },
15 },
16 utils::{
16 utils::{
17 files::find_dirs,
17 files::find_dirs,
18 hg_path::{HgPath, HgPathBuf},
18 hg_path::{HgPath, HgPathBuf},
19 Escaped,
19 Escaped,
20 },
20 },
21 DirsMultiset, DirstateMapError, FastHashMap, IgnorePattern, PatternError,
21 DirsMultiset, DirstateMapError, FastHashMap, IgnorePattern, PatternError,
22 PatternSyntax,
22 PatternSyntax,
23 };
23 };
24
24
25 use crate::dirstate::status::IgnoreFnType;
25 use crate::dirstate::status::IgnoreFnType;
26 use crate::filepatterns::normalize_path_bytes;
26 use crate::filepatterns::normalize_path_bytes;
27 use std::borrow::ToOwned;
27 use std::borrow::ToOwned;
28 use std::collections::HashSet;
28 use std::collections::HashSet;
29 use std::fmt::{Display, Error, Formatter};
29 use std::fmt::{Display, Error, Formatter};
30 use std::iter::FromIterator;
30 use std::iter::FromIterator;
31 use std::ops::Deref;
31 use std::ops::Deref;
32 use std::path::{Path, PathBuf};
32 use std::path::{Path, PathBuf};
33
33
34 use micro_timer::timed;
34 use micro_timer::timed;
35
35
36 #[derive(Debug, PartialEq)]
36 #[derive(Debug, PartialEq)]
37 pub enum VisitChildrenSet {
37 pub enum VisitChildrenSet {
38 /// Don't visit anything
38 /// Don't visit anything
39 Empty,
39 Empty,
40 /// Only visit this directory
40 /// Only visit this directory
41 This,
41 This,
42 /// Visit this directory and these subdirectories
42 /// Visit this directory and these subdirectories
43 /// TODO Should we implement a `NonEmptyHashSet`?
43 /// TODO Should we implement a `NonEmptyHashSet`?
44 Set(HashSet<HgPathBuf>),
44 Set(HashSet<HgPathBuf>),
45 /// Visit this directory and all subdirectories
45 /// Visit this directory and all subdirectories
46 Recursive,
46 Recursive,
47 }
47 }
48
48
49 pub trait Matcher {
49 pub trait Matcher {
50 /// Explicitly listed files
50 /// Explicitly listed files
51 fn file_set(&self) -> Option<&HashSet<HgPathBuf>>;
51 fn file_set(&self) -> Option<&HashSet<HgPathBuf>>;
52 /// Returns whether `filename` is in `file_set`
52 /// Returns whether `filename` is in `file_set`
53 fn exact_match(&self, filename: &HgPath) -> bool;
53 fn exact_match(&self, filename: &HgPath) -> bool;
54 /// Returns whether `filename` is matched by this matcher
54 /// Returns whether `filename` is matched by this matcher
55 fn matches(&self, filename: &HgPath) -> bool;
55 fn matches(&self, filename: &HgPath) -> bool;
56 /// Decides whether a directory should be visited based on whether it
56 /// Decides whether a directory should be visited based on whether it
57 /// has potential matches in it or one of its subdirectories, and
57 /// has potential matches in it or one of its subdirectories, and
58 /// potentially lists which subdirectories of that directory should be
58 /// potentially lists which subdirectories of that directory should be
59 /// visited. This is based on the match's primary, included, and excluded
59 /// visited. This is based on the match's primary, included, and excluded
60 /// patterns.
60 /// patterns.
61 ///
61 ///
62 /// # Example
62 /// # Example
63 ///
63 ///
64 /// Assume matchers `['path:foo/bar', 'rootfilesin:qux']`, we would
64 /// Assume matchers `['path:foo/bar', 'rootfilesin:qux']`, we would
65 /// return the following values (assuming the implementation of
65 /// return the following values (assuming the implementation of
66 /// visit_children_set is capable of recognizing this; some implementations
66 /// visit_children_set is capable of recognizing this; some implementations
67 /// are not).
67 /// are not).
68 ///
68 ///
69 /// ```text
69 /// ```text
70 /// ```ignore
70 /// ```ignore
71 /// '' -> {'foo', 'qux'}
71 /// '' -> {'foo', 'qux'}
72 /// 'baz' -> set()
72 /// 'baz' -> set()
73 /// 'foo' -> {'bar'}
73 /// 'foo' -> {'bar'}
74 /// // Ideally this would be `Recursive`, but since the prefix nature of
74 /// // Ideally this would be `Recursive`, but since the prefix nature of
75 /// // matchers is applied to the entire matcher, we have to downgrade this
75 /// // matchers is applied to the entire matcher, we have to downgrade this
76 /// // to `This` due to the (yet to be implemented in Rust) non-prefix
76 /// // to `This` due to the (yet to be implemented in Rust) non-prefix
77 /// // `RootFilesIn'-kind matcher being mixed in.
77 /// // `RootFilesIn'-kind matcher being mixed in.
78 /// 'foo/bar' -> 'this'
78 /// 'foo/bar' -> 'this'
79 /// 'qux' -> 'this'
79 /// 'qux' -> 'this'
80 /// ```
80 /// ```
81 /// # Important
81 /// # Important
82 ///
82 ///
83 /// Most matchers do not know if they're representing files or
83 /// Most matchers do not know if they're representing files or
84 /// directories. They see `['path:dir/f']` and don't know whether `f` is a
84 /// directories. They see `['path:dir/f']` and don't know whether `f` is a
85 /// file or a directory, so `visit_children_set('dir')` for most matchers
85 /// file or a directory, so `visit_children_set('dir')` for most matchers
86 /// will return `HashSet{ HgPath { "f" } }`, but if the matcher knows it's
86 /// will return `HashSet{ HgPath { "f" } }`, but if the matcher knows it's
87 /// a file (like the yet to be implemented in Rust `ExactMatcher` does),
87 /// a file (like the yet to be implemented in Rust `ExactMatcher` does),
88 /// it may return `VisitChildrenSet::This`.
88 /// it may return `VisitChildrenSet::This`.
89 /// Do not rely on the return being a `HashSet` indicating that there are
89 /// Do not rely on the return being a `HashSet` indicating that there are
90 /// no files in this dir to investigate (or equivalently that if there are
90 /// no files in this dir to investigate (or equivalently that if there are
91 /// files to investigate in 'dir' that it will always return
91 /// files to investigate in 'dir' that it will always return
92 /// `VisitChildrenSet::This`).
92 /// `VisitChildrenSet::This`).
93 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet;
93 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet;
94 /// Matcher will match everything and `files_set()` will be empty:
94 /// Matcher will match everything and `files_set()` will be empty:
95 /// optimization might be possible.
95 /// optimization might be possible.
96 fn matches_everything(&self) -> bool;
96 fn matches_everything(&self) -> bool;
97 /// Matcher will match exactly the files in `files_set()`: optimization
97 /// Matcher will match exactly the files in `files_set()`: optimization
98 /// might be possible.
98 /// might be possible.
99 fn is_exact(&self) -> bool;
99 fn is_exact(&self) -> bool;
100 }
100 }
101
101
102 /// Matches everything.
102 /// Matches everything.
103 ///```
103 ///```
104 /// use hg::{ matchers::{Matcher, AlwaysMatcher}, utils::hg_path::HgPath };
104 /// use hg::{ matchers::{Matcher, AlwaysMatcher}, utils::hg_path::HgPath };
105 ///
105 ///
106 /// let matcher = AlwaysMatcher;
106 /// let matcher = AlwaysMatcher;
107 ///
107 ///
108 /// assert_eq!(matcher.matches(HgPath::new(b"whatever")), true);
108 /// assert_eq!(matcher.matches(HgPath::new(b"whatever")), true);
109 /// assert_eq!(matcher.matches(HgPath::new(b"b.txt")), true);
109 /// assert_eq!(matcher.matches(HgPath::new(b"b.txt")), true);
110 /// assert_eq!(matcher.matches(HgPath::new(b"main.c")), true);
110 /// assert_eq!(matcher.matches(HgPath::new(b"main.c")), true);
111 /// assert_eq!(matcher.matches(HgPath::new(br"re:.*\.c$")), true);
111 /// assert_eq!(matcher.matches(HgPath::new(br"re:.*\.c$")), true);
112 /// ```
112 /// ```
113 #[derive(Debug)]
113 #[derive(Debug)]
114 pub struct AlwaysMatcher;
114 pub struct AlwaysMatcher;
115
115
116 impl Matcher for AlwaysMatcher {
116 impl Matcher for AlwaysMatcher {
117 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
117 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
118 None
118 None
119 }
119 }
120 fn exact_match(&self, _filename: &HgPath) -> bool {
120 fn exact_match(&self, _filename: &HgPath) -> bool {
121 false
121 false
122 }
122 }
123 fn matches(&self, _filename: &HgPath) -> bool {
123 fn matches(&self, _filename: &HgPath) -> bool {
124 true
124 true
125 }
125 }
126 fn visit_children_set(&self, _directory: &HgPath) -> VisitChildrenSet {
126 fn visit_children_set(&self, _directory: &HgPath) -> VisitChildrenSet {
127 VisitChildrenSet::Recursive
127 VisitChildrenSet::Recursive
128 }
128 }
129 fn matches_everything(&self) -> bool {
129 fn matches_everything(&self) -> bool {
130 true
130 true
131 }
131 }
132 fn is_exact(&self) -> bool {
132 fn is_exact(&self) -> bool {
133 false
133 false
134 }
134 }
135 }
135 }
136
136
137 /// Matches nothing.
138 #[derive(Debug)]
139 pub struct NeverMatcher;
140
141 impl Matcher for NeverMatcher {
142 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
143 None
144 }
145 fn exact_match(&self, _filename: &HgPath) -> bool {
146 false
147 }
148 fn matches(&self, _filename: &HgPath) -> bool {
149 false
150 }
151 fn visit_children_set(&self, _directory: &HgPath) -> VisitChildrenSet {
152 VisitChildrenSet::Empty
153 }
154 fn matches_everything(&self) -> bool {
155 false
156 }
157 fn is_exact(&self) -> bool {
158 true
159 }
160 }
161
137 /// Matches the input files exactly. They are interpreted as paths, not
162 /// Matches the input files exactly. They are interpreted as paths, not
138 /// patterns.
163 /// patterns.
139 ///
164 ///
140 ///```
165 ///```
141 /// use hg::{ matchers::{Matcher, FileMatcher}, utils::hg_path::{HgPath, HgPathBuf} };
166 /// use hg::{ matchers::{Matcher, FileMatcher}, utils::hg_path::{HgPath, HgPathBuf} };
142 ///
167 ///
143 /// let files = vec![HgPathBuf::from_bytes(b"a.txt"), HgPathBuf::from_bytes(br"re:.*\.c$")];
168 /// let files = vec![HgPathBuf::from_bytes(b"a.txt"), HgPathBuf::from_bytes(br"re:.*\.c$")];
144 /// let matcher = FileMatcher::new(files).unwrap();
169 /// let matcher = FileMatcher::new(files).unwrap();
145 ///
170 ///
146 /// assert_eq!(matcher.matches(HgPath::new(b"a.txt")), true);
171 /// assert_eq!(matcher.matches(HgPath::new(b"a.txt")), true);
147 /// assert_eq!(matcher.matches(HgPath::new(b"b.txt")), false);
172 /// assert_eq!(matcher.matches(HgPath::new(b"b.txt")), false);
148 /// assert_eq!(matcher.matches(HgPath::new(b"main.c")), false);
173 /// assert_eq!(matcher.matches(HgPath::new(b"main.c")), false);
149 /// assert_eq!(matcher.matches(HgPath::new(br"re:.*\.c$")), true);
174 /// assert_eq!(matcher.matches(HgPath::new(br"re:.*\.c$")), true);
150 /// ```
175 /// ```
151 #[derive(Debug)]
176 #[derive(Debug)]
152 pub struct FileMatcher {
177 pub struct FileMatcher {
153 files: HashSet<HgPathBuf>,
178 files: HashSet<HgPathBuf>,
154 dirs: DirsMultiset,
179 dirs: DirsMultiset,
155 }
180 }
156
181
157 impl FileMatcher {
182 impl FileMatcher {
158 pub fn new(files: Vec<HgPathBuf>) -> Result<Self, DirstateMapError> {
183 pub fn new(files: Vec<HgPathBuf>) -> Result<Self, DirstateMapError> {
159 let dirs = DirsMultiset::from_manifest(&files)?;
184 let dirs = DirsMultiset::from_manifest(&files)?;
160 Ok(Self {
185 Ok(Self {
161 files: HashSet::from_iter(files.into_iter()),
186 files: HashSet::from_iter(files.into_iter()),
162 dirs,
187 dirs,
163 })
188 })
164 }
189 }
165 fn inner_matches(&self, filename: &HgPath) -> bool {
190 fn inner_matches(&self, filename: &HgPath) -> bool {
166 self.files.contains(filename.as_ref())
191 self.files.contains(filename.as_ref())
167 }
192 }
168 }
193 }
169
194
170 impl Matcher for FileMatcher {
195 impl Matcher for FileMatcher {
171 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
196 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
172 Some(&self.files)
197 Some(&self.files)
173 }
198 }
174 fn exact_match(&self, filename: &HgPath) -> bool {
199 fn exact_match(&self, filename: &HgPath) -> bool {
175 self.inner_matches(filename)
200 self.inner_matches(filename)
176 }
201 }
177 fn matches(&self, filename: &HgPath) -> bool {
202 fn matches(&self, filename: &HgPath) -> bool {
178 self.inner_matches(filename)
203 self.inner_matches(filename)
179 }
204 }
180 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
205 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
181 if self.files.is_empty() || !self.dirs.contains(&directory) {
206 if self.files.is_empty() || !self.dirs.contains(&directory) {
182 return VisitChildrenSet::Empty;
207 return VisitChildrenSet::Empty;
183 }
208 }
184 let mut candidates: HashSet<HgPathBuf> =
209 let mut candidates: HashSet<HgPathBuf> =
185 self.dirs.iter().cloned().collect();
210 self.dirs.iter().cloned().collect();
186
211
187 candidates.extend(self.files.iter().cloned());
212 candidates.extend(self.files.iter().cloned());
188 candidates.remove(HgPath::new(b""));
213 candidates.remove(HgPath::new(b""));
189
214
190 if !directory.as_ref().is_empty() {
215 if !directory.as_ref().is_empty() {
191 let directory = [directory.as_ref().as_bytes(), b"/"].concat();
216 let directory = [directory.as_ref().as_bytes(), b"/"].concat();
192 candidates = candidates
217 candidates = candidates
193 .iter()
218 .iter()
194 .filter_map(|c| {
219 .filter_map(|c| {
195 if c.as_bytes().starts_with(&directory) {
220 if c.as_bytes().starts_with(&directory) {
196 Some(HgPathBuf::from_bytes(
221 Some(HgPathBuf::from_bytes(
197 &c.as_bytes()[directory.len()..],
222 &c.as_bytes()[directory.len()..],
198 ))
223 ))
199 } else {
224 } else {
200 None
225 None
201 }
226 }
202 })
227 })
203 .collect();
228 .collect();
204 }
229 }
205
230
206 // `self.dirs` includes all of the directories, recursively, so if
231 // `self.dirs` includes all of the directories, recursively, so if
207 // we're attempting to match 'foo/bar/baz.txt', it'll have '', 'foo',
232 // we're attempting to match 'foo/bar/baz.txt', it'll have '', 'foo',
208 // 'foo/bar' in it. Thus we can safely ignore a candidate that has a
233 // 'foo/bar' in it. Thus we can safely ignore a candidate that has a
209 // '/' in it, indicating it's for a subdir-of-a-subdir; the immediate
234 // '/' in it, indicating it's for a subdir-of-a-subdir; the immediate
210 // subdir will be in there without a slash.
235 // subdir will be in there without a slash.
211 VisitChildrenSet::Set(
236 VisitChildrenSet::Set(
212 candidates
237 candidates
213 .into_iter()
238 .into_iter()
214 .filter_map(|c| {
239 .filter_map(|c| {
215 if c.bytes().all(|b| *b != b'/') {
240 if c.bytes().all(|b| *b != b'/') {
216 Some(c)
241 Some(c)
217 } else {
242 } else {
218 None
243 None
219 }
244 }
220 })
245 })
221 .collect(),
246 .collect(),
222 )
247 )
223 }
248 }
224 fn matches_everything(&self) -> bool {
249 fn matches_everything(&self) -> bool {
225 false
250 false
226 }
251 }
227 fn is_exact(&self) -> bool {
252 fn is_exact(&self) -> bool {
228 true
253 true
229 }
254 }
230 }
255 }
231
256
232 /// Matches files that are included in the ignore rules.
257 /// Matches files that are included in the ignore rules.
233 /// ```
258 /// ```
234 /// use hg::{
259 /// use hg::{
235 /// matchers::{IncludeMatcher, Matcher},
260 /// matchers::{IncludeMatcher, Matcher},
236 /// IgnorePattern,
261 /// IgnorePattern,
237 /// PatternSyntax,
262 /// PatternSyntax,
238 /// utils::hg_path::HgPath
263 /// utils::hg_path::HgPath
239 /// };
264 /// };
240 /// use std::path::Path;
265 /// use std::path::Path;
241 /// ///
266 /// ///
242 /// let ignore_patterns =
267 /// let ignore_patterns =
243 /// vec![IgnorePattern::new(PatternSyntax::RootGlob, b"this*", Path::new(""))];
268 /// vec![IgnorePattern::new(PatternSyntax::RootGlob, b"this*", Path::new(""))];
244 /// let matcher = IncludeMatcher::new(ignore_patterns).unwrap();
269 /// let matcher = IncludeMatcher::new(ignore_patterns).unwrap();
245 /// ///
270 /// ///
246 /// assert_eq!(matcher.matches(HgPath::new(b"testing")), false);
271 /// assert_eq!(matcher.matches(HgPath::new(b"testing")), false);
247 /// assert_eq!(matcher.matches(HgPath::new(b"this should work")), true);
272 /// assert_eq!(matcher.matches(HgPath::new(b"this should work")), true);
248 /// assert_eq!(matcher.matches(HgPath::new(b"this also")), true);
273 /// assert_eq!(matcher.matches(HgPath::new(b"this also")), true);
249 /// assert_eq!(matcher.matches(HgPath::new(b"but not this")), false);
274 /// assert_eq!(matcher.matches(HgPath::new(b"but not this")), false);
250 /// ```
275 /// ```
251 pub struct IncludeMatcher<'a> {
276 pub struct IncludeMatcher<'a> {
252 patterns: Vec<u8>,
277 patterns: Vec<u8>,
253 match_fn: IgnoreFnType<'a>,
278 match_fn: IgnoreFnType<'a>,
254 /// Whether all the patterns match a prefix (i.e. recursively)
279 /// Whether all the patterns match a prefix (i.e. recursively)
255 prefix: bool,
280 prefix: bool,
256 roots: HashSet<HgPathBuf>,
281 roots: HashSet<HgPathBuf>,
257 dirs: HashSet<HgPathBuf>,
282 dirs: HashSet<HgPathBuf>,
258 parents: HashSet<HgPathBuf>,
283 parents: HashSet<HgPathBuf>,
259 }
284 }
260
285
261 impl<'a> Matcher for IncludeMatcher<'a> {
286 impl<'a> Matcher for IncludeMatcher<'a> {
262 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
287 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
263 None
288 None
264 }
289 }
265
290
266 fn exact_match(&self, _filename: &HgPath) -> bool {
291 fn exact_match(&self, _filename: &HgPath) -> bool {
267 false
292 false
268 }
293 }
269
294
270 fn matches(&self, filename: &HgPath) -> bool {
295 fn matches(&self, filename: &HgPath) -> bool {
271 (self.match_fn)(filename.as_ref())
296 (self.match_fn)(filename.as_ref())
272 }
297 }
273
298
274 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
299 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
275 let dir = directory.as_ref();
300 let dir = directory.as_ref();
276 if self.prefix && self.roots.contains(dir) {
301 if self.prefix && self.roots.contains(dir) {
277 return VisitChildrenSet::Recursive;
302 return VisitChildrenSet::Recursive;
278 }
303 }
279 if self.roots.contains(HgPath::new(b""))
304 if self.roots.contains(HgPath::new(b""))
280 || self.roots.contains(dir)
305 || self.roots.contains(dir)
281 || self.dirs.contains(dir)
306 || self.dirs.contains(dir)
282 || find_dirs(dir).any(|parent_dir| self.roots.contains(parent_dir))
307 || find_dirs(dir).any(|parent_dir| self.roots.contains(parent_dir))
283 {
308 {
284 return VisitChildrenSet::This;
309 return VisitChildrenSet::This;
285 }
310 }
286
311
287 if self.parents.contains(directory.as_ref()) {
312 if self.parents.contains(directory.as_ref()) {
288 let multiset = self.get_all_parents_children();
313 let multiset = self.get_all_parents_children();
289 if let Some(children) = multiset.get(dir) {
314 if let Some(children) = multiset.get(dir) {
290 return VisitChildrenSet::Set(
315 return VisitChildrenSet::Set(
291 children.into_iter().map(HgPathBuf::from).collect(),
316 children.into_iter().map(HgPathBuf::from).collect(),
292 );
317 );
293 }
318 }
294 }
319 }
295 VisitChildrenSet::Empty
320 VisitChildrenSet::Empty
296 }
321 }
297
322
298 fn matches_everything(&self) -> bool {
323 fn matches_everything(&self) -> bool {
299 false
324 false
300 }
325 }
301
326
302 fn is_exact(&self) -> bool {
327 fn is_exact(&self) -> bool {
303 false
328 false
304 }
329 }
305 }
330 }
306
331
307 /// The union of multiple matchers. Will match if any of the matchers match.
332 /// The union of multiple matchers. Will match if any of the matchers match.
308 pub struct UnionMatcher {
333 pub struct UnionMatcher {
309 matchers: Vec<Box<dyn Matcher + Sync>>,
334 matchers: Vec<Box<dyn Matcher + Sync>>,
310 }
335 }
311
336
312 impl Matcher for UnionMatcher {
337 impl Matcher for UnionMatcher {
313 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
338 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
314 None
339 None
315 }
340 }
316
341
317 fn exact_match(&self, _filename: &HgPath) -> bool {
342 fn exact_match(&self, _filename: &HgPath) -> bool {
318 false
343 false
319 }
344 }
320
345
321 fn matches(&self, filename: &HgPath) -> bool {
346 fn matches(&self, filename: &HgPath) -> bool {
322 self.matchers.iter().any(|m| m.matches(filename))
347 self.matchers.iter().any(|m| m.matches(filename))
323 }
348 }
324
349
325 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
350 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
326 let mut result = HashSet::new();
351 let mut result = HashSet::new();
327 let mut this = false;
352 let mut this = false;
328 for matcher in self.matchers.iter() {
353 for matcher in self.matchers.iter() {
329 let visit = matcher.visit_children_set(directory);
354 let visit = matcher.visit_children_set(directory);
330 match visit {
355 match visit {
331 VisitChildrenSet::Empty => continue,
356 VisitChildrenSet::Empty => continue,
332 VisitChildrenSet::This => {
357 VisitChildrenSet::This => {
333 this = true;
358 this = true;
334 // Don't break, we might have an 'all' in here.
359 // Don't break, we might have an 'all' in here.
335 continue;
360 continue;
336 }
361 }
337 VisitChildrenSet::Set(set) => {
362 VisitChildrenSet::Set(set) => {
338 result.extend(set);
363 result.extend(set);
339 }
364 }
340 VisitChildrenSet::Recursive => {
365 VisitChildrenSet::Recursive => {
341 return visit;
366 return visit;
342 }
367 }
343 }
368 }
344 }
369 }
345 if this {
370 if this {
346 return VisitChildrenSet::This;
371 return VisitChildrenSet::This;
347 }
372 }
348 if result.is_empty() {
373 if result.is_empty() {
349 VisitChildrenSet::Empty
374 VisitChildrenSet::Empty
350 } else {
375 } else {
351 VisitChildrenSet::Set(result)
376 VisitChildrenSet::Set(result)
352 }
377 }
353 }
378 }
354
379
355 fn matches_everything(&self) -> bool {
380 fn matches_everything(&self) -> bool {
356 // TODO Maybe if all are AlwaysMatcher?
381 // TODO Maybe if all are AlwaysMatcher?
357 false
382 false
358 }
383 }
359
384
360 fn is_exact(&self) -> bool {
385 fn is_exact(&self) -> bool {
361 false
386 false
362 }
387 }
363 }
388 }
364
389
365 impl UnionMatcher {
390 impl UnionMatcher {
366 pub fn new(matchers: Vec<Box<dyn Matcher + Sync>>) -> Self {
391 pub fn new(matchers: Vec<Box<dyn Matcher + Sync>>) -> Self {
367 Self { matchers }
392 Self { matchers }
368 }
393 }
369 }
394 }
370
395
371 pub struct IntersectionMatcher {
396 pub struct IntersectionMatcher {
372 m1: Box<dyn Matcher + Sync>,
397 m1: Box<dyn Matcher + Sync>,
373 m2: Box<dyn Matcher + Sync>,
398 m2: Box<dyn Matcher + Sync>,
374 files: Option<HashSet<HgPathBuf>>,
399 files: Option<HashSet<HgPathBuf>>,
375 }
400 }
376
401
377 impl Matcher for IntersectionMatcher {
402 impl Matcher for IntersectionMatcher {
378 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
403 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
379 self.files.as_ref()
404 self.files.as_ref()
380 }
405 }
381
406
382 fn exact_match(&self, filename: &HgPath) -> bool {
407 fn exact_match(&self, filename: &HgPath) -> bool {
383 self.files.as_ref().map_or(false, |f| f.contains(filename))
408 self.files.as_ref().map_or(false, |f| f.contains(filename))
384 }
409 }
385
410
386 fn matches(&self, filename: &HgPath) -> bool {
411 fn matches(&self, filename: &HgPath) -> bool {
387 self.m1.matches(filename) && self.m2.matches(filename)
412 self.m1.matches(filename) && self.m2.matches(filename)
388 }
413 }
389
414
390 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
415 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
391 let m1_set = self.m1.visit_children_set(directory);
416 let m1_set = self.m1.visit_children_set(directory);
392 if m1_set == VisitChildrenSet::Empty {
417 if m1_set == VisitChildrenSet::Empty {
393 return VisitChildrenSet::Empty;
418 return VisitChildrenSet::Empty;
394 }
419 }
395 let m2_set = self.m2.visit_children_set(directory);
420 let m2_set = self.m2.visit_children_set(directory);
396 if m2_set == VisitChildrenSet::Empty {
421 if m2_set == VisitChildrenSet::Empty {
397 return VisitChildrenSet::Empty;
422 return VisitChildrenSet::Empty;
398 }
423 }
399
424
400 if m1_set == VisitChildrenSet::Recursive {
425 if m1_set == VisitChildrenSet::Recursive {
401 return m2_set;
426 return m2_set;
402 } else if m2_set == VisitChildrenSet::Recursive {
427 } else if m2_set == VisitChildrenSet::Recursive {
403 return m1_set;
428 return m1_set;
404 }
429 }
405
430
406 match (&m1_set, &m2_set) {
431 match (&m1_set, &m2_set) {
407 (VisitChildrenSet::Recursive, _) => m2_set,
432 (VisitChildrenSet::Recursive, _) => m2_set,
408 (_, VisitChildrenSet::Recursive) => m1_set,
433 (_, VisitChildrenSet::Recursive) => m1_set,
409 (VisitChildrenSet::This, _) | (_, VisitChildrenSet::This) => {
434 (VisitChildrenSet::This, _) | (_, VisitChildrenSet::This) => {
410 VisitChildrenSet::This
435 VisitChildrenSet::This
411 }
436 }
412 (VisitChildrenSet::Set(m1), VisitChildrenSet::Set(m2)) => {
437 (VisitChildrenSet::Set(m1), VisitChildrenSet::Set(m2)) => {
413 let set: HashSet<_> = m1.intersection(&m2).cloned().collect();
438 let set: HashSet<_> = m1.intersection(&m2).cloned().collect();
414 if set.is_empty() {
439 if set.is_empty() {
415 VisitChildrenSet::Empty
440 VisitChildrenSet::Empty
416 } else {
441 } else {
417 VisitChildrenSet::Set(set)
442 VisitChildrenSet::Set(set)
418 }
443 }
419 }
444 }
420 _ => unreachable!(),
445 _ => unreachable!(),
421 }
446 }
422 }
447 }
423
448
424 fn matches_everything(&self) -> bool {
449 fn matches_everything(&self) -> bool {
425 self.m1.matches_everything() && self.m2.matches_everything()
450 self.m1.matches_everything() && self.m2.matches_everything()
426 }
451 }
427
452
428 fn is_exact(&self) -> bool {
453 fn is_exact(&self) -> bool {
429 self.m1.is_exact() || self.m2.is_exact()
454 self.m1.is_exact() || self.m2.is_exact()
430 }
455 }
431 }
456 }
432
457
433 impl IntersectionMatcher {
458 impl IntersectionMatcher {
434 pub fn new(
459 pub fn new(
435 mut m1: Box<dyn Matcher + Sync>,
460 mut m1: Box<dyn Matcher + Sync>,
436 mut m2: Box<dyn Matcher + Sync>,
461 mut m2: Box<dyn Matcher + Sync>,
437 ) -> Self {
462 ) -> Self {
438 let files = if m1.is_exact() || m2.is_exact() {
463 let files = if m1.is_exact() || m2.is_exact() {
439 if !m1.is_exact() {
464 if !m1.is_exact() {
440 std::mem::swap(&mut m1, &mut m2);
465 std::mem::swap(&mut m1, &mut m2);
441 }
466 }
442 m1.file_set().map(|m1_files| {
467 m1.file_set().map(|m1_files| {
443 m1_files.iter().cloned().filter(|f| m2.matches(f)).collect()
468 m1_files.iter().cloned().filter(|f| m2.matches(f)).collect()
444 })
469 })
445 } else {
470 } else {
446 None
471 None
447 };
472 };
448 Self { m1, m2, files }
473 Self { m1, m2, files }
449 }
474 }
450 }
475 }
451
476
452 /// Returns a function that matches an `HgPath` against the given regex
477 /// Returns a function that matches an `HgPath` against the given regex
453 /// pattern.
478 /// pattern.
454 ///
479 ///
455 /// This can fail when the pattern is invalid or not supported by the
480 /// This can fail when the pattern is invalid or not supported by the
456 /// underlying engine (the `regex` crate), for instance anything with
481 /// underlying engine (the `regex` crate), for instance anything with
457 /// back-references.
482 /// back-references.
458 #[timed]
483 #[timed]
459 fn re_matcher(
484 fn re_matcher(
460 pattern: &[u8],
485 pattern: &[u8],
461 ) -> PatternResult<impl Fn(&HgPath) -> bool + Sync> {
486 ) -> PatternResult<impl Fn(&HgPath) -> bool + Sync> {
462 use std::io::Write;
487 use std::io::Write;
463
488
464 // The `regex` crate adds `.*` to the start and end of expressions if there
489 // The `regex` crate adds `.*` to the start and end of expressions if there
465 // are no anchors, so add the start anchor.
490 // are no anchors, so add the start anchor.
466 let mut escaped_bytes = vec![b'^', b'(', b'?', b':'];
491 let mut escaped_bytes = vec![b'^', b'(', b'?', b':'];
467 for byte in pattern {
492 for byte in pattern {
468 if *byte > 127 {
493 if *byte > 127 {
469 write!(escaped_bytes, "\\x{:x}", *byte).unwrap();
494 write!(escaped_bytes, "\\x{:x}", *byte).unwrap();
470 } else {
495 } else {
471 escaped_bytes.push(*byte);
496 escaped_bytes.push(*byte);
472 }
497 }
473 }
498 }
474 escaped_bytes.push(b')');
499 escaped_bytes.push(b')');
475
500
476 // Avoid the cost of UTF8 checking
501 // Avoid the cost of UTF8 checking
477 //
502 //
478 // # Safety
503 // # Safety
479 // This is safe because we escaped all non-ASCII bytes.
504 // This is safe because we escaped all non-ASCII bytes.
480 let pattern_string = unsafe { String::from_utf8_unchecked(escaped_bytes) };
505 let pattern_string = unsafe { String::from_utf8_unchecked(escaped_bytes) };
481 let re = regex::bytes::RegexBuilder::new(&pattern_string)
506 let re = regex::bytes::RegexBuilder::new(&pattern_string)
482 .unicode(false)
507 .unicode(false)
483 // Big repos with big `.hgignore` will hit the default limit and
508 // Big repos with big `.hgignore` will hit the default limit and
484 // incur a significant performance hit. One repo's `hg status` hit
509 // incur a significant performance hit. One repo's `hg status` hit
485 // multiple *minutes*.
510 // multiple *minutes*.
486 .dfa_size_limit(50 * (1 << 20))
511 .dfa_size_limit(50 * (1 << 20))
487 .build()
512 .build()
488 .map_err(|e| PatternError::UnsupportedSyntax(e.to_string()))?;
513 .map_err(|e| PatternError::UnsupportedSyntax(e.to_string()))?;
489
514
490 Ok(move |path: &HgPath| re.is_match(path.as_bytes()))
515 Ok(move |path: &HgPath| re.is_match(path.as_bytes()))
491 }
516 }
492
517
493 /// Returns the regex pattern and a function that matches an `HgPath` against
518 /// Returns the regex pattern and a function that matches an `HgPath` against
494 /// said regex formed by the given ignore patterns.
519 /// said regex formed by the given ignore patterns.
495 fn build_regex_match<'a, 'b>(
520 fn build_regex_match<'a, 'b>(
496 ignore_patterns: &'a [IgnorePattern],
521 ignore_patterns: &'a [IgnorePattern],
497 ) -> PatternResult<(Vec<u8>, IgnoreFnType<'b>)> {
522 ) -> PatternResult<(Vec<u8>, IgnoreFnType<'b>)> {
498 let mut regexps = vec![];
523 let mut regexps = vec![];
499 let mut exact_set = HashSet::new();
524 let mut exact_set = HashSet::new();
500
525
501 for pattern in ignore_patterns {
526 for pattern in ignore_patterns {
502 if let Some(re) = build_single_regex(pattern)? {
527 if let Some(re) = build_single_regex(pattern)? {
503 regexps.push(re);
528 regexps.push(re);
504 } else {
529 } else {
505 let exact = normalize_path_bytes(&pattern.pattern);
530 let exact = normalize_path_bytes(&pattern.pattern);
506 exact_set.insert(HgPathBuf::from_bytes(&exact));
531 exact_set.insert(HgPathBuf::from_bytes(&exact));
507 }
532 }
508 }
533 }
509
534
510 let full_regex = regexps.join(&b'|');
535 let full_regex = regexps.join(&b'|');
511
536
512 // An empty pattern would cause the regex engine to incorrectly match the
537 // An empty pattern would cause the regex engine to incorrectly match the
513 // (empty) root directory
538 // (empty) root directory
514 let func = if !(regexps.is_empty()) {
539 let func = if !(regexps.is_empty()) {
515 let matcher = re_matcher(&full_regex)?;
540 let matcher = re_matcher(&full_regex)?;
516 let func = move |filename: &HgPath| {
541 let func = move |filename: &HgPath| {
517 exact_set.contains(filename) || matcher(filename)
542 exact_set.contains(filename) || matcher(filename)
518 };
543 };
519 Box::new(func) as IgnoreFnType
544 Box::new(func) as IgnoreFnType
520 } else {
545 } else {
521 let func = move |filename: &HgPath| exact_set.contains(filename);
546 let func = move |filename: &HgPath| exact_set.contains(filename);
522 Box::new(func) as IgnoreFnType
547 Box::new(func) as IgnoreFnType
523 };
548 };
524
549
525 Ok((full_regex, func))
550 Ok((full_regex, func))
526 }
551 }
527
552
528 /// Returns roots and directories corresponding to each pattern.
553 /// Returns roots and directories corresponding to each pattern.
529 ///
554 ///
530 /// This calculates the roots and directories exactly matching the patterns and
555 /// This calculates the roots and directories exactly matching the patterns and
531 /// returns a tuple of (roots, dirs). It does not return other directories
556 /// returns a tuple of (roots, dirs). It does not return other directories
532 /// which may also need to be considered, like the parent directories.
557 /// which may also need to be considered, like the parent directories.
533 fn roots_and_dirs(
558 fn roots_and_dirs(
534 ignore_patterns: &[IgnorePattern],
559 ignore_patterns: &[IgnorePattern],
535 ) -> (Vec<HgPathBuf>, Vec<HgPathBuf>) {
560 ) -> (Vec<HgPathBuf>, Vec<HgPathBuf>) {
536 let mut roots = Vec::new();
561 let mut roots = Vec::new();
537 let mut dirs = Vec::new();
562 let mut dirs = Vec::new();
538
563
539 for ignore_pattern in ignore_patterns {
564 for ignore_pattern in ignore_patterns {
540 let IgnorePattern {
565 let IgnorePattern {
541 syntax, pattern, ..
566 syntax, pattern, ..
542 } = ignore_pattern;
567 } = ignore_pattern;
543 match syntax {
568 match syntax {
544 PatternSyntax::RootGlob | PatternSyntax::Glob => {
569 PatternSyntax::RootGlob | PatternSyntax::Glob => {
545 let mut root = HgPathBuf::new();
570 let mut root = HgPathBuf::new();
546 for p in pattern.split(|c| *c == b'/') {
571 for p in pattern.split(|c| *c == b'/') {
547 if p.iter().any(|c| match *c {
572 if p.iter().any(|c| match *c {
548 b'[' | b'{' | b'*' | b'?' => true,
573 b'[' | b'{' | b'*' | b'?' => true,
549 _ => false,
574 _ => false,
550 }) {
575 }) {
551 break;
576 break;
552 }
577 }
553 root.push(HgPathBuf::from_bytes(p).as_ref());
578 root.push(HgPathBuf::from_bytes(p).as_ref());
554 }
579 }
555 roots.push(root);
580 roots.push(root);
556 }
581 }
557 PatternSyntax::Path | PatternSyntax::RelPath => {
582 PatternSyntax::Path | PatternSyntax::RelPath => {
558 let pat = HgPath::new(if pattern == b"." {
583 let pat = HgPath::new(if pattern == b"." {
559 &[] as &[u8]
584 &[] as &[u8]
560 } else {
585 } else {
561 pattern
586 pattern
562 });
587 });
563 roots.push(pat.to_owned());
588 roots.push(pat.to_owned());
564 }
589 }
565 PatternSyntax::RootFiles => {
590 PatternSyntax::RootFiles => {
566 let pat = if pattern == b"." {
591 let pat = if pattern == b"." {
567 &[] as &[u8]
592 &[] as &[u8]
568 } else {
593 } else {
569 pattern
594 pattern
570 };
595 };
571 dirs.push(HgPathBuf::from_bytes(pat));
596 dirs.push(HgPathBuf::from_bytes(pat));
572 }
597 }
573 _ => {
598 _ => {
574 roots.push(HgPathBuf::new());
599 roots.push(HgPathBuf::new());
575 }
600 }
576 }
601 }
577 }
602 }
578 (roots, dirs)
603 (roots, dirs)
579 }
604 }
580
605
581 /// Paths extracted from patterns
606 /// Paths extracted from patterns
582 #[derive(Debug, PartialEq)]
607 #[derive(Debug, PartialEq)]
583 struct RootsDirsAndParents {
608 struct RootsDirsAndParents {
584 /// Directories to match recursively
609 /// Directories to match recursively
585 pub roots: HashSet<HgPathBuf>,
610 pub roots: HashSet<HgPathBuf>,
586 /// Directories to match non-recursively
611 /// Directories to match non-recursively
587 pub dirs: HashSet<HgPathBuf>,
612 pub dirs: HashSet<HgPathBuf>,
588 /// Implicitly required directories to go to items in either roots or dirs
613 /// Implicitly required directories to go to items in either roots or dirs
589 pub parents: HashSet<HgPathBuf>,
614 pub parents: HashSet<HgPathBuf>,
590 }
615 }
591
616
592 /// Extract roots, dirs and parents from patterns.
617 /// Extract roots, dirs and parents from patterns.
593 fn roots_dirs_and_parents(
618 fn roots_dirs_and_parents(
594 ignore_patterns: &[IgnorePattern],
619 ignore_patterns: &[IgnorePattern],
595 ) -> PatternResult<RootsDirsAndParents> {
620 ) -> PatternResult<RootsDirsAndParents> {
596 let (roots, dirs) = roots_and_dirs(ignore_patterns);
621 let (roots, dirs) = roots_and_dirs(ignore_patterns);
597
622
598 let mut parents = HashSet::new();
623 let mut parents = HashSet::new();
599
624
600 parents.extend(
625 parents.extend(
601 DirsMultiset::from_manifest(&dirs)
626 DirsMultiset::from_manifest(&dirs)
602 .map_err(|e| match e {
627 .map_err(|e| match e {
603 DirstateMapError::InvalidPath(e) => e,
628 DirstateMapError::InvalidPath(e) => e,
604 _ => unreachable!(),
629 _ => unreachable!(),
605 })?
630 })?
606 .iter()
631 .iter()
607 .map(ToOwned::to_owned),
632 .map(ToOwned::to_owned),
608 );
633 );
609 parents.extend(
634 parents.extend(
610 DirsMultiset::from_manifest(&roots)
635 DirsMultiset::from_manifest(&roots)
611 .map_err(|e| match e {
636 .map_err(|e| match e {
612 DirstateMapError::InvalidPath(e) => e,
637 DirstateMapError::InvalidPath(e) => e,
613 _ => unreachable!(),
638 _ => unreachable!(),
614 })?
639 })?
615 .iter()
640 .iter()
616 .map(ToOwned::to_owned),
641 .map(ToOwned::to_owned),
617 );
642 );
618
643
619 Ok(RootsDirsAndParents {
644 Ok(RootsDirsAndParents {
620 roots: HashSet::from_iter(roots),
645 roots: HashSet::from_iter(roots),
621 dirs: HashSet::from_iter(dirs),
646 dirs: HashSet::from_iter(dirs),
622 parents,
647 parents,
623 })
648 })
624 }
649 }
625
650
626 /// Returns a function that checks whether a given file (in the general sense)
651 /// Returns a function that checks whether a given file (in the general sense)
627 /// should be matched.
652 /// should be matched.
628 fn build_match<'a, 'b>(
653 fn build_match<'a, 'b>(
629 ignore_patterns: Vec<IgnorePattern>,
654 ignore_patterns: Vec<IgnorePattern>,
630 ) -> PatternResult<(Vec<u8>, IgnoreFnType<'b>)> {
655 ) -> PatternResult<(Vec<u8>, IgnoreFnType<'b>)> {
631 let mut match_funcs: Vec<IgnoreFnType<'b>> = vec![];
656 let mut match_funcs: Vec<IgnoreFnType<'b>> = vec![];
632 // For debugging and printing
657 // For debugging and printing
633 let mut patterns = vec![];
658 let mut patterns = vec![];
634
659
635 let (subincludes, ignore_patterns) = filter_subincludes(ignore_patterns)?;
660 let (subincludes, ignore_patterns) = filter_subincludes(ignore_patterns)?;
636
661
637 if !subincludes.is_empty() {
662 if !subincludes.is_empty() {
638 // Build prefix-based matcher functions for subincludes
663 // Build prefix-based matcher functions for subincludes
639 let mut submatchers = FastHashMap::default();
664 let mut submatchers = FastHashMap::default();
640 let mut prefixes = vec![];
665 let mut prefixes = vec![];
641
666
642 for sub_include in subincludes {
667 for sub_include in subincludes {
643 let matcher = IncludeMatcher::new(sub_include.included_patterns)?;
668 let matcher = IncludeMatcher::new(sub_include.included_patterns)?;
644 let match_fn =
669 let match_fn =
645 Box::new(move |path: &HgPath| matcher.matches(path));
670 Box::new(move |path: &HgPath| matcher.matches(path));
646 prefixes.push(sub_include.prefix.clone());
671 prefixes.push(sub_include.prefix.clone());
647 submatchers.insert(sub_include.prefix.clone(), match_fn);
672 submatchers.insert(sub_include.prefix.clone(), match_fn);
648 }
673 }
649
674
650 let match_subinclude = move |filename: &HgPath| {
675 let match_subinclude = move |filename: &HgPath| {
651 for prefix in prefixes.iter() {
676 for prefix in prefixes.iter() {
652 if let Some(rel) = filename.relative_to(prefix) {
677 if let Some(rel) = filename.relative_to(prefix) {
653 if (submatchers[prefix])(rel) {
678 if (submatchers[prefix])(rel) {
654 return true;
679 return true;
655 }
680 }
656 }
681 }
657 }
682 }
658 false
683 false
659 };
684 };
660
685
661 match_funcs.push(Box::new(match_subinclude));
686 match_funcs.push(Box::new(match_subinclude));
662 }
687 }
663
688
664 if !ignore_patterns.is_empty() {
689 if !ignore_patterns.is_empty() {
665 // Either do dumb matching if all patterns are rootfiles, or match
690 // Either do dumb matching if all patterns are rootfiles, or match
666 // with a regex.
691 // with a regex.
667 if ignore_patterns
692 if ignore_patterns
668 .iter()
693 .iter()
669 .all(|k| k.syntax == PatternSyntax::RootFiles)
694 .all(|k| k.syntax == PatternSyntax::RootFiles)
670 {
695 {
671 let dirs: HashSet<_> = ignore_patterns
696 let dirs: HashSet<_> = ignore_patterns
672 .iter()
697 .iter()
673 .map(|k| k.pattern.to_owned())
698 .map(|k| k.pattern.to_owned())
674 .collect();
699 .collect();
675 let mut dirs_vec: Vec<_> = dirs.iter().cloned().collect();
700 let mut dirs_vec: Vec<_> = dirs.iter().cloned().collect();
676
701
677 let match_func = move |path: &HgPath| -> bool {
702 let match_func = move |path: &HgPath| -> bool {
678 let path = path.as_bytes();
703 let path = path.as_bytes();
679 let i = path.iter().rfind(|a| **a == b'/');
704 let i = path.iter().rfind(|a| **a == b'/');
680 let dir = if let Some(i) = i {
705 let dir = if let Some(i) = i {
681 &path[..*i as usize]
706 &path[..*i as usize]
682 } else {
707 } else {
683 b"."
708 b"."
684 };
709 };
685 dirs.contains(dir.deref())
710 dirs.contains(dir.deref())
686 };
711 };
687 match_funcs.push(Box::new(match_func));
712 match_funcs.push(Box::new(match_func));
688
713
689 patterns.extend(b"rootfilesin: ");
714 patterns.extend(b"rootfilesin: ");
690 dirs_vec.sort();
715 dirs_vec.sort();
691 patterns.extend(dirs_vec.escaped_bytes());
716 patterns.extend(dirs_vec.escaped_bytes());
692 } else {
717 } else {
693 let (new_re, match_func) = build_regex_match(&ignore_patterns)?;
718 let (new_re, match_func) = build_regex_match(&ignore_patterns)?;
694 patterns = new_re;
719 patterns = new_re;
695 match_funcs.push(match_func)
720 match_funcs.push(match_func)
696 }
721 }
697 }
722 }
698
723
699 Ok(if match_funcs.len() == 1 {
724 Ok(if match_funcs.len() == 1 {
700 (patterns, match_funcs.remove(0))
725 (patterns, match_funcs.remove(0))
701 } else {
726 } else {
702 (
727 (
703 patterns,
728 patterns,
704 Box::new(move |f: &HgPath| -> bool {
729 Box::new(move |f: &HgPath| -> bool {
705 match_funcs.iter().any(|match_func| match_func(f))
730 match_funcs.iter().any(|match_func| match_func(f))
706 }),
731 }),
707 )
732 )
708 })
733 })
709 }
734 }
710
735
711 /// Parses all "ignore" files with their recursive includes and returns a
736 /// Parses all "ignore" files with their recursive includes and returns a
712 /// function that checks whether a given file (in the general sense) should be
737 /// function that checks whether a given file (in the general sense) should be
713 /// ignored.
738 /// ignored.
714 pub fn get_ignore_matcher<'a>(
739 pub fn get_ignore_matcher<'a>(
715 mut all_pattern_files: Vec<PathBuf>,
740 mut all_pattern_files: Vec<PathBuf>,
716 root_dir: &Path,
741 root_dir: &Path,
717 inspect_pattern_bytes: &mut impl FnMut(&[u8]),
742 inspect_pattern_bytes: &mut impl FnMut(&[u8]),
718 ) -> PatternResult<(IncludeMatcher<'a>, Vec<PatternFileWarning>)> {
743 ) -> PatternResult<(IncludeMatcher<'a>, Vec<PatternFileWarning>)> {
719 let mut all_patterns = vec![];
744 let mut all_patterns = vec![];
720 let mut all_warnings = vec![];
745 let mut all_warnings = vec![];
721
746
722 // Sort to make the ordering of calls to `inspect_pattern_bytes`
747 // Sort to make the ordering of calls to `inspect_pattern_bytes`
723 // deterministic even if the ordering of `all_pattern_files` is not (such
748 // deterministic even if the ordering of `all_pattern_files` is not (such
724 // as when a iteration order of a Python dict or Rust HashMap is involved).
749 // as when a iteration order of a Python dict or Rust HashMap is involved).
725 // Sort by "string" representation instead of the default by component
750 // Sort by "string" representation instead of the default by component
726 // (with a Rust-specific definition of a component)
751 // (with a Rust-specific definition of a component)
727 all_pattern_files
752 all_pattern_files
728 .sort_unstable_by(|a, b| a.as_os_str().cmp(b.as_os_str()));
753 .sort_unstable_by(|a, b| a.as_os_str().cmp(b.as_os_str()));
729
754
730 for pattern_file in &all_pattern_files {
755 for pattern_file in &all_pattern_files {
731 let (patterns, warnings) = get_patterns_from_file(
756 let (patterns, warnings) = get_patterns_from_file(
732 pattern_file,
757 pattern_file,
733 root_dir,
758 root_dir,
734 inspect_pattern_bytes,
759 inspect_pattern_bytes,
735 )?;
760 )?;
736
761
737 all_patterns.extend(patterns.to_owned());
762 all_patterns.extend(patterns.to_owned());
738 all_warnings.extend(warnings);
763 all_warnings.extend(warnings);
739 }
764 }
740 let matcher = IncludeMatcher::new(all_patterns)?;
765 let matcher = IncludeMatcher::new(all_patterns)?;
741 Ok((matcher, all_warnings))
766 Ok((matcher, all_warnings))
742 }
767 }
743
768
744 /// Parses all "ignore" files with their recursive includes and returns a
769 /// Parses all "ignore" files with their recursive includes and returns a
745 /// function that checks whether a given file (in the general sense) should be
770 /// function that checks whether a given file (in the general sense) should be
746 /// ignored.
771 /// ignored.
747 pub fn get_ignore_function<'a>(
772 pub fn get_ignore_function<'a>(
748 all_pattern_files: Vec<PathBuf>,
773 all_pattern_files: Vec<PathBuf>,
749 root_dir: &Path,
774 root_dir: &Path,
750 inspect_pattern_bytes: &mut impl FnMut(&[u8]),
775 inspect_pattern_bytes: &mut impl FnMut(&[u8]),
751 ) -> PatternResult<(IgnoreFnType<'a>, Vec<PatternFileWarning>)> {
776 ) -> PatternResult<(IgnoreFnType<'a>, Vec<PatternFileWarning>)> {
752 let res =
777 let res =
753 get_ignore_matcher(all_pattern_files, root_dir, inspect_pattern_bytes);
778 get_ignore_matcher(all_pattern_files, root_dir, inspect_pattern_bytes);
754 res.map(|(matcher, all_warnings)| {
779 res.map(|(matcher, all_warnings)| {
755 let res: IgnoreFnType<'a> =
780 let res: IgnoreFnType<'a> =
756 Box::new(move |path: &HgPath| matcher.matches(path));
781 Box::new(move |path: &HgPath| matcher.matches(path));
757
782
758 (res, all_warnings)
783 (res, all_warnings)
759 })
784 })
760 }
785 }
761
786
762 impl<'a> IncludeMatcher<'a> {
787 impl<'a> IncludeMatcher<'a> {
763 pub fn new(ignore_patterns: Vec<IgnorePattern>) -> PatternResult<Self> {
788 pub fn new(ignore_patterns: Vec<IgnorePattern>) -> PatternResult<Self> {
764 let RootsDirsAndParents {
789 let RootsDirsAndParents {
765 roots,
790 roots,
766 dirs,
791 dirs,
767 parents,
792 parents,
768 } = roots_dirs_and_parents(&ignore_patterns)?;
793 } = roots_dirs_and_parents(&ignore_patterns)?;
769 let prefix = ignore_patterns.iter().any(|k| match k.syntax {
794 let prefix = ignore_patterns.iter().any(|k| match k.syntax {
770 PatternSyntax::Path | PatternSyntax::RelPath => true,
795 PatternSyntax::Path | PatternSyntax::RelPath => true,
771 _ => false,
796 _ => false,
772 });
797 });
773 let (patterns, match_fn) = build_match(ignore_patterns)?;
798 let (patterns, match_fn) = build_match(ignore_patterns)?;
774
799
775 Ok(Self {
800 Ok(Self {
776 patterns,
801 patterns,
777 match_fn,
802 match_fn,
778 prefix,
803 prefix,
779 roots,
804 roots,
780 dirs,
805 dirs,
781 parents,
806 parents,
782 })
807 })
783 }
808 }
784
809
785 fn get_all_parents_children(&self) -> DirsChildrenMultiset {
810 fn get_all_parents_children(&self) -> DirsChildrenMultiset {
786 // TODO cache
811 // TODO cache
787 let thing = self
812 let thing = self
788 .dirs
813 .dirs
789 .iter()
814 .iter()
790 .chain(self.roots.iter())
815 .chain(self.roots.iter())
791 .chain(self.parents.iter());
816 .chain(self.parents.iter());
792 DirsChildrenMultiset::new(thing, Some(&self.parents))
817 DirsChildrenMultiset::new(thing, Some(&self.parents))
793 }
818 }
794
819
795 pub fn debug_get_patterns(&self) -> &[u8] {
820 pub fn debug_get_patterns(&self) -> &[u8] {
796 self.patterns.as_ref()
821 self.patterns.as_ref()
797 }
822 }
798 }
823 }
799
824
800 impl<'a> Display for IncludeMatcher<'a> {
825 impl<'a> Display for IncludeMatcher<'a> {
801 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
826 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
802 // XXX What about exact matches?
827 // XXX What about exact matches?
803 // I'm not sure it's worth it to clone the HashSet and keep it
828 // I'm not sure it's worth it to clone the HashSet and keep it
804 // around just in case someone wants to display the matcher, plus
829 // around just in case someone wants to display the matcher, plus
805 // it's going to be unreadable after a few entries, but we need to
830 // it's going to be unreadable after a few entries, but we need to
806 // inform in this display that exact matches are being used and are
831 // inform in this display that exact matches are being used and are
807 // (on purpose) missing from the `includes`.
832 // (on purpose) missing from the `includes`.
808 write!(
833 write!(
809 f,
834 f,
810 "IncludeMatcher(includes='{}')",
835 "IncludeMatcher(includes='{}')",
811 String::from_utf8_lossy(&self.patterns.escaped_bytes())
836 String::from_utf8_lossy(&self.patterns.escaped_bytes())
812 )
837 )
813 }
838 }
814 }
839 }
815
840
816 #[cfg(test)]
841 #[cfg(test)]
817 mod tests {
842 mod tests {
818 use super::*;
843 use super::*;
819 use pretty_assertions::assert_eq;
844 use pretty_assertions::assert_eq;
820 use std::path::Path;
845 use std::path::Path;
821
846
822 #[test]
847 #[test]
823 fn test_roots_and_dirs() {
848 fn test_roots_and_dirs() {
824 let pats = vec![
849 let pats = vec![
825 IgnorePattern::new(PatternSyntax::Glob, b"g/h/*", Path::new("")),
850 IgnorePattern::new(PatternSyntax::Glob, b"g/h/*", Path::new("")),
826 IgnorePattern::new(PatternSyntax::Glob, b"g/h", Path::new("")),
851 IgnorePattern::new(PatternSyntax::Glob, b"g/h", Path::new("")),
827 IgnorePattern::new(PatternSyntax::Glob, b"g*", Path::new("")),
852 IgnorePattern::new(PatternSyntax::Glob, b"g*", Path::new("")),
828 ];
853 ];
829 let (roots, dirs) = roots_and_dirs(&pats);
854 let (roots, dirs) = roots_and_dirs(&pats);
830
855
831 assert_eq!(
856 assert_eq!(
832 roots,
857 roots,
833 vec!(
858 vec!(
834 HgPathBuf::from_bytes(b"g/h"),
859 HgPathBuf::from_bytes(b"g/h"),
835 HgPathBuf::from_bytes(b"g/h"),
860 HgPathBuf::from_bytes(b"g/h"),
836 HgPathBuf::new()
861 HgPathBuf::new()
837 ),
862 ),
838 );
863 );
839 assert_eq!(dirs, vec!());
864 assert_eq!(dirs, vec!());
840 }
865 }
841
866
842 #[test]
867 #[test]
843 fn test_roots_dirs_and_parents() {
868 fn test_roots_dirs_and_parents() {
844 let pats = vec![
869 let pats = vec![
845 IgnorePattern::new(PatternSyntax::Glob, b"g/h/*", Path::new("")),
870 IgnorePattern::new(PatternSyntax::Glob, b"g/h/*", Path::new("")),
846 IgnorePattern::new(PatternSyntax::Glob, b"g/h", Path::new("")),
871 IgnorePattern::new(PatternSyntax::Glob, b"g/h", Path::new("")),
847 IgnorePattern::new(PatternSyntax::Glob, b"g*", Path::new("")),
872 IgnorePattern::new(PatternSyntax::Glob, b"g*", Path::new("")),
848 ];
873 ];
849
874
850 let mut roots = HashSet::new();
875 let mut roots = HashSet::new();
851 roots.insert(HgPathBuf::from_bytes(b"g/h"));
876 roots.insert(HgPathBuf::from_bytes(b"g/h"));
852 roots.insert(HgPathBuf::new());
877 roots.insert(HgPathBuf::new());
853
878
854 let dirs = HashSet::new();
879 let dirs = HashSet::new();
855
880
856 let mut parents = HashSet::new();
881 let mut parents = HashSet::new();
857 parents.insert(HgPathBuf::new());
882 parents.insert(HgPathBuf::new());
858 parents.insert(HgPathBuf::from_bytes(b"g"));
883 parents.insert(HgPathBuf::from_bytes(b"g"));
859
884
860 assert_eq!(
885 assert_eq!(
861 roots_dirs_and_parents(&pats).unwrap(),
886 roots_dirs_and_parents(&pats).unwrap(),
862 RootsDirsAndParents {
887 RootsDirsAndParents {
863 roots,
888 roots,
864 dirs,
889 dirs,
865 parents
890 parents
866 }
891 }
867 );
892 );
868 }
893 }
869
894
870 #[test]
895 #[test]
871 fn test_filematcher_visit_children_set() {
896 fn test_filematcher_visit_children_set() {
872 // Visitchildrenset
897 // Visitchildrenset
873 let files = vec![HgPathBuf::from_bytes(b"dir/subdir/foo.txt")];
898 let files = vec![HgPathBuf::from_bytes(b"dir/subdir/foo.txt")];
874 let matcher = FileMatcher::new(files).unwrap();
899 let matcher = FileMatcher::new(files).unwrap();
875
900
876 let mut set = HashSet::new();
901 let mut set = HashSet::new();
877 set.insert(HgPathBuf::from_bytes(b"dir"));
902 set.insert(HgPathBuf::from_bytes(b"dir"));
878 assert_eq!(
903 assert_eq!(
879 matcher.visit_children_set(HgPath::new(b"")),
904 matcher.visit_children_set(HgPath::new(b"")),
880 VisitChildrenSet::Set(set)
905 VisitChildrenSet::Set(set)
881 );
906 );
882
907
883 let mut set = HashSet::new();
908 let mut set = HashSet::new();
884 set.insert(HgPathBuf::from_bytes(b"subdir"));
909 set.insert(HgPathBuf::from_bytes(b"subdir"));
885 assert_eq!(
910 assert_eq!(
886 matcher.visit_children_set(HgPath::new(b"dir")),
911 matcher.visit_children_set(HgPath::new(b"dir")),
887 VisitChildrenSet::Set(set)
912 VisitChildrenSet::Set(set)
888 );
913 );
889
914
890 let mut set = HashSet::new();
915 let mut set = HashSet::new();
891 set.insert(HgPathBuf::from_bytes(b"foo.txt"));
916 set.insert(HgPathBuf::from_bytes(b"foo.txt"));
892 assert_eq!(
917 assert_eq!(
893 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
918 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
894 VisitChildrenSet::Set(set)
919 VisitChildrenSet::Set(set)
895 );
920 );
896
921
897 assert_eq!(
922 assert_eq!(
898 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
923 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
899 VisitChildrenSet::Empty
924 VisitChildrenSet::Empty
900 );
925 );
901 assert_eq!(
926 assert_eq!(
902 matcher.visit_children_set(HgPath::new(b"dir/subdir/foo.txt")),
927 matcher.visit_children_set(HgPath::new(b"dir/subdir/foo.txt")),
903 VisitChildrenSet::Empty
928 VisitChildrenSet::Empty
904 );
929 );
905 assert_eq!(
930 assert_eq!(
906 matcher.visit_children_set(HgPath::new(b"folder")),
931 matcher.visit_children_set(HgPath::new(b"folder")),
907 VisitChildrenSet::Empty
932 VisitChildrenSet::Empty
908 );
933 );
909 }
934 }
910
935
911 #[test]
936 #[test]
912 fn test_filematcher_visit_children_set_files_and_dirs() {
937 fn test_filematcher_visit_children_set_files_and_dirs() {
913 let files = vec![
938 let files = vec![
914 HgPathBuf::from_bytes(b"rootfile.txt"),
939 HgPathBuf::from_bytes(b"rootfile.txt"),
915 HgPathBuf::from_bytes(b"a/file1.txt"),
940 HgPathBuf::from_bytes(b"a/file1.txt"),
916 HgPathBuf::from_bytes(b"a/b/file2.txt"),
941 HgPathBuf::from_bytes(b"a/b/file2.txt"),
917 // No file in a/b/c
942 // No file in a/b/c
918 HgPathBuf::from_bytes(b"a/b/c/d/file4.txt"),
943 HgPathBuf::from_bytes(b"a/b/c/d/file4.txt"),
919 ];
944 ];
920 let matcher = FileMatcher::new(files).unwrap();
945 let matcher = FileMatcher::new(files).unwrap();
921
946
922 let mut set = HashSet::new();
947 let mut set = HashSet::new();
923 set.insert(HgPathBuf::from_bytes(b"a"));
948 set.insert(HgPathBuf::from_bytes(b"a"));
924 set.insert(HgPathBuf::from_bytes(b"rootfile.txt"));
949 set.insert(HgPathBuf::from_bytes(b"rootfile.txt"));
925 assert_eq!(
950 assert_eq!(
926 matcher.visit_children_set(HgPath::new(b"")),
951 matcher.visit_children_set(HgPath::new(b"")),
927 VisitChildrenSet::Set(set)
952 VisitChildrenSet::Set(set)
928 );
953 );
929
954
930 let mut set = HashSet::new();
955 let mut set = HashSet::new();
931 set.insert(HgPathBuf::from_bytes(b"b"));
956 set.insert(HgPathBuf::from_bytes(b"b"));
932 set.insert(HgPathBuf::from_bytes(b"file1.txt"));
957 set.insert(HgPathBuf::from_bytes(b"file1.txt"));
933 assert_eq!(
958 assert_eq!(
934 matcher.visit_children_set(HgPath::new(b"a")),
959 matcher.visit_children_set(HgPath::new(b"a")),
935 VisitChildrenSet::Set(set)
960 VisitChildrenSet::Set(set)
936 );
961 );
937
962
938 let mut set = HashSet::new();
963 let mut set = HashSet::new();
939 set.insert(HgPathBuf::from_bytes(b"c"));
964 set.insert(HgPathBuf::from_bytes(b"c"));
940 set.insert(HgPathBuf::from_bytes(b"file2.txt"));
965 set.insert(HgPathBuf::from_bytes(b"file2.txt"));
941 assert_eq!(
966 assert_eq!(
942 matcher.visit_children_set(HgPath::new(b"a/b")),
967 matcher.visit_children_set(HgPath::new(b"a/b")),
943 VisitChildrenSet::Set(set)
968 VisitChildrenSet::Set(set)
944 );
969 );
945
970
946 let mut set = HashSet::new();
971 let mut set = HashSet::new();
947 set.insert(HgPathBuf::from_bytes(b"d"));
972 set.insert(HgPathBuf::from_bytes(b"d"));
948 assert_eq!(
973 assert_eq!(
949 matcher.visit_children_set(HgPath::new(b"a/b/c")),
974 matcher.visit_children_set(HgPath::new(b"a/b/c")),
950 VisitChildrenSet::Set(set)
975 VisitChildrenSet::Set(set)
951 );
976 );
952 let mut set = HashSet::new();
977 let mut set = HashSet::new();
953 set.insert(HgPathBuf::from_bytes(b"file4.txt"));
978 set.insert(HgPathBuf::from_bytes(b"file4.txt"));
954 assert_eq!(
979 assert_eq!(
955 matcher.visit_children_set(HgPath::new(b"a/b/c/d")),
980 matcher.visit_children_set(HgPath::new(b"a/b/c/d")),
956 VisitChildrenSet::Set(set)
981 VisitChildrenSet::Set(set)
957 );
982 );
958
983
959 assert_eq!(
984 assert_eq!(
960 matcher.visit_children_set(HgPath::new(b"a/b/c/d/e")),
985 matcher.visit_children_set(HgPath::new(b"a/b/c/d/e")),
961 VisitChildrenSet::Empty
986 VisitChildrenSet::Empty
962 );
987 );
963 assert_eq!(
988 assert_eq!(
964 matcher.visit_children_set(HgPath::new(b"folder")),
989 matcher.visit_children_set(HgPath::new(b"folder")),
965 VisitChildrenSet::Empty
990 VisitChildrenSet::Empty
966 );
991 );
967 }
992 }
968
993
969 #[test]
994 #[test]
970 fn test_includematcher() {
995 fn test_includematcher() {
971 // VisitchildrensetPrefix
996 // VisitchildrensetPrefix
972 let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
997 let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
973 PatternSyntax::RelPath,
998 PatternSyntax::RelPath,
974 b"dir/subdir",
999 b"dir/subdir",
975 Path::new(""),
1000 Path::new(""),
976 )])
1001 )])
977 .unwrap();
1002 .unwrap();
978
1003
979 let mut set = HashSet::new();
1004 let mut set = HashSet::new();
980 set.insert(HgPathBuf::from_bytes(b"dir"));
1005 set.insert(HgPathBuf::from_bytes(b"dir"));
981 assert_eq!(
1006 assert_eq!(
982 matcher.visit_children_set(HgPath::new(b"")),
1007 matcher.visit_children_set(HgPath::new(b"")),
983 VisitChildrenSet::Set(set)
1008 VisitChildrenSet::Set(set)
984 );
1009 );
985
1010
986 let mut set = HashSet::new();
1011 let mut set = HashSet::new();
987 set.insert(HgPathBuf::from_bytes(b"subdir"));
1012 set.insert(HgPathBuf::from_bytes(b"subdir"));
988 assert_eq!(
1013 assert_eq!(
989 matcher.visit_children_set(HgPath::new(b"dir")),
1014 matcher.visit_children_set(HgPath::new(b"dir")),
990 VisitChildrenSet::Set(set)
1015 VisitChildrenSet::Set(set)
991 );
1016 );
992 assert_eq!(
1017 assert_eq!(
993 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1018 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
994 VisitChildrenSet::Recursive
1019 VisitChildrenSet::Recursive
995 );
1020 );
996 // OPT: This should probably be 'all' if its parent is?
1021 // OPT: This should probably be 'all' if its parent is?
997 assert_eq!(
1022 assert_eq!(
998 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1023 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
999 VisitChildrenSet::This
1024 VisitChildrenSet::This
1000 );
1025 );
1001 assert_eq!(
1026 assert_eq!(
1002 matcher.visit_children_set(HgPath::new(b"folder")),
1027 matcher.visit_children_set(HgPath::new(b"folder")),
1003 VisitChildrenSet::Empty
1028 VisitChildrenSet::Empty
1004 );
1029 );
1005
1030
1006 // VisitchildrensetRootfilesin
1031 // VisitchildrensetRootfilesin
1007 let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
1032 let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
1008 PatternSyntax::RootFiles,
1033 PatternSyntax::RootFiles,
1009 b"dir/subdir",
1034 b"dir/subdir",
1010 Path::new(""),
1035 Path::new(""),
1011 )])
1036 )])
1012 .unwrap();
1037 .unwrap();
1013
1038
1014 let mut set = HashSet::new();
1039 let mut set = HashSet::new();
1015 set.insert(HgPathBuf::from_bytes(b"dir"));
1040 set.insert(HgPathBuf::from_bytes(b"dir"));
1016 assert_eq!(
1041 assert_eq!(
1017 matcher.visit_children_set(HgPath::new(b"")),
1042 matcher.visit_children_set(HgPath::new(b"")),
1018 VisitChildrenSet::Set(set)
1043 VisitChildrenSet::Set(set)
1019 );
1044 );
1020
1045
1021 let mut set = HashSet::new();
1046 let mut set = HashSet::new();
1022 set.insert(HgPathBuf::from_bytes(b"subdir"));
1047 set.insert(HgPathBuf::from_bytes(b"subdir"));
1023 assert_eq!(
1048 assert_eq!(
1024 matcher.visit_children_set(HgPath::new(b"dir")),
1049 matcher.visit_children_set(HgPath::new(b"dir")),
1025 VisitChildrenSet::Set(set)
1050 VisitChildrenSet::Set(set)
1026 );
1051 );
1027
1052
1028 assert_eq!(
1053 assert_eq!(
1029 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1054 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1030 VisitChildrenSet::This
1055 VisitChildrenSet::This
1031 );
1056 );
1032 assert_eq!(
1057 assert_eq!(
1033 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1058 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1034 VisitChildrenSet::Empty
1059 VisitChildrenSet::Empty
1035 );
1060 );
1036 assert_eq!(
1061 assert_eq!(
1037 matcher.visit_children_set(HgPath::new(b"folder")),
1062 matcher.visit_children_set(HgPath::new(b"folder")),
1038 VisitChildrenSet::Empty
1063 VisitChildrenSet::Empty
1039 );
1064 );
1040
1065
1041 // VisitchildrensetGlob
1066 // VisitchildrensetGlob
1042 let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
1067 let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
1043 PatternSyntax::Glob,
1068 PatternSyntax::Glob,
1044 b"dir/z*",
1069 b"dir/z*",
1045 Path::new(""),
1070 Path::new(""),
1046 )])
1071 )])
1047 .unwrap();
1072 .unwrap();
1048
1073
1049 let mut set = HashSet::new();
1074 let mut set = HashSet::new();
1050 set.insert(HgPathBuf::from_bytes(b"dir"));
1075 set.insert(HgPathBuf::from_bytes(b"dir"));
1051 assert_eq!(
1076 assert_eq!(
1052 matcher.visit_children_set(HgPath::new(b"")),
1077 matcher.visit_children_set(HgPath::new(b"")),
1053 VisitChildrenSet::Set(set)
1078 VisitChildrenSet::Set(set)
1054 );
1079 );
1055 assert_eq!(
1080 assert_eq!(
1056 matcher.visit_children_set(HgPath::new(b"folder")),
1081 matcher.visit_children_set(HgPath::new(b"folder")),
1057 VisitChildrenSet::Empty
1082 VisitChildrenSet::Empty
1058 );
1083 );
1059 assert_eq!(
1084 assert_eq!(
1060 matcher.visit_children_set(HgPath::new(b"dir")),
1085 matcher.visit_children_set(HgPath::new(b"dir")),
1061 VisitChildrenSet::This
1086 VisitChildrenSet::This
1062 );
1087 );
1063 // OPT: these should probably be set().
1088 // OPT: these should probably be set().
1064 assert_eq!(
1089 assert_eq!(
1065 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1090 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1066 VisitChildrenSet::This
1091 VisitChildrenSet::This
1067 );
1092 );
1068 assert_eq!(
1093 assert_eq!(
1069 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1094 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1070 VisitChildrenSet::This
1095 VisitChildrenSet::This
1071 );
1096 );
1072 }
1097 }
1073
1098
1074 #[test]
1099 #[test]
1075 fn test_unionmatcher() {
1100 fn test_unionmatcher() {
1076 // Path + Rootfiles
1101 // Path + Rootfiles
1077 let m1 = IncludeMatcher::new(vec![IgnorePattern::new(
1102 let m1 = IncludeMatcher::new(vec![IgnorePattern::new(
1078 PatternSyntax::RelPath,
1103 PatternSyntax::RelPath,
1079 b"dir/subdir",
1104 b"dir/subdir",
1080 Path::new(""),
1105 Path::new(""),
1081 )])
1106 )])
1082 .unwrap();
1107 .unwrap();
1083 let m2 = IncludeMatcher::new(vec![IgnorePattern::new(
1108 let m2 = IncludeMatcher::new(vec![IgnorePattern::new(
1084 PatternSyntax::RootFiles,
1109 PatternSyntax::RootFiles,
1085 b"dir",
1110 b"dir",
1086 Path::new(""),
1111 Path::new(""),
1087 )])
1112 )])
1088 .unwrap();
1113 .unwrap();
1089 let matcher = UnionMatcher::new(vec![Box::new(m1), Box::new(m2)]);
1114 let matcher = UnionMatcher::new(vec![Box::new(m1), Box::new(m2)]);
1090
1115
1091 let mut set = HashSet::new();
1116 let mut set = HashSet::new();
1092 set.insert(HgPathBuf::from_bytes(b"dir"));
1117 set.insert(HgPathBuf::from_bytes(b"dir"));
1093 assert_eq!(
1118 assert_eq!(
1094 matcher.visit_children_set(HgPath::new(b"")),
1119 matcher.visit_children_set(HgPath::new(b"")),
1095 VisitChildrenSet::Set(set)
1120 VisitChildrenSet::Set(set)
1096 );
1121 );
1097 assert_eq!(
1122 assert_eq!(
1098 matcher.visit_children_set(HgPath::new(b"dir")),
1123 matcher.visit_children_set(HgPath::new(b"dir")),
1099 VisitChildrenSet::This
1124 VisitChildrenSet::This
1100 );
1125 );
1101 assert_eq!(
1126 assert_eq!(
1102 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1127 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1103 VisitChildrenSet::Recursive
1128 VisitChildrenSet::Recursive
1104 );
1129 );
1105 assert_eq!(
1130 assert_eq!(
1106 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1131 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1107 VisitChildrenSet::Empty
1132 VisitChildrenSet::Empty
1108 );
1133 );
1109 assert_eq!(
1134 assert_eq!(
1110 matcher.visit_children_set(HgPath::new(b"folder")),
1135 matcher.visit_children_set(HgPath::new(b"folder")),
1111 VisitChildrenSet::Empty
1136 VisitChildrenSet::Empty
1112 );
1137 );
1113 assert_eq!(
1138 assert_eq!(
1114 matcher.visit_children_set(HgPath::new(b"folder")),
1139 matcher.visit_children_set(HgPath::new(b"folder")),
1115 VisitChildrenSet::Empty
1140 VisitChildrenSet::Empty
1116 );
1141 );
1117
1142
1118 // OPT: These next two could be 'all' instead of 'this'.
1143 // OPT: These next two could be 'all' instead of 'this'.
1119 assert_eq!(
1144 assert_eq!(
1120 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1145 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1121 VisitChildrenSet::This
1146 VisitChildrenSet::This
1122 );
1147 );
1123 assert_eq!(
1148 assert_eq!(
1124 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1149 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1125 VisitChildrenSet::This
1150 VisitChildrenSet::This
1126 );
1151 );
1127
1152
1128 // Path + unrelated Path
1153 // Path + unrelated Path
1129 let m1 = IncludeMatcher::new(vec![IgnorePattern::new(
1154 let m1 = IncludeMatcher::new(vec![IgnorePattern::new(
1130 PatternSyntax::RelPath,
1155 PatternSyntax::RelPath,
1131 b"dir/subdir",
1156 b"dir/subdir",
1132 Path::new(""),
1157 Path::new(""),
1133 )])
1158 )])
1134 .unwrap();
1159 .unwrap();
1135 let m2 = IncludeMatcher::new(vec![IgnorePattern::new(
1160 let m2 = IncludeMatcher::new(vec![IgnorePattern::new(
1136 PatternSyntax::RelPath,
1161 PatternSyntax::RelPath,
1137 b"folder",
1162 b"folder",
1138 Path::new(""),
1163 Path::new(""),
1139 )])
1164 )])
1140 .unwrap();
1165 .unwrap();
1141 let matcher = UnionMatcher::new(vec![Box::new(m1), Box::new(m2)]);
1166 let matcher = UnionMatcher::new(vec![Box::new(m1), Box::new(m2)]);
1142
1167
1143 let mut set = HashSet::new();
1168 let mut set = HashSet::new();
1144 set.insert(HgPathBuf::from_bytes(b"folder"));
1169 set.insert(HgPathBuf::from_bytes(b"folder"));
1145 set.insert(HgPathBuf::from_bytes(b"dir"));
1170 set.insert(HgPathBuf::from_bytes(b"dir"));
1146 assert_eq!(
1171 assert_eq!(
1147 matcher.visit_children_set(HgPath::new(b"")),
1172 matcher.visit_children_set(HgPath::new(b"")),
1148 VisitChildrenSet::Set(set)
1173 VisitChildrenSet::Set(set)
1149 );
1174 );
1150 let mut set = HashSet::new();
1175 let mut set = HashSet::new();
1151 set.insert(HgPathBuf::from_bytes(b"subdir"));
1176 set.insert(HgPathBuf::from_bytes(b"subdir"));
1152 assert_eq!(
1177 assert_eq!(
1153 matcher.visit_children_set(HgPath::new(b"dir")),
1178 matcher.visit_children_set(HgPath::new(b"dir")),
1154 VisitChildrenSet::Set(set)
1179 VisitChildrenSet::Set(set)
1155 );
1180 );
1156
1181
1157 assert_eq!(
1182 assert_eq!(
1158 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1183 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1159 VisitChildrenSet::Recursive
1184 VisitChildrenSet::Recursive
1160 );
1185 );
1161 assert_eq!(
1186 assert_eq!(
1162 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1187 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1163 VisitChildrenSet::Empty
1188 VisitChildrenSet::Empty
1164 );
1189 );
1165
1190
1166 assert_eq!(
1191 assert_eq!(
1167 matcher.visit_children_set(HgPath::new(b"folder")),
1192 matcher.visit_children_set(HgPath::new(b"folder")),
1168 VisitChildrenSet::Recursive
1193 VisitChildrenSet::Recursive
1169 );
1194 );
1170 // OPT: These next two could be 'all' instead of 'this'.
1195 // OPT: These next two could be 'all' instead of 'this'.
1171 assert_eq!(
1196 assert_eq!(
1172 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1197 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1173 VisitChildrenSet::This
1198 VisitChildrenSet::This
1174 );
1199 );
1175 assert_eq!(
1200 assert_eq!(
1176 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1201 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1177 VisitChildrenSet::This
1202 VisitChildrenSet::This
1178 );
1203 );
1179
1204
1180 // Path + subpath
1205 // Path + subpath
1181 let m1 = IncludeMatcher::new(vec![IgnorePattern::new(
1206 let m1 = IncludeMatcher::new(vec![IgnorePattern::new(
1182 PatternSyntax::RelPath,
1207 PatternSyntax::RelPath,
1183 b"dir/subdir/x",
1208 b"dir/subdir/x",
1184 Path::new(""),
1209 Path::new(""),
1185 )])
1210 )])
1186 .unwrap();
1211 .unwrap();
1187 let m2 = IncludeMatcher::new(vec![IgnorePattern::new(
1212 let m2 = IncludeMatcher::new(vec![IgnorePattern::new(
1188 PatternSyntax::RelPath,
1213 PatternSyntax::RelPath,
1189 b"dir/subdir",
1214 b"dir/subdir",
1190 Path::new(""),
1215 Path::new(""),
1191 )])
1216 )])
1192 .unwrap();
1217 .unwrap();
1193 let matcher = UnionMatcher::new(vec![Box::new(m1), Box::new(m2)]);
1218 let matcher = UnionMatcher::new(vec![Box::new(m1), Box::new(m2)]);
1194
1219
1195 let mut set = HashSet::new();
1220 let mut set = HashSet::new();
1196 set.insert(HgPathBuf::from_bytes(b"dir"));
1221 set.insert(HgPathBuf::from_bytes(b"dir"));
1197 assert_eq!(
1222 assert_eq!(
1198 matcher.visit_children_set(HgPath::new(b"")),
1223 matcher.visit_children_set(HgPath::new(b"")),
1199 VisitChildrenSet::Set(set)
1224 VisitChildrenSet::Set(set)
1200 );
1225 );
1201 let mut set = HashSet::new();
1226 let mut set = HashSet::new();
1202 set.insert(HgPathBuf::from_bytes(b"subdir"));
1227 set.insert(HgPathBuf::from_bytes(b"subdir"));
1203 assert_eq!(
1228 assert_eq!(
1204 matcher.visit_children_set(HgPath::new(b"dir")),
1229 matcher.visit_children_set(HgPath::new(b"dir")),
1205 VisitChildrenSet::Set(set)
1230 VisitChildrenSet::Set(set)
1206 );
1231 );
1207
1232
1208 assert_eq!(
1233 assert_eq!(
1209 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1234 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1210 VisitChildrenSet::Recursive
1235 VisitChildrenSet::Recursive
1211 );
1236 );
1212 assert_eq!(
1237 assert_eq!(
1213 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1238 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1214 VisitChildrenSet::Empty
1239 VisitChildrenSet::Empty
1215 );
1240 );
1216
1241
1217 assert_eq!(
1242 assert_eq!(
1218 matcher.visit_children_set(HgPath::new(b"folder")),
1243 matcher.visit_children_set(HgPath::new(b"folder")),
1219 VisitChildrenSet::Empty
1244 VisitChildrenSet::Empty
1220 );
1245 );
1221 assert_eq!(
1246 assert_eq!(
1222 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1247 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1223 VisitChildrenSet::Recursive
1248 VisitChildrenSet::Recursive
1224 );
1249 );
1225 // OPT: this should probably be 'all' not 'this'.
1250 // OPT: this should probably be 'all' not 'this'.
1226 assert_eq!(
1251 assert_eq!(
1227 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1252 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1228 VisitChildrenSet::This
1253 VisitChildrenSet::This
1229 );
1254 );
1230 }
1255 }
1231
1256
1232 #[test]
1257 #[test]
1233 fn test_intersectionmatcher() {
1258 fn test_intersectionmatcher() {
1234 // Include path + Include rootfiles
1259 // Include path + Include rootfiles
1235 let m1 = Box::new(
1260 let m1 = Box::new(
1236 IncludeMatcher::new(vec![IgnorePattern::new(
1261 IncludeMatcher::new(vec![IgnorePattern::new(
1237 PatternSyntax::RelPath,
1262 PatternSyntax::RelPath,
1238 b"dir/subdir",
1263 b"dir/subdir",
1239 Path::new(""),
1264 Path::new(""),
1240 )])
1265 )])
1241 .unwrap(),
1266 .unwrap(),
1242 );
1267 );
1243 let m2 = Box::new(
1268 let m2 = Box::new(
1244 IncludeMatcher::new(vec![IgnorePattern::new(
1269 IncludeMatcher::new(vec![IgnorePattern::new(
1245 PatternSyntax::RootFiles,
1270 PatternSyntax::RootFiles,
1246 b"dir",
1271 b"dir",
1247 Path::new(""),
1272 Path::new(""),
1248 )])
1273 )])
1249 .unwrap(),
1274 .unwrap(),
1250 );
1275 );
1251 let matcher = IntersectionMatcher::new(m1, m2);
1276 let matcher = IntersectionMatcher::new(m1, m2);
1252
1277
1253 let mut set = HashSet::new();
1278 let mut set = HashSet::new();
1254 set.insert(HgPathBuf::from_bytes(b"dir"));
1279 set.insert(HgPathBuf::from_bytes(b"dir"));
1255 assert_eq!(
1280 assert_eq!(
1256 matcher.visit_children_set(HgPath::new(b"")),
1281 matcher.visit_children_set(HgPath::new(b"")),
1257 VisitChildrenSet::Set(set)
1282 VisitChildrenSet::Set(set)
1258 );
1283 );
1259 assert_eq!(
1284 assert_eq!(
1260 matcher.visit_children_set(HgPath::new(b"dir")),
1285 matcher.visit_children_set(HgPath::new(b"dir")),
1261 VisitChildrenSet::This
1286 VisitChildrenSet::This
1262 );
1287 );
1263 assert_eq!(
1288 assert_eq!(
1264 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1289 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1265 VisitChildrenSet::Empty
1290 VisitChildrenSet::Empty
1266 );
1291 );
1267 assert_eq!(
1292 assert_eq!(
1268 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1293 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1269 VisitChildrenSet::Empty
1294 VisitChildrenSet::Empty
1270 );
1295 );
1271 assert_eq!(
1296 assert_eq!(
1272 matcher.visit_children_set(HgPath::new(b"folder")),
1297 matcher.visit_children_set(HgPath::new(b"folder")),
1273 VisitChildrenSet::Empty
1298 VisitChildrenSet::Empty
1274 );
1299 );
1275 assert_eq!(
1300 assert_eq!(
1276 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1301 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1277 VisitChildrenSet::Empty
1302 VisitChildrenSet::Empty
1278 );
1303 );
1279 assert_eq!(
1304 assert_eq!(
1280 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1305 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1281 VisitChildrenSet::Empty
1306 VisitChildrenSet::Empty
1282 );
1307 );
1283
1308
1284 // Non intersecting paths
1309 // Non intersecting paths
1285 let m1 = Box::new(
1310 let m1 = Box::new(
1286 IncludeMatcher::new(vec![IgnorePattern::new(
1311 IncludeMatcher::new(vec![IgnorePattern::new(
1287 PatternSyntax::RelPath,
1312 PatternSyntax::RelPath,
1288 b"dir/subdir",
1313 b"dir/subdir",
1289 Path::new(""),
1314 Path::new(""),
1290 )])
1315 )])
1291 .unwrap(),
1316 .unwrap(),
1292 );
1317 );
1293 let m2 = Box::new(
1318 let m2 = Box::new(
1294 IncludeMatcher::new(vec![IgnorePattern::new(
1319 IncludeMatcher::new(vec![IgnorePattern::new(
1295 PatternSyntax::RelPath,
1320 PatternSyntax::RelPath,
1296 b"folder",
1321 b"folder",
1297 Path::new(""),
1322 Path::new(""),
1298 )])
1323 )])
1299 .unwrap(),
1324 .unwrap(),
1300 );
1325 );
1301 let matcher = IntersectionMatcher::new(m1, m2);
1326 let matcher = IntersectionMatcher::new(m1, m2);
1302
1327
1303 assert_eq!(
1328 assert_eq!(
1304 matcher.visit_children_set(HgPath::new(b"")),
1329 matcher.visit_children_set(HgPath::new(b"")),
1305 VisitChildrenSet::Empty
1330 VisitChildrenSet::Empty
1306 );
1331 );
1307 assert_eq!(
1332 assert_eq!(
1308 matcher.visit_children_set(HgPath::new(b"dir")),
1333 matcher.visit_children_set(HgPath::new(b"dir")),
1309 VisitChildrenSet::Empty
1334 VisitChildrenSet::Empty
1310 );
1335 );
1311 assert_eq!(
1336 assert_eq!(
1312 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1337 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1313 VisitChildrenSet::Empty
1338 VisitChildrenSet::Empty
1314 );
1339 );
1315 assert_eq!(
1340 assert_eq!(
1316 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1341 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1317 VisitChildrenSet::Empty
1342 VisitChildrenSet::Empty
1318 );
1343 );
1319 assert_eq!(
1344 assert_eq!(
1320 matcher.visit_children_set(HgPath::new(b"folder")),
1345 matcher.visit_children_set(HgPath::new(b"folder")),
1321 VisitChildrenSet::Empty
1346 VisitChildrenSet::Empty
1322 );
1347 );
1323 assert_eq!(
1348 assert_eq!(
1324 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1349 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1325 VisitChildrenSet::Empty
1350 VisitChildrenSet::Empty
1326 );
1351 );
1327 assert_eq!(
1352 assert_eq!(
1328 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1353 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1329 VisitChildrenSet::Empty
1354 VisitChildrenSet::Empty
1330 );
1355 );
1331
1356
1332 // Nested paths
1357 // Nested paths
1333 let m1 = Box::new(
1358 let m1 = Box::new(
1334 IncludeMatcher::new(vec![IgnorePattern::new(
1359 IncludeMatcher::new(vec![IgnorePattern::new(
1335 PatternSyntax::RelPath,
1360 PatternSyntax::RelPath,
1336 b"dir/subdir/x",
1361 b"dir/subdir/x",
1337 Path::new(""),
1362 Path::new(""),
1338 )])
1363 )])
1339 .unwrap(),
1364 .unwrap(),
1340 );
1365 );
1341 let m2 = Box::new(
1366 let m2 = Box::new(
1342 IncludeMatcher::new(vec![IgnorePattern::new(
1367 IncludeMatcher::new(vec![IgnorePattern::new(
1343 PatternSyntax::RelPath,
1368 PatternSyntax::RelPath,
1344 b"dir/subdir",
1369 b"dir/subdir",
1345 Path::new(""),
1370 Path::new(""),
1346 )])
1371 )])
1347 .unwrap(),
1372 .unwrap(),
1348 );
1373 );
1349 let matcher = IntersectionMatcher::new(m1, m2);
1374 let matcher = IntersectionMatcher::new(m1, m2);
1350
1375
1351 let mut set = HashSet::new();
1376 let mut set = HashSet::new();
1352 set.insert(HgPathBuf::from_bytes(b"dir"));
1377 set.insert(HgPathBuf::from_bytes(b"dir"));
1353 assert_eq!(
1378 assert_eq!(
1354 matcher.visit_children_set(HgPath::new(b"")),
1379 matcher.visit_children_set(HgPath::new(b"")),
1355 VisitChildrenSet::Set(set)
1380 VisitChildrenSet::Set(set)
1356 );
1381 );
1357
1382
1358 let mut set = HashSet::new();
1383 let mut set = HashSet::new();
1359 set.insert(HgPathBuf::from_bytes(b"subdir"));
1384 set.insert(HgPathBuf::from_bytes(b"subdir"));
1360 assert_eq!(
1385 assert_eq!(
1361 matcher.visit_children_set(HgPath::new(b"dir")),
1386 matcher.visit_children_set(HgPath::new(b"dir")),
1362 VisitChildrenSet::Set(set)
1387 VisitChildrenSet::Set(set)
1363 );
1388 );
1364 let mut set = HashSet::new();
1389 let mut set = HashSet::new();
1365 set.insert(HgPathBuf::from_bytes(b"x"));
1390 set.insert(HgPathBuf::from_bytes(b"x"));
1366 assert_eq!(
1391 assert_eq!(
1367 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1392 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1368 VisitChildrenSet::Set(set)
1393 VisitChildrenSet::Set(set)
1369 );
1394 );
1370 assert_eq!(
1395 assert_eq!(
1371 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1396 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1372 VisitChildrenSet::Empty
1397 VisitChildrenSet::Empty
1373 );
1398 );
1374 assert_eq!(
1399 assert_eq!(
1375 matcher.visit_children_set(HgPath::new(b"folder")),
1400 matcher.visit_children_set(HgPath::new(b"folder")),
1376 VisitChildrenSet::Empty
1401 VisitChildrenSet::Empty
1377 );
1402 );
1378 assert_eq!(
1403 assert_eq!(
1379 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1404 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1380 VisitChildrenSet::Empty
1405 VisitChildrenSet::Empty
1381 );
1406 );
1382 // OPT: this should probably be 'all' not 'this'.
1407 // OPT: this should probably be 'all' not 'this'.
1383 assert_eq!(
1408 assert_eq!(
1384 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1409 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1385 VisitChildrenSet::This
1410 VisitChildrenSet::This
1386 );
1411 );
1387
1412
1388 // Diverging paths
1413 // Diverging paths
1389 let m1 = Box::new(
1414 let m1 = Box::new(
1390 IncludeMatcher::new(vec![IgnorePattern::new(
1415 IncludeMatcher::new(vec![IgnorePattern::new(
1391 PatternSyntax::RelPath,
1416 PatternSyntax::RelPath,
1392 b"dir/subdir/x",
1417 b"dir/subdir/x",
1393 Path::new(""),
1418 Path::new(""),
1394 )])
1419 )])
1395 .unwrap(),
1420 .unwrap(),
1396 );
1421 );
1397 let m2 = Box::new(
1422 let m2 = Box::new(
1398 IncludeMatcher::new(vec![IgnorePattern::new(
1423 IncludeMatcher::new(vec![IgnorePattern::new(
1399 PatternSyntax::RelPath,
1424 PatternSyntax::RelPath,
1400 b"dir/subdir/z",
1425 b"dir/subdir/z",
1401 Path::new(""),
1426 Path::new(""),
1402 )])
1427 )])
1403 .unwrap(),
1428 .unwrap(),
1404 );
1429 );
1405 let matcher = IntersectionMatcher::new(m1, m2);
1430 let matcher = IntersectionMatcher::new(m1, m2);
1406
1431
1407 // OPT: these next two could probably be Empty as well.
1432 // OPT: these next two could probably be Empty as well.
1408 let mut set = HashSet::new();
1433 let mut set = HashSet::new();
1409 set.insert(HgPathBuf::from_bytes(b"dir"));
1434 set.insert(HgPathBuf::from_bytes(b"dir"));
1410 assert_eq!(
1435 assert_eq!(
1411 matcher.visit_children_set(HgPath::new(b"")),
1436 matcher.visit_children_set(HgPath::new(b"")),
1412 VisitChildrenSet::Set(set)
1437 VisitChildrenSet::Set(set)
1413 );
1438 );
1414 // OPT: these next two could probably be Empty as well.
1439 // OPT: these next two could probably be Empty as well.
1415 let mut set = HashSet::new();
1440 let mut set = HashSet::new();
1416 set.insert(HgPathBuf::from_bytes(b"subdir"));
1441 set.insert(HgPathBuf::from_bytes(b"subdir"));
1417 assert_eq!(
1442 assert_eq!(
1418 matcher.visit_children_set(HgPath::new(b"dir")),
1443 matcher.visit_children_set(HgPath::new(b"dir")),
1419 VisitChildrenSet::Set(set)
1444 VisitChildrenSet::Set(set)
1420 );
1445 );
1421 assert_eq!(
1446 assert_eq!(
1422 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1447 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1423 VisitChildrenSet::Empty
1448 VisitChildrenSet::Empty
1424 );
1449 );
1425 assert_eq!(
1450 assert_eq!(
1426 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1451 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1427 VisitChildrenSet::Empty
1452 VisitChildrenSet::Empty
1428 );
1453 );
1429 assert_eq!(
1454 assert_eq!(
1430 matcher.visit_children_set(HgPath::new(b"folder")),
1455 matcher.visit_children_set(HgPath::new(b"folder")),
1431 VisitChildrenSet::Empty
1456 VisitChildrenSet::Empty
1432 );
1457 );
1433 assert_eq!(
1458 assert_eq!(
1434 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1459 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1435 VisitChildrenSet::Empty
1460 VisitChildrenSet::Empty
1436 );
1461 );
1437 assert_eq!(
1462 assert_eq!(
1438 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1463 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1439 VisitChildrenSet::Empty
1464 VisitChildrenSet::Empty
1440 );
1465 );
1441 }
1466 }
1442 }
1467 }
@@ -1,298 +1,299 b''
1 // status.rs
1 // status.rs
2 //
2 //
3 // Copyright 2019, Raphaël Gomès <rgomes@octobus.net>
3 // Copyright 2019, Raphaël Gomès <rgomes@octobus.net>
4 //
4 //
5 // This software may be used and distributed according to the terms of the
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
6 // GNU General Public License version 2 or any later version.
7
7
8 //! Bindings for the `hg::status` module provided by the
8 //! Bindings for the `hg::status` module provided by the
9 //! `hg-core` crate. From Python, this will be seen as
9 //! `hg-core` crate. From Python, this will be seen as
10 //! `rustext.dirstate.status`.
10 //! `rustext.dirstate.status`.
11
11
12 use crate::{dirstate::DirstateMap, exceptions::FallbackError};
12 use crate::{dirstate::DirstateMap, exceptions::FallbackError};
13 use cpython::{
13 use cpython::{
14 exc::ValueError, ObjectProtocol, PyBytes, PyErr, PyList, PyObject,
14 exc::ValueError, ObjectProtocol, PyBytes, PyErr, PyList, PyObject,
15 PyResult, PyTuple, Python, PythonObject, ToPyObject,
15 PyResult, PyTuple, Python, PythonObject, ToPyObject,
16 };
16 };
17 use hg::dirstate::status::StatusPath;
17 use hg::dirstate::status::StatusPath;
18 use hg::matchers::{Matcher, UnionMatcher, IntersectionMatcher};
18 use hg::matchers::{IntersectionMatcher, Matcher, NeverMatcher, UnionMatcher};
19 use hg::{
19 use hg::{
20 matchers::{AlwaysMatcher, FileMatcher, IncludeMatcher},
20 matchers::{AlwaysMatcher, FileMatcher, IncludeMatcher},
21 parse_pattern_syntax,
21 parse_pattern_syntax,
22 utils::{
22 utils::{
23 files::{get_bytes_from_path, get_path_from_bytes},
23 files::{get_bytes_from_path, get_path_from_bytes},
24 hg_path::{HgPath, HgPathBuf},
24 hg_path::{HgPath, HgPathBuf},
25 },
25 },
26 BadMatch, DirstateStatus, IgnorePattern, PatternFileWarning, StatusError,
26 BadMatch, DirstateStatus, IgnorePattern, PatternFileWarning, StatusError,
27 StatusOptions,
27 StatusOptions,
28 };
28 };
29 use std::borrow::Borrow;
29 use std::borrow::Borrow;
30
30
31 fn collect_status_path_list(py: Python, paths: &[StatusPath<'_>]) -> PyList {
31 fn collect_status_path_list(py: Python, paths: &[StatusPath<'_>]) -> PyList {
32 collect_pybytes_list(py, paths.iter().map(|item| &*item.path))
32 collect_pybytes_list(py, paths.iter().map(|item| &*item.path))
33 }
33 }
34
34
35 /// This will be useless once trait impls for collection are added to `PyBytes`
35 /// This will be useless once trait impls for collection are added to `PyBytes`
36 /// upstream.
36 /// upstream.
37 fn collect_pybytes_list(
37 fn collect_pybytes_list(
38 py: Python,
38 py: Python,
39 iter: impl Iterator<Item = impl AsRef<HgPath>>,
39 iter: impl Iterator<Item = impl AsRef<HgPath>>,
40 ) -> PyList {
40 ) -> PyList {
41 let list = PyList::new(py, &[]);
41 let list = PyList::new(py, &[]);
42
42
43 for path in iter {
43 for path in iter {
44 list.append(
44 list.append(
45 py,
45 py,
46 PyBytes::new(py, path.as_ref().as_bytes()).into_object(),
46 PyBytes::new(py, path.as_ref().as_bytes()).into_object(),
47 )
47 )
48 }
48 }
49
49
50 list
50 list
51 }
51 }
52
52
53 fn collect_bad_matches(
53 fn collect_bad_matches(
54 py: Python,
54 py: Python,
55 collection: &[(impl AsRef<HgPath>, BadMatch)],
55 collection: &[(impl AsRef<HgPath>, BadMatch)],
56 ) -> PyResult<PyList> {
56 ) -> PyResult<PyList> {
57 let list = PyList::new(py, &[]);
57 let list = PyList::new(py, &[]);
58
58
59 let os = py.import("os")?;
59 let os = py.import("os")?;
60 let get_error_message = |code: i32| -> PyResult<_> {
60 let get_error_message = |code: i32| -> PyResult<_> {
61 os.call(
61 os.call(
62 py,
62 py,
63 "strerror",
63 "strerror",
64 PyTuple::new(py, &[code.to_py_object(py).into_object()]),
64 PyTuple::new(py, &[code.to_py_object(py).into_object()]),
65 None,
65 None,
66 )
66 )
67 };
67 };
68
68
69 for (path, bad_match) in collection.iter() {
69 for (path, bad_match) in collection.iter() {
70 let message = match bad_match {
70 let message = match bad_match {
71 BadMatch::OsError(code) => get_error_message(*code)?,
71 BadMatch::OsError(code) => get_error_message(*code)?,
72 BadMatch::BadType(bad_type) => format!(
72 BadMatch::BadType(bad_type) => format!(
73 "unsupported file type (type is {})",
73 "unsupported file type (type is {})",
74 bad_type.to_string()
74 bad_type.to_string()
75 )
75 )
76 .to_py_object(py)
76 .to_py_object(py)
77 .into_object(),
77 .into_object(),
78 };
78 };
79 list.append(
79 list.append(
80 py,
80 py,
81 (PyBytes::new(py, path.as_ref().as_bytes()), message)
81 (PyBytes::new(py, path.as_ref().as_bytes()), message)
82 .to_py_object(py)
82 .to_py_object(py)
83 .into_object(),
83 .into_object(),
84 )
84 )
85 }
85 }
86
86
87 Ok(list)
87 Ok(list)
88 }
88 }
89
89
90 fn handle_fallback(py: Python, err: StatusError) -> PyErr {
90 fn handle_fallback(py: Python, err: StatusError) -> PyErr {
91 match err {
91 match err {
92 StatusError::Pattern(e) => {
92 StatusError::Pattern(e) => {
93 let as_string = e.to_string();
93 let as_string = e.to_string();
94 log::trace!("Rust status fallback: `{}`", &as_string);
94 log::trace!("Rust status fallback: `{}`", &as_string);
95
95
96 PyErr::new::<FallbackError, _>(py, &as_string)
96 PyErr::new::<FallbackError, _>(py, &as_string)
97 }
97 }
98 e => PyErr::new::<ValueError, _>(py, e.to_string()),
98 e => PyErr::new::<ValueError, _>(py, e.to_string()),
99 }
99 }
100 }
100 }
101
101
102 pub fn status_wrapper(
102 pub fn status_wrapper(
103 py: Python,
103 py: Python,
104 dmap: DirstateMap,
104 dmap: DirstateMap,
105 matcher: PyObject,
105 matcher: PyObject,
106 root_dir: PyObject,
106 root_dir: PyObject,
107 ignore_files: PyList,
107 ignore_files: PyList,
108 check_exec: bool,
108 check_exec: bool,
109 list_clean: bool,
109 list_clean: bool,
110 list_ignored: bool,
110 list_ignored: bool,
111 list_unknown: bool,
111 list_unknown: bool,
112 collect_traversed_dirs: bool,
112 collect_traversed_dirs: bool,
113 ) -> PyResult<PyTuple> {
113 ) -> PyResult<PyTuple> {
114 let bytes = root_dir.extract::<PyBytes>(py)?;
114 let bytes = root_dir.extract::<PyBytes>(py)?;
115 let root_dir = get_path_from_bytes(bytes.data(py));
115 let root_dir = get_path_from_bytes(bytes.data(py));
116
116
117 let dmap: DirstateMap = dmap.to_py_object(py);
117 let dmap: DirstateMap = dmap.to_py_object(py);
118 let mut dmap = dmap.get_inner_mut(py);
118 let mut dmap = dmap.get_inner_mut(py);
119
119
120 let ignore_files: PyResult<Vec<_>> = ignore_files
120 let ignore_files: PyResult<Vec<_>> = ignore_files
121 .iter(py)
121 .iter(py)
122 .map(|b| {
122 .map(|b| {
123 let file = b.extract::<PyBytes>(py)?;
123 let file = b.extract::<PyBytes>(py)?;
124 Ok(get_path_from_bytes(file.data(py)).to_owned())
124 Ok(get_path_from_bytes(file.data(py)).to_owned())
125 })
125 })
126 .collect();
126 .collect();
127 let ignore_files = ignore_files?;
127 let ignore_files = ignore_files?;
128 // The caller may call `copymap.items()` separately
128 // The caller may call `copymap.items()` separately
129 let list_copies = false;
129 let list_copies = false;
130
130
131 let after_status = |res: Result<(DirstateStatus<'_>, _), StatusError>| {
131 let after_status = |res: Result<(DirstateStatus<'_>, _), StatusError>| {
132 let (status_res, warnings) =
132 let (status_res, warnings) =
133 res.map_err(|e| handle_fallback(py, e))?;
133 res.map_err(|e| handle_fallback(py, e))?;
134 build_response(py, status_res, warnings)
134 build_response(py, status_res, warnings)
135 };
135 };
136
136
137 let matcher = extract_matcher(py, matcher)?;
137 let matcher = extract_matcher(py, matcher)?;
138 dmap.with_status(
138 dmap.with_status(
139 &*matcher,
139 &*matcher,
140 root_dir.to_path_buf(),
140 root_dir.to_path_buf(),
141 ignore_files,
141 ignore_files,
142 StatusOptions {
142 StatusOptions {
143 check_exec,
143 check_exec,
144 list_clean,
144 list_clean,
145 list_ignored,
145 list_ignored,
146 list_unknown,
146 list_unknown,
147 list_copies,
147 list_copies,
148 collect_traversed_dirs,
148 collect_traversed_dirs,
149 },
149 },
150 after_status,
150 after_status,
151 )
151 )
152 }
152 }
153
153
154 /// Transform a Python matcher into a Rust matcher.
154 /// Transform a Python matcher into a Rust matcher.
155 fn extract_matcher(
155 fn extract_matcher(
156 py: Python,
156 py: Python,
157 matcher: PyObject,
157 matcher: PyObject,
158 ) -> PyResult<Box<dyn Matcher + Sync>> {
158 ) -> PyResult<Box<dyn Matcher + Sync>> {
159 match matcher.get_type(py).name(py).borrow() {
159 match matcher.get_type(py).name(py).borrow() {
160 "alwaysmatcher" => Ok(Box::new(AlwaysMatcher)),
160 "alwaysmatcher" => Ok(Box::new(AlwaysMatcher)),
161 "nevermatcher" => Ok(Box::new(NeverMatcher)),
161 "exactmatcher" => {
162 "exactmatcher" => {
162 let files = matcher.call_method(
163 let files = matcher.call_method(
163 py,
164 py,
164 "files",
165 "files",
165 PyTuple::new(py, &[]),
166 PyTuple::new(py, &[]),
166 None,
167 None,
167 )?;
168 )?;
168 let files: PyList = files.cast_into(py)?;
169 let files: PyList = files.cast_into(py)?;
169 let files: PyResult<Vec<HgPathBuf>> = files
170 let files: PyResult<Vec<HgPathBuf>> = files
170 .iter(py)
171 .iter(py)
171 .map(|f| {
172 .map(|f| {
172 Ok(HgPathBuf::from_bytes(
173 Ok(HgPathBuf::from_bytes(
173 f.extract::<PyBytes>(py)?.data(py),
174 f.extract::<PyBytes>(py)?.data(py),
174 ))
175 ))
175 })
176 })
176 .collect();
177 .collect();
177
178
178 let files = files?;
179 let files = files?;
179 let file_matcher = FileMatcher::new(files)
180 let file_matcher = FileMatcher::new(files)
180 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
181 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
181 Ok(Box::new(file_matcher))
182 Ok(Box::new(file_matcher))
182 }
183 }
183 "includematcher" => {
184 "includematcher" => {
184 // Get the patterns from Python even though most of them are
185 // Get the patterns from Python even though most of them are
185 // redundant with those we will parse later on, as they include
186 // redundant with those we will parse later on, as they include
186 // those passed from the command line.
187 // those passed from the command line.
187 let ignore_patterns: PyResult<Vec<_>> = matcher
188 let ignore_patterns: PyResult<Vec<_>> = matcher
188 .getattr(py, "_kindpats")?
189 .getattr(py, "_kindpats")?
189 .iter(py)?
190 .iter(py)?
190 .map(|k| {
191 .map(|k| {
191 let k = k?;
192 let k = k?;
192 let syntax = parse_pattern_syntax(
193 let syntax = parse_pattern_syntax(
193 &[
194 &[
194 k.get_item(py, 0)?
195 k.get_item(py, 0)?
195 .extract::<PyBytes>(py)?
196 .extract::<PyBytes>(py)?
196 .data(py),
197 .data(py),
197 &b":"[..],
198 &b":"[..],
198 ]
199 ]
199 .concat(),
200 .concat(),
200 )
201 )
201 .map_err(|e| {
202 .map_err(|e| {
202 handle_fallback(py, StatusError::Pattern(e))
203 handle_fallback(py, StatusError::Pattern(e))
203 })?;
204 })?;
204 let pattern = k.get_item(py, 1)?.extract::<PyBytes>(py)?;
205 let pattern = k.get_item(py, 1)?.extract::<PyBytes>(py)?;
205 let pattern = pattern.data(py);
206 let pattern = pattern.data(py);
206 let source = k.get_item(py, 2)?.extract::<PyBytes>(py)?;
207 let source = k.get_item(py, 2)?.extract::<PyBytes>(py)?;
207 let source = get_path_from_bytes(source.data(py));
208 let source = get_path_from_bytes(source.data(py));
208 let new = IgnorePattern::new(syntax, pattern, source);
209 let new = IgnorePattern::new(syntax, pattern, source);
209 Ok(new)
210 Ok(new)
210 })
211 })
211 .collect();
212 .collect();
212
213
213 let ignore_patterns = ignore_patterns?;
214 let ignore_patterns = ignore_patterns?;
214
215
215 let matcher = IncludeMatcher::new(ignore_patterns)
216 let matcher = IncludeMatcher::new(ignore_patterns)
216 .map_err(|e| handle_fallback(py, e.into()))?;
217 .map_err(|e| handle_fallback(py, e.into()))?;
217
218
218 Ok(Box::new(matcher))
219 Ok(Box::new(matcher))
219 }
220 }
220 "unionmatcher" => {
221 "unionmatcher" => {
221 let matchers: PyResult<Vec<_>> = matcher
222 let matchers: PyResult<Vec<_>> = matcher
222 .getattr(py, "_matchers")?
223 .getattr(py, "_matchers")?
223 .iter(py)?
224 .iter(py)?
224 .map(|py_matcher| extract_matcher(py, py_matcher?))
225 .map(|py_matcher| extract_matcher(py, py_matcher?))
225 .collect();
226 .collect();
226
227
227 Ok(Box::new(UnionMatcher::new(matchers?)))
228 Ok(Box::new(UnionMatcher::new(matchers?)))
228 }
229 }
229 "intersectionmatcher" => {
230 "intersectionmatcher" => {
230 let m1 = extract_matcher(py, matcher.getattr(py, "_m1")?)?;
231 let m1 = extract_matcher(py, matcher.getattr(py, "_m1")?)?;
231 let m2 = extract_matcher(py, matcher.getattr(py, "_m2")?)?;
232 let m2 = extract_matcher(py, matcher.getattr(py, "_m2")?)?;
232
233
233 Ok(Box::new(IntersectionMatcher::new(m1, m2)))
234 Ok(Box::new(IntersectionMatcher::new(m1, m2)))
234 }
235 }
235 e => Err(PyErr::new::<FallbackError, _>(
236 e => Err(PyErr::new::<FallbackError, _>(
236 py,
237 py,
237 format!("Unsupported matcher {}", e),
238 format!("Unsupported matcher {}", e),
238 )),
239 )),
239 }
240 }
240 }
241 }
241
242
242 fn build_response(
243 fn build_response(
243 py: Python,
244 py: Python,
244 status_res: DirstateStatus,
245 status_res: DirstateStatus,
245 warnings: Vec<PatternFileWarning>,
246 warnings: Vec<PatternFileWarning>,
246 ) -> PyResult<PyTuple> {
247 ) -> PyResult<PyTuple> {
247 let modified = collect_status_path_list(py, &status_res.modified);
248 let modified = collect_status_path_list(py, &status_res.modified);
248 let added = collect_status_path_list(py, &status_res.added);
249 let added = collect_status_path_list(py, &status_res.added);
249 let removed = collect_status_path_list(py, &status_res.removed);
250 let removed = collect_status_path_list(py, &status_res.removed);
250 let deleted = collect_status_path_list(py, &status_res.deleted);
251 let deleted = collect_status_path_list(py, &status_res.deleted);
251 let clean = collect_status_path_list(py, &status_res.clean);
252 let clean = collect_status_path_list(py, &status_res.clean);
252 let ignored = collect_status_path_list(py, &status_res.ignored);
253 let ignored = collect_status_path_list(py, &status_res.ignored);
253 let unknown = collect_status_path_list(py, &status_res.unknown);
254 let unknown = collect_status_path_list(py, &status_res.unknown);
254 let unsure = collect_status_path_list(py, &status_res.unsure);
255 let unsure = collect_status_path_list(py, &status_res.unsure);
255 let bad = collect_bad_matches(py, &status_res.bad)?;
256 let bad = collect_bad_matches(py, &status_res.bad)?;
256 let traversed = collect_pybytes_list(py, status_res.traversed.iter());
257 let traversed = collect_pybytes_list(py, status_res.traversed.iter());
257 let dirty = status_res.dirty.to_py_object(py);
258 let dirty = status_res.dirty.to_py_object(py);
258 let py_warnings = PyList::new(py, &[]);
259 let py_warnings = PyList::new(py, &[]);
259 for warning in warnings.iter() {
260 for warning in warnings.iter() {
260 // We use duck-typing on the Python side for dispatch, good enough for
261 // We use duck-typing on the Python side for dispatch, good enough for
261 // now.
262 // now.
262 match warning {
263 match warning {
263 PatternFileWarning::InvalidSyntax(file, syn) => {
264 PatternFileWarning::InvalidSyntax(file, syn) => {
264 py_warnings.append(
265 py_warnings.append(
265 py,
266 py,
266 (
267 (
267 PyBytes::new(py, &get_bytes_from_path(&file)),
268 PyBytes::new(py, &get_bytes_from_path(&file)),
268 PyBytes::new(py, syn),
269 PyBytes::new(py, syn),
269 )
270 )
270 .to_py_object(py)
271 .to_py_object(py)
271 .into_object(),
272 .into_object(),
272 );
273 );
273 }
274 }
274 PatternFileWarning::NoSuchFile(file) => py_warnings.append(
275 PatternFileWarning::NoSuchFile(file) => py_warnings.append(
275 py,
276 py,
276 PyBytes::new(py, &get_bytes_from_path(&file)).into_object(),
277 PyBytes::new(py, &get_bytes_from_path(&file)).into_object(),
277 ),
278 ),
278 }
279 }
279 }
280 }
280
281
281 Ok(PyTuple::new(
282 Ok(PyTuple::new(
282 py,
283 py,
283 &[
284 &[
284 unsure.into_object(),
285 unsure.into_object(),
285 modified.into_object(),
286 modified.into_object(),
286 added.into_object(),
287 added.into_object(),
287 removed.into_object(),
288 removed.into_object(),
288 deleted.into_object(),
289 deleted.into_object(),
289 clean.into_object(),
290 clean.into_object(),
290 ignored.into_object(),
291 ignored.into_object(),
291 unknown.into_object(),
292 unknown.into_object(),
292 py_warnings.into_object(),
293 py_warnings.into_object(),
293 bad.into_object(),
294 bad.into_object(),
294 traversed.into_object(),
295 traversed.into_object(),
295 dirty.into_object(),
296 dirty.into_object(),
296 ][..],
297 ][..],
297 ))
298 ))
298 }
299 }
General Comments 0
You need to be logged in to leave comments. Login now