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