##// END OF EJS Templates
rust-dirstate: add `intersectionmatcher` to the allowed matchers...
Raphaël Gomès -
r50246:0b00998e default
parent child Browse files
Show More
@@ -1,1468 +1,1469 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.unionmatcher,
1251 matchmod.unionmatcher,
1251 )
1252 )
1252
1253
1253 if rustmod is None:
1254 if rustmod is None:
1254 use_rust = False
1255 use_rust = False
1255 elif self._checkcase:
1256 elif self._checkcase:
1256 # Case-insensitive filesystems are not handled yet
1257 # Case-insensitive filesystems are not handled yet
1257 use_rust = False
1258 use_rust = False
1258 elif subrepos:
1259 elif subrepos:
1259 use_rust = False
1260 use_rust = False
1260 elif sparse.enabled:
1261 elif sparse.enabled:
1261 use_rust = False
1262 use_rust = False
1262 elif not isinstance(match, allowed_matchers):
1263 elif not isinstance(match, allowed_matchers):
1263 # Some matchers have yet to be implemented
1264 # Some matchers have yet to be implemented
1264 use_rust = False
1265 use_rust = False
1265
1266
1266 # Get the time from the filesystem so we can disambiguate files that
1267 # Get the time from the filesystem so we can disambiguate files that
1267 # appear modified in the present or future.
1268 # appear modified in the present or future.
1268 try:
1269 try:
1269 mtime_boundary = timestamp.get_fs_now(self._opener)
1270 mtime_boundary = timestamp.get_fs_now(self._opener)
1270 except OSError:
1271 except OSError:
1271 # In largefiles or readonly context
1272 # In largefiles or readonly context
1272 mtime_boundary = None
1273 mtime_boundary = None
1273
1274
1274 if use_rust:
1275 if use_rust:
1275 try:
1276 try:
1276 res = self._rust_status(
1277 res = self._rust_status(
1277 match, listclean, listignored, listunknown
1278 match, listclean, listignored, listunknown
1278 )
1279 )
1279 return res + (mtime_boundary,)
1280 return res + (mtime_boundary,)
1280 except rustmod.FallbackError:
1281 except rustmod.FallbackError:
1281 pass
1282 pass
1282
1283
1283 def noop(f):
1284 def noop(f):
1284 pass
1285 pass
1285
1286
1286 dcontains = dmap.__contains__
1287 dcontains = dmap.__contains__
1287 dget = dmap.__getitem__
1288 dget = dmap.__getitem__
1288 ladd = lookup.append # aka "unsure"
1289 ladd = lookup.append # aka "unsure"
1289 madd = modified.append
1290 madd = modified.append
1290 aadd = added.append
1291 aadd = added.append
1291 uadd = unknown.append if listunknown else noop
1292 uadd = unknown.append if listunknown else noop
1292 iadd = ignored.append if listignored else noop
1293 iadd = ignored.append if listignored else noop
1293 radd = removed.append
1294 radd = removed.append
1294 dadd = deleted.append
1295 dadd = deleted.append
1295 cadd = clean.append if listclean else noop
1296 cadd = clean.append if listclean else noop
1296 mexact = match.exact
1297 mexact = match.exact
1297 dirignore = self._dirignore
1298 dirignore = self._dirignore
1298 checkexec = self._checkexec
1299 checkexec = self._checkexec
1299 checklink = self._checklink
1300 checklink = self._checklink
1300 copymap = self._map.copymap
1301 copymap = self._map.copymap
1301
1302
1302 # We need to do full walks when either
1303 # We need to do full walks when either
1303 # - we're listing all clean files, or
1304 # - we're listing all clean files, or
1304 # - match.traversedir does something, because match.traversedir should
1305 # - match.traversedir does something, because match.traversedir should
1305 # be called for every dir in the working dir
1306 # be called for every dir in the working dir
1306 full = listclean or match.traversedir is not None
1307 full = listclean or match.traversedir is not None
1307 for fn, st in self.walk(
1308 for fn, st in self.walk(
1308 match, subrepos, listunknown, listignored, full=full
1309 match, subrepos, listunknown, listignored, full=full
1309 ).items():
1310 ).items():
1310 if not dcontains(fn):
1311 if not dcontains(fn):
1311 if (listignored or mexact(fn)) and dirignore(fn):
1312 if (listignored or mexact(fn)) and dirignore(fn):
1312 if listignored:
1313 if listignored:
1313 iadd(fn)
1314 iadd(fn)
1314 else:
1315 else:
1315 uadd(fn)
1316 uadd(fn)
1316 continue
1317 continue
1317
1318
1318 t = dget(fn)
1319 t = dget(fn)
1319 mode = t.mode
1320 mode = t.mode
1320 size = t.size
1321 size = t.size
1321
1322
1322 if not st and t.tracked:
1323 if not st and t.tracked:
1323 dadd(fn)
1324 dadd(fn)
1324 elif t.p2_info:
1325 elif t.p2_info:
1325 madd(fn)
1326 madd(fn)
1326 elif t.added:
1327 elif t.added:
1327 aadd(fn)
1328 aadd(fn)
1328 elif t.removed:
1329 elif t.removed:
1329 radd(fn)
1330 radd(fn)
1330 elif t.tracked:
1331 elif t.tracked:
1331 if not checklink and t.has_fallback_symlink:
1332 if not checklink and t.has_fallback_symlink:
1332 # If the file system does not support symlink, the mode
1333 # If the file system does not support symlink, the mode
1333 # might not be correctly stored in the dirstate, so do not
1334 # might not be correctly stored in the dirstate, so do not
1334 # trust it.
1335 # trust it.
1335 ladd(fn)
1336 ladd(fn)
1336 elif not checkexec and t.has_fallback_exec:
1337 elif not checkexec and t.has_fallback_exec:
1337 # If the file system does not support exec bits, the mode
1338 # If the file system does not support exec bits, the mode
1338 # might not be correctly stored in the dirstate, so do not
1339 # might not be correctly stored in the dirstate, so do not
1339 # trust it.
1340 # trust it.
1340 ladd(fn)
1341 ladd(fn)
1341 elif (
1342 elif (
1342 size >= 0
1343 size >= 0
1343 and (
1344 and (
1344 (size != st.st_size and size != st.st_size & _rangemask)
1345 (size != st.st_size and size != st.st_size & _rangemask)
1345 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1346 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1346 )
1347 )
1347 or fn in copymap
1348 or fn in copymap
1348 ):
1349 ):
1349 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1350 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1350 # issue6456: Size returned may be longer due to
1351 # issue6456: Size returned may be longer due to
1351 # encryption on EXT-4 fscrypt, undecided.
1352 # encryption on EXT-4 fscrypt, undecided.
1352 ladd(fn)
1353 ladd(fn)
1353 else:
1354 else:
1354 madd(fn)
1355 madd(fn)
1355 elif not t.mtime_likely_equal_to(timestamp.mtime_of(st)):
1356 elif not t.mtime_likely_equal_to(timestamp.mtime_of(st)):
1356 # There might be a change in the future if for example the
1357 # There might be a change in the future if for example the
1357 # internal clock is off, but this is a case where the issues
1358 # internal clock is off, but this is a case where the issues
1358 # the user would face would be a lot worse and there is
1359 # the user would face would be a lot worse and there is
1359 # nothing we can really do.
1360 # nothing we can really do.
1360 ladd(fn)
1361 ladd(fn)
1361 elif listclean:
1362 elif listclean:
1362 cadd(fn)
1363 cadd(fn)
1363 status = scmutil.status(
1364 status = scmutil.status(
1364 modified, added, removed, deleted, unknown, ignored, clean
1365 modified, added, removed, deleted, unknown, ignored, clean
1365 )
1366 )
1366 return (lookup, status, mtime_boundary)
1367 return (lookup, status, mtime_boundary)
1367
1368
1368 def matches(self, match):
1369 def matches(self, match):
1369 """
1370 """
1370 return files in the dirstate (in whatever state) filtered by match
1371 return files in the dirstate (in whatever state) filtered by match
1371 """
1372 """
1372 dmap = self._map
1373 dmap = self._map
1373 if rustmod is not None:
1374 if rustmod is not None:
1374 dmap = self._map._map
1375 dmap = self._map._map
1375
1376
1376 if match.always():
1377 if match.always():
1377 return dmap.keys()
1378 return dmap.keys()
1378 files = match.files()
1379 files = match.files()
1379 if match.isexact():
1380 if match.isexact():
1380 # fast path -- filter the other way around, since typically files is
1381 # fast path -- filter the other way around, since typically files is
1381 # much smaller than dmap
1382 # much smaller than dmap
1382 return [f for f in files if f in dmap]
1383 return [f for f in files if f in dmap]
1383 if match.prefix() and all(fn in dmap for fn in files):
1384 if match.prefix() and all(fn in dmap for fn in files):
1384 # fast path -- all the values are known to be files, so just return
1385 # fast path -- all the values are known to be files, so just return
1385 # that
1386 # that
1386 return list(files)
1387 return list(files)
1387 return [f for f in dmap if match(f)]
1388 return [f for f in dmap if match(f)]
1388
1389
1389 def _actualfilename(self, tr):
1390 def _actualfilename(self, tr):
1390 if tr:
1391 if tr:
1391 return self._pendingfilename
1392 return self._pendingfilename
1392 else:
1393 else:
1393 return self._filename
1394 return self._filename
1394
1395
1395 def savebackup(self, tr, backupname):
1396 def savebackup(self, tr, backupname):
1396 '''Save current dirstate into backup file'''
1397 '''Save current dirstate into backup file'''
1397 filename = self._actualfilename(tr)
1398 filename = self._actualfilename(tr)
1398 assert backupname != filename
1399 assert backupname != filename
1399
1400
1400 # use '_writedirstate' instead of 'write' to write changes certainly,
1401 # use '_writedirstate' instead of 'write' to write changes certainly,
1401 # because the latter omits writing out if transaction is running.
1402 # because the latter omits writing out if transaction is running.
1402 # output file will be used to create backup of dirstate at this point.
1403 # output file will be used to create backup of dirstate at this point.
1403 if self._dirty or not self._opener.exists(filename):
1404 if self._dirty or not self._opener.exists(filename):
1404 self._writedirstate(
1405 self._writedirstate(
1405 tr,
1406 tr,
1406 self._opener(filename, b"w", atomictemp=True, checkambig=True),
1407 self._opener(filename, b"w", atomictemp=True, checkambig=True),
1407 )
1408 )
1408
1409
1409 if tr:
1410 if tr:
1410 # ensure that subsequent tr.writepending returns True for
1411 # ensure that subsequent tr.writepending returns True for
1411 # changes written out above, even if dirstate is never
1412 # changes written out above, even if dirstate is never
1412 # changed after this
1413 # changed after this
1413 tr.addfilegenerator(
1414 tr.addfilegenerator(
1414 b'dirstate-1-main',
1415 b'dirstate-1-main',
1415 (self._filename,),
1416 (self._filename,),
1416 lambda f: self._writedirstate(tr, f),
1417 lambda f: self._writedirstate(tr, f),
1417 location=b'plain',
1418 location=b'plain',
1418 post_finalize=True,
1419 post_finalize=True,
1419 )
1420 )
1420
1421
1421 # ensure that pending file written above is unlinked at
1422 # ensure that pending file written above is unlinked at
1422 # failure, even if tr.writepending isn't invoked until the
1423 # failure, even if tr.writepending isn't invoked until the
1423 # end of this transaction
1424 # end of this transaction
1424 tr.registertmp(filename, location=b'plain')
1425 tr.registertmp(filename, location=b'plain')
1425
1426
1426 self._opener.tryunlink(backupname)
1427 self._opener.tryunlink(backupname)
1427 # hardlink backup is okay because _writedirstate is always called
1428 # hardlink backup is okay because _writedirstate is always called
1428 # with an "atomictemp=True" file.
1429 # with an "atomictemp=True" file.
1429 util.copyfile(
1430 util.copyfile(
1430 self._opener.join(filename),
1431 self._opener.join(filename),
1431 self._opener.join(backupname),
1432 self._opener.join(backupname),
1432 hardlink=True,
1433 hardlink=True,
1433 )
1434 )
1434
1435
1435 def restorebackup(self, tr, backupname):
1436 def restorebackup(self, tr, backupname):
1436 '''Restore dirstate by backup file'''
1437 '''Restore dirstate by backup file'''
1437 # this "invalidate()" prevents "wlock.release()" from writing
1438 # this "invalidate()" prevents "wlock.release()" from writing
1438 # changes of dirstate out after restoring from backup file
1439 # changes of dirstate out after restoring from backup file
1439 self.invalidate()
1440 self.invalidate()
1440 filename = self._actualfilename(tr)
1441 filename = self._actualfilename(tr)
1441 o = self._opener
1442 o = self._opener
1442 if util.samefile(o.join(backupname), o.join(filename)):
1443 if util.samefile(o.join(backupname), o.join(filename)):
1443 o.unlink(backupname)
1444 o.unlink(backupname)
1444 else:
1445 else:
1445 o.rename(backupname, filename, checkambig=True)
1446 o.rename(backupname, filename, checkambig=True)
1446
1447
1447 def clearbackup(self, tr, backupname):
1448 def clearbackup(self, tr, backupname):
1448 '''Clear backup file'''
1449 '''Clear backup file'''
1449 self._opener.unlink(backupname)
1450 self._opener.unlink(backupname)
1450
1451
1451 def verify(self, m1, m2):
1452 def verify(self, m1, m2):
1452 """check the dirstate content again the parent manifest and yield errors"""
1453 """check the dirstate content again the parent manifest and yield errors"""
1453 missing_from_p1 = b"%s in state %s, but not in manifest1\n"
1454 missing_from_p1 = b"%s in state %s, but not in manifest1\n"
1454 unexpected_in_p1 = b"%s in state %s, but also in manifest1\n"
1455 unexpected_in_p1 = b"%s in state %s, but also in manifest1\n"
1455 missing_from_ps = b"%s in state %s, but not in either manifest\n"
1456 missing_from_ps = b"%s in state %s, but not in either manifest\n"
1456 missing_from_ds = b"%s in manifest1, but listed as state %s\n"
1457 missing_from_ds = b"%s in manifest1, but listed as state %s\n"
1457 for f, entry in self.items():
1458 for f, entry in self.items():
1458 state = entry.state
1459 state = entry.state
1459 if state in b"nr" and f not in m1:
1460 if state in b"nr" and f not in m1:
1460 yield (missing_from_p1, f, state)
1461 yield (missing_from_p1, f, state)
1461 if state in b"a" and f in m1:
1462 if state in b"a" and f in m1:
1462 yield (unexpected_in_p1, f, state)
1463 yield (unexpected_in_p1, f, state)
1463 if state in b"m" and f not in m1 and f not in m2:
1464 if state in b"m" and f not in m1 and f not in m2:
1464 yield (missing_from_ps, f, state)
1465 yield (missing_from_ps, f, state)
1465 for f in m1:
1466 for f in m1:
1466 state = self.get_entry(f).state
1467 state = self.get_entry(f).state
1467 if state not in b"nrm":
1468 if state not in b"nrm":
1468 yield (missing_from_ds, f, state)
1469 yield (missing_from_ds, f, state)
@@ -1,292 +1,298 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};
18 use hg::matchers::{Matcher, UnionMatcher, IntersectionMatcher};
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 "exactmatcher" => {
161 "exactmatcher" => {
162 let files = matcher.call_method(
162 let files = matcher.call_method(
163 py,
163 py,
164 "files",
164 "files",
165 PyTuple::new(py, &[]),
165 PyTuple::new(py, &[]),
166 None,
166 None,
167 )?;
167 )?;
168 let files: PyList = files.cast_into(py)?;
168 let files: PyList = files.cast_into(py)?;
169 let files: PyResult<Vec<HgPathBuf>> = files
169 let files: PyResult<Vec<HgPathBuf>> = files
170 .iter(py)
170 .iter(py)
171 .map(|f| {
171 .map(|f| {
172 Ok(HgPathBuf::from_bytes(
172 Ok(HgPathBuf::from_bytes(
173 f.extract::<PyBytes>(py)?.data(py),
173 f.extract::<PyBytes>(py)?.data(py),
174 ))
174 ))
175 })
175 })
176 .collect();
176 .collect();
177
177
178 let files = files?;
178 let files = files?;
179 let file_matcher = FileMatcher::new(files)
179 let file_matcher = FileMatcher::new(files)
180 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
180 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
181 Ok(Box::new(file_matcher))
181 Ok(Box::new(file_matcher))
182 }
182 }
183 "includematcher" => {
183 "includematcher" => {
184 // Get the patterns from Python even though most of them are
184 // Get the patterns from Python even though most of them are
185 // redundant with those we will parse later on, as they include
185 // redundant with those we will parse later on, as they include
186 // those passed from the command line.
186 // those passed from the command line.
187 let ignore_patterns: PyResult<Vec<_>> = matcher
187 let ignore_patterns: PyResult<Vec<_>> = matcher
188 .getattr(py, "_kindpats")?
188 .getattr(py, "_kindpats")?
189 .iter(py)?
189 .iter(py)?
190 .map(|k| {
190 .map(|k| {
191 let k = k?;
191 let k = k?;
192 let syntax = parse_pattern_syntax(
192 let syntax = parse_pattern_syntax(
193 &[
193 &[
194 k.get_item(py, 0)?
194 k.get_item(py, 0)?
195 .extract::<PyBytes>(py)?
195 .extract::<PyBytes>(py)?
196 .data(py),
196 .data(py),
197 &b":"[..],
197 &b":"[..],
198 ]
198 ]
199 .concat(),
199 .concat(),
200 )
200 )
201 .map_err(|e| {
201 .map_err(|e| {
202 handle_fallback(py, StatusError::Pattern(e))
202 handle_fallback(py, StatusError::Pattern(e))
203 })?;
203 })?;
204 let pattern = k.get_item(py, 1)?.extract::<PyBytes>(py)?;
204 let pattern = k.get_item(py, 1)?.extract::<PyBytes>(py)?;
205 let pattern = pattern.data(py);
205 let pattern = pattern.data(py);
206 let source = k.get_item(py, 2)?.extract::<PyBytes>(py)?;
206 let source = k.get_item(py, 2)?.extract::<PyBytes>(py)?;
207 let source = get_path_from_bytes(source.data(py));
207 let source = get_path_from_bytes(source.data(py));
208 let new = IgnorePattern::new(syntax, pattern, source);
208 let new = IgnorePattern::new(syntax, pattern, source);
209 Ok(new)
209 Ok(new)
210 })
210 })
211 .collect();
211 .collect();
212
212
213 let ignore_patterns = ignore_patterns?;
213 let ignore_patterns = ignore_patterns?;
214
214
215 let matcher = IncludeMatcher::new(ignore_patterns)
215 let matcher = IncludeMatcher::new(ignore_patterns)
216 .map_err(|e| handle_fallback(py, e.into()))?;
216 .map_err(|e| handle_fallback(py, e.into()))?;
217
217
218 Ok(Box::new(matcher))
218 Ok(Box::new(matcher))
219 }
219 }
220 "unionmatcher" => {
220 "unionmatcher" => {
221 let matchers: PyResult<Vec<_>> = matcher
221 let matchers: PyResult<Vec<_>> = matcher
222 .getattr(py, "_matchers")?
222 .getattr(py, "_matchers")?
223 .iter(py)?
223 .iter(py)?
224 .map(|py_matcher| extract_matcher(py, py_matcher?))
224 .map(|py_matcher| extract_matcher(py, py_matcher?))
225 .collect();
225 .collect();
226
226
227 Ok(Box::new(UnionMatcher::new(matchers?)))
227 Ok(Box::new(UnionMatcher::new(matchers?)))
228 }
228 }
229 "intersectionmatcher" => {
230 let m1 = extract_matcher(py, matcher.getattr(py, "_m1")?)?;
231 let m2 = extract_matcher(py, matcher.getattr(py, "_m2")?)?;
232
233 Ok(Box::new(IntersectionMatcher::new(m1, m2)))
234 }
229 e => Err(PyErr::new::<FallbackError, _>(
235 e => Err(PyErr::new::<FallbackError, _>(
230 py,
236 py,
231 format!("Unsupported matcher {}", e),
237 format!("Unsupported matcher {}", e),
232 )),
238 )),
233 }
239 }
234 }
240 }
235
241
236 fn build_response(
242 fn build_response(
237 py: Python,
243 py: Python,
238 status_res: DirstateStatus,
244 status_res: DirstateStatus,
239 warnings: Vec<PatternFileWarning>,
245 warnings: Vec<PatternFileWarning>,
240 ) -> PyResult<PyTuple> {
246 ) -> PyResult<PyTuple> {
241 let modified = collect_status_path_list(py, &status_res.modified);
247 let modified = collect_status_path_list(py, &status_res.modified);
242 let added = collect_status_path_list(py, &status_res.added);
248 let added = collect_status_path_list(py, &status_res.added);
243 let removed = collect_status_path_list(py, &status_res.removed);
249 let removed = collect_status_path_list(py, &status_res.removed);
244 let deleted = collect_status_path_list(py, &status_res.deleted);
250 let deleted = collect_status_path_list(py, &status_res.deleted);
245 let clean = collect_status_path_list(py, &status_res.clean);
251 let clean = collect_status_path_list(py, &status_res.clean);
246 let ignored = collect_status_path_list(py, &status_res.ignored);
252 let ignored = collect_status_path_list(py, &status_res.ignored);
247 let unknown = collect_status_path_list(py, &status_res.unknown);
253 let unknown = collect_status_path_list(py, &status_res.unknown);
248 let unsure = collect_status_path_list(py, &status_res.unsure);
254 let unsure = collect_status_path_list(py, &status_res.unsure);
249 let bad = collect_bad_matches(py, &status_res.bad)?;
255 let bad = collect_bad_matches(py, &status_res.bad)?;
250 let traversed = collect_pybytes_list(py, status_res.traversed.iter());
256 let traversed = collect_pybytes_list(py, status_res.traversed.iter());
251 let dirty = status_res.dirty.to_py_object(py);
257 let dirty = status_res.dirty.to_py_object(py);
252 let py_warnings = PyList::new(py, &[]);
258 let py_warnings = PyList::new(py, &[]);
253 for warning in warnings.iter() {
259 for warning in warnings.iter() {
254 // We use duck-typing on the Python side for dispatch, good enough for
260 // We use duck-typing on the Python side for dispatch, good enough for
255 // now.
261 // now.
256 match warning {
262 match warning {
257 PatternFileWarning::InvalidSyntax(file, syn) => {
263 PatternFileWarning::InvalidSyntax(file, syn) => {
258 py_warnings.append(
264 py_warnings.append(
259 py,
265 py,
260 (
266 (
261 PyBytes::new(py, &get_bytes_from_path(&file)),
267 PyBytes::new(py, &get_bytes_from_path(&file)),
262 PyBytes::new(py, syn),
268 PyBytes::new(py, syn),
263 )
269 )
264 .to_py_object(py)
270 .to_py_object(py)
265 .into_object(),
271 .into_object(),
266 );
272 );
267 }
273 }
268 PatternFileWarning::NoSuchFile(file) => py_warnings.append(
274 PatternFileWarning::NoSuchFile(file) => py_warnings.append(
269 py,
275 py,
270 PyBytes::new(py, &get_bytes_from_path(&file)).into_object(),
276 PyBytes::new(py, &get_bytes_from_path(&file)).into_object(),
271 ),
277 ),
272 }
278 }
273 }
279 }
274
280
275 Ok(PyTuple::new(
281 Ok(PyTuple::new(
276 py,
282 py,
277 &[
283 &[
278 unsure.into_object(),
284 unsure.into_object(),
279 modified.into_object(),
285 modified.into_object(),
280 added.into_object(),
286 added.into_object(),
281 removed.into_object(),
287 removed.into_object(),
282 deleted.into_object(),
288 deleted.into_object(),
283 clean.into_object(),
289 clean.into_object(),
284 ignored.into_object(),
290 ignored.into_object(),
285 unknown.into_object(),
291 unknown.into_object(),
286 py_warnings.into_object(),
292 py_warnings.into_object(),
287 bad.into_object(),
293 bad.into_object(),
288 traversed.into_object(),
294 traversed.into_object(),
289 dirty.into_object(),
295 dirty.into_object(),
290 ][..],
296 ][..],
291 ))
297 ))
292 }
298 }
General Comments 0
You need to be logged in to leave comments. Login now