##// END OF EJS Templates
dirstate: remove `lastnormaltime` mechanism...
Raphaël Gomès -
r49214:a19d1225 default draft
parent child Browse files
Show More
@@ -1,1515 +1,1491 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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import collections
10 import collections
11 import contextlib
11 import contextlib
12 import errno
12 import errno
13 import os
13 import os
14 import stat
14 import stat
15
15
16 from .i18n import _
16 from .i18n import _
17 from .pycompat import delattr
17 from .pycompat import delattr
18
18
19 from hgdemandimport import tracing
19 from hgdemandimport import tracing
20
20
21 from . import (
21 from . import (
22 dirstatemap,
22 dirstatemap,
23 encoding,
23 encoding,
24 error,
24 error,
25 match as matchmod,
25 match as matchmod,
26 pathutil,
26 pathutil,
27 policy,
27 policy,
28 pycompat,
28 pycompat,
29 scmutil,
29 scmutil,
30 sparse,
30 sparse,
31 util,
31 util,
32 )
32 )
33
33
34 from .dirstateutils import (
34 from .dirstateutils import (
35 timestamp,
35 timestamp,
36 )
36 )
37
37
38 from .interfaces import (
38 from .interfaces import (
39 dirstate as intdirstate,
39 dirstate as intdirstate,
40 util as interfaceutil,
40 util as interfaceutil,
41 )
41 )
42
42
43 parsers = policy.importmod('parsers')
43 parsers = policy.importmod('parsers')
44 rustmod = policy.importrust('dirstate')
44 rustmod = policy.importrust('dirstate')
45
45
46 HAS_FAST_DIRSTATE_V2 = rustmod is not None
46 HAS_FAST_DIRSTATE_V2 = rustmod is not None
47
47
48 propertycache = util.propertycache
48 propertycache = util.propertycache
49 filecache = scmutil.filecache
49 filecache = scmutil.filecache
50 _rangemask = dirstatemap.rangemask
50 _rangemask = dirstatemap.rangemask
51
51
52 DirstateItem = dirstatemap.DirstateItem
52 DirstateItem = dirstatemap.DirstateItem
53
53
54
54
55 class repocache(filecache):
55 class repocache(filecache):
56 """filecache for files in .hg/"""
56 """filecache for files in .hg/"""
57
57
58 def join(self, obj, fname):
58 def join(self, obj, fname):
59 return obj._opener.join(fname)
59 return obj._opener.join(fname)
60
60
61
61
62 class rootcache(filecache):
62 class rootcache(filecache):
63 """filecache for files in the repository root"""
63 """filecache for files in the repository root"""
64
64
65 def join(self, obj, fname):
65 def join(self, obj, fname):
66 return obj._join(fname)
66 return obj._join(fname)
67
67
68
68
69 def requires_parents_change(func):
69 def requires_parents_change(func):
70 def wrap(self, *args, **kwargs):
70 def wrap(self, *args, **kwargs):
71 if not self.pendingparentchange():
71 if not self.pendingparentchange():
72 msg = 'calling `%s` outside of a parentchange context'
72 msg = 'calling `%s` outside of a parentchange context'
73 msg %= func.__name__
73 msg %= func.__name__
74 raise error.ProgrammingError(msg)
74 raise error.ProgrammingError(msg)
75 return func(self, *args, **kwargs)
75 return func(self, *args, **kwargs)
76
76
77 return wrap
77 return wrap
78
78
79
79
80 def requires_no_parents_change(func):
80 def requires_no_parents_change(func):
81 def wrap(self, *args, **kwargs):
81 def wrap(self, *args, **kwargs):
82 if self.pendingparentchange():
82 if self.pendingparentchange():
83 msg = 'calling `%s` inside of a parentchange context'
83 msg = 'calling `%s` inside of a parentchange context'
84 msg %= func.__name__
84 msg %= func.__name__
85 raise error.ProgrammingError(msg)
85 raise error.ProgrammingError(msg)
86 return func(self, *args, **kwargs)
86 return func(self, *args, **kwargs)
87
87
88 return wrap
88 return wrap
89
89
90
90
91 @interfaceutil.implementer(intdirstate.idirstate)
91 @interfaceutil.implementer(intdirstate.idirstate)
92 class dirstate(object):
92 class dirstate(object):
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 ):
102 ):
103 """Create a new dirstate object.
103 """Create a new dirstate object.
104
104
105 opener is an open()-like callable that can be used to open the
105 opener is an open()-like callable that can be used to open the
106 dirstate file; root is the root of the directory tracked by
106 dirstate file; root is the root of the directory tracked by
107 the dirstate.
107 the dirstate.
108 """
108 """
109 self._use_dirstate_v2 = use_dirstate_v2
109 self._use_dirstate_v2 = use_dirstate_v2
110 self._nodeconstants = nodeconstants
110 self._nodeconstants = nodeconstants
111 self._opener = opener
111 self._opener = opener
112 self._validate = validate
112 self._validate = validate
113 self._root = root
113 self._root = root
114 self._sparsematchfn = sparsematchfn
114 self._sparsematchfn = sparsematchfn
115 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
115 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
116 # UNC path pointing to root share (issue4557)
116 # UNC path pointing to root share (issue4557)
117 self._rootdir = pathutil.normasprefix(root)
117 self._rootdir = pathutil.normasprefix(root)
118 self._dirty = False
118 self._dirty = False
119 self._lastnormaltime = timestamp.zero()
120 self._ui = ui
119 self._ui = ui
121 self._filecache = {}
120 self._filecache = {}
122 self._parentwriters = 0
121 self._parentwriters = 0
123 self._filename = b'dirstate'
122 self._filename = b'dirstate'
124 self._pendingfilename = b'%s.pending' % self._filename
123 self._pendingfilename = b'%s.pending' % self._filename
125 self._plchangecallbacks = {}
124 self._plchangecallbacks = {}
126 self._origpl = None
125 self._origpl = None
127 self._mapcls = dirstatemap.dirstatemap
126 self._mapcls = dirstatemap.dirstatemap
128 # Access and cache cwd early, so we don't access it for the first time
127 # Access and cache cwd early, so we don't access it for the first time
129 # after a working-copy update caused it to not exist (accessing it then
128 # after a working-copy update caused it to not exist (accessing it then
130 # raises an exception).
129 # raises an exception).
131 self._cwd
130 self._cwd
132
131
133 def prefetch_parents(self):
132 def prefetch_parents(self):
134 """make sure the parents are loaded
133 """make sure the parents are loaded
135
134
136 Used to avoid a race condition.
135 Used to avoid a race condition.
137 """
136 """
138 self._pl
137 self._pl
139
138
140 @contextlib.contextmanager
139 @contextlib.contextmanager
141 def parentchange(self):
140 def parentchange(self):
142 """Context manager for handling dirstate parents.
141 """Context manager for handling dirstate parents.
143
142
144 If an exception occurs in the scope of the context manager,
143 If an exception occurs in the scope of the context manager,
145 the incoherent dirstate won't be written when wlock is
144 the incoherent dirstate won't be written when wlock is
146 released.
145 released.
147 """
146 """
148 self._parentwriters += 1
147 self._parentwriters += 1
149 yield
148 yield
150 # Typically we want the "undo" step of a context manager in a
149 # Typically we want the "undo" step of a context manager in a
151 # finally block so it happens even when an exception
150 # finally block so it happens even when an exception
152 # occurs. In this case, however, we only want to decrement
151 # occurs. In this case, however, we only want to decrement
153 # parentwriters if the code in the with statement exits
152 # parentwriters if the code in the with statement exits
154 # normally, so we don't have a try/finally here on purpose.
153 # normally, so we don't have a try/finally here on purpose.
155 self._parentwriters -= 1
154 self._parentwriters -= 1
156
155
157 def pendingparentchange(self):
156 def pendingparentchange(self):
158 """Returns true if the dirstate is in the middle of a set of changes
157 """Returns true if the dirstate is in the middle of a set of changes
159 that modify the dirstate parent.
158 that modify the dirstate parent.
160 """
159 """
161 return self._parentwriters > 0
160 return self._parentwriters > 0
162
161
163 @propertycache
162 @propertycache
164 def _map(self):
163 def _map(self):
165 """Return the dirstate contents (see documentation for dirstatemap)."""
164 """Return the dirstate contents (see documentation for dirstatemap)."""
166 self._map = self._mapcls(
165 self._map = self._mapcls(
167 self._ui,
166 self._ui,
168 self._opener,
167 self._opener,
169 self._root,
168 self._root,
170 self._nodeconstants,
169 self._nodeconstants,
171 self._use_dirstate_v2,
170 self._use_dirstate_v2,
172 )
171 )
173 return self._map
172 return self._map
174
173
175 @property
174 @property
176 def _sparsematcher(self):
175 def _sparsematcher(self):
177 """The matcher for the sparse checkout.
176 """The matcher for the sparse checkout.
178
177
179 The working directory may not include every file from a manifest. The
178 The working directory may not include every file from a manifest. The
180 matcher obtained by this property will match a path if it is to be
179 matcher obtained by this property will match a path if it is to be
181 included in the working directory.
180 included in the working directory.
182 """
181 """
183 # TODO there is potential to cache this property. For now, the matcher
182 # TODO there is potential to cache this property. For now, the matcher
184 # is resolved on every access. (But the called function does use a
183 # is resolved on every access. (But the called function does use a
185 # cache to keep the lookup fast.)
184 # cache to keep the lookup fast.)
186 return self._sparsematchfn()
185 return self._sparsematchfn()
187
186
188 @repocache(b'branch')
187 @repocache(b'branch')
189 def _branch(self):
188 def _branch(self):
190 try:
189 try:
191 return self._opener.read(b"branch").strip() or b"default"
190 return self._opener.read(b"branch").strip() or b"default"
192 except IOError as inst:
191 except IOError as inst:
193 if inst.errno != errno.ENOENT:
192 if inst.errno != errno.ENOENT:
194 raise
193 raise
195 return b"default"
194 return b"default"
196
195
197 @property
196 @property
198 def _pl(self):
197 def _pl(self):
199 return self._map.parents()
198 return self._map.parents()
200
199
201 def hasdir(self, d):
200 def hasdir(self, d):
202 return self._map.hastrackeddir(d)
201 return self._map.hastrackeddir(d)
203
202
204 @rootcache(b'.hgignore')
203 @rootcache(b'.hgignore')
205 def _ignore(self):
204 def _ignore(self):
206 files = self._ignorefiles()
205 files = self._ignorefiles()
207 if not files:
206 if not files:
208 return matchmod.never()
207 return matchmod.never()
209
208
210 pats = [b'include:%s' % f for f in files]
209 pats = [b'include:%s' % f for f in files]
211 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
210 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
212
211
213 @propertycache
212 @propertycache
214 def _slash(self):
213 def _slash(self):
215 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
214 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
216
215
217 @propertycache
216 @propertycache
218 def _checklink(self):
217 def _checklink(self):
219 return util.checklink(self._root)
218 return util.checklink(self._root)
220
219
221 @propertycache
220 @propertycache
222 def _checkexec(self):
221 def _checkexec(self):
223 return bool(util.checkexec(self._root))
222 return bool(util.checkexec(self._root))
224
223
225 @propertycache
224 @propertycache
226 def _checkcase(self):
225 def _checkcase(self):
227 return not util.fscasesensitive(self._join(b'.hg'))
226 return not util.fscasesensitive(self._join(b'.hg'))
228
227
229 def _join(self, f):
228 def _join(self, f):
230 # much faster than os.path.join()
229 # much faster than os.path.join()
231 # it's safe because f is always a relative path
230 # it's safe because f is always a relative path
232 return self._rootdir + f
231 return self._rootdir + f
233
232
234 def flagfunc(self, buildfallback):
233 def flagfunc(self, buildfallback):
235 """build a callable that returns flags associated with a filename
234 """build a callable that returns flags associated with a filename
236
235
237 The information is extracted from three possible layers:
236 The information is extracted from three possible layers:
238 1. the file system if it supports the information
237 1. the file system if it supports the information
239 2. the "fallback" information stored in the dirstate if any
238 2. the "fallback" information stored in the dirstate if any
240 3. a more expensive mechanism inferring the flags from the parents.
239 3. a more expensive mechanism inferring the flags from the parents.
241 """
240 """
242
241
243 # small hack to cache the result of buildfallback()
242 # small hack to cache the result of buildfallback()
244 fallback_func = []
243 fallback_func = []
245
244
246 def get_flags(x):
245 def get_flags(x):
247 entry = None
246 entry = None
248 fallback_value = None
247 fallback_value = None
249 try:
248 try:
250 st = os.lstat(self._join(x))
249 st = os.lstat(self._join(x))
251 except OSError:
250 except OSError:
252 return b''
251 return b''
253
252
254 if self._checklink:
253 if self._checklink:
255 if util.statislink(st):
254 if util.statislink(st):
256 return b'l'
255 return b'l'
257 else:
256 else:
258 entry = self.get_entry(x)
257 entry = self.get_entry(x)
259 if entry.has_fallback_symlink:
258 if entry.has_fallback_symlink:
260 if entry.fallback_symlink:
259 if entry.fallback_symlink:
261 return b'l'
260 return b'l'
262 else:
261 else:
263 if not fallback_func:
262 if not fallback_func:
264 fallback_func.append(buildfallback())
263 fallback_func.append(buildfallback())
265 fallback_value = fallback_func[0](x)
264 fallback_value = fallback_func[0](x)
266 if b'l' in fallback_value:
265 if b'l' in fallback_value:
267 return b'l'
266 return b'l'
268
267
269 if self._checkexec:
268 if self._checkexec:
270 if util.statisexec(st):
269 if util.statisexec(st):
271 return b'x'
270 return b'x'
272 else:
271 else:
273 if entry is None:
272 if entry is None:
274 entry = self.get_entry(x)
273 entry = self.get_entry(x)
275 if entry.has_fallback_exec:
274 if entry.has_fallback_exec:
276 if entry.fallback_exec:
275 if entry.fallback_exec:
277 return b'x'
276 return b'x'
278 else:
277 else:
279 if fallback_value is None:
278 if fallback_value is None:
280 if not fallback_func:
279 if not fallback_func:
281 fallback_func.append(buildfallback())
280 fallback_func.append(buildfallback())
282 fallback_value = fallback_func[0](x)
281 fallback_value = fallback_func[0](x)
283 if b'x' in fallback_value:
282 if b'x' in fallback_value:
284 return b'x'
283 return b'x'
285 return b''
284 return b''
286
285
287 return get_flags
286 return get_flags
288
287
289 @propertycache
288 @propertycache
290 def _cwd(self):
289 def _cwd(self):
291 # internal config: ui.forcecwd
290 # internal config: ui.forcecwd
292 forcecwd = self._ui.config(b'ui', b'forcecwd')
291 forcecwd = self._ui.config(b'ui', b'forcecwd')
293 if forcecwd:
292 if forcecwd:
294 return forcecwd
293 return forcecwd
295 return encoding.getcwd()
294 return encoding.getcwd()
296
295
297 def getcwd(self):
296 def getcwd(self):
298 """Return the path from which a canonical path is calculated.
297 """Return the path from which a canonical path is calculated.
299
298
300 This path should be used to resolve file patterns or to convert
299 This path should be used to resolve file patterns or to convert
301 canonical paths back to file paths for display. It shouldn't be
300 canonical paths back to file paths for display. It shouldn't be
302 used to get real file paths. Use vfs functions instead.
301 used to get real file paths. Use vfs functions instead.
303 """
302 """
304 cwd = self._cwd
303 cwd = self._cwd
305 if cwd == self._root:
304 if cwd == self._root:
306 return b''
305 return b''
307 # self._root ends with a path separator if self._root is '/' or 'C:\'
306 # self._root ends with a path separator if self._root is '/' or 'C:\'
308 rootsep = self._root
307 rootsep = self._root
309 if not util.endswithsep(rootsep):
308 if not util.endswithsep(rootsep):
310 rootsep += pycompat.ossep
309 rootsep += pycompat.ossep
311 if cwd.startswith(rootsep):
310 if cwd.startswith(rootsep):
312 return cwd[len(rootsep) :]
311 return cwd[len(rootsep) :]
313 else:
312 else:
314 # we're outside the repo. return an absolute path.
313 # we're outside the repo. return an absolute path.
315 return cwd
314 return cwd
316
315
317 def pathto(self, f, cwd=None):
316 def pathto(self, f, cwd=None):
318 if cwd is None:
317 if cwd is None:
319 cwd = self.getcwd()
318 cwd = self.getcwd()
320 path = util.pathto(self._root, cwd, f)
319 path = util.pathto(self._root, cwd, f)
321 if self._slash:
320 if self._slash:
322 return util.pconvert(path)
321 return util.pconvert(path)
323 return path
322 return path
324
323
325 def __getitem__(self, key):
324 def __getitem__(self, key):
326 """Return the current state of key (a filename) in the dirstate.
325 """Return the current state of key (a filename) in the dirstate.
327
326
328 States are:
327 States are:
329 n normal
328 n normal
330 m needs merging
329 m needs merging
331 r marked for removal
330 r marked for removal
332 a marked for addition
331 a marked for addition
333 ? not tracked
332 ? not tracked
334
333
335 XXX The "state" is a bit obscure to be in the "public" API. we should
334 XXX The "state" is a bit obscure to be in the "public" API. we should
336 consider migrating all user of this to going through the dirstate entry
335 consider migrating all user of this to going through the dirstate entry
337 instead.
336 instead.
338 """
337 """
339 msg = b"don't use dirstate[file], use dirstate.get_entry(file)"
338 msg = b"don't use dirstate[file], use dirstate.get_entry(file)"
340 util.nouideprecwarn(msg, b'6.1', stacklevel=2)
339 util.nouideprecwarn(msg, b'6.1', stacklevel=2)
341 entry = self._map.get(key)
340 entry = self._map.get(key)
342 if entry is not None:
341 if entry is not None:
343 return entry.state
342 return entry.state
344 return b'?'
343 return b'?'
345
344
346 def get_entry(self, path):
345 def get_entry(self, path):
347 """return a DirstateItem for the associated path"""
346 """return a DirstateItem for the associated path"""
348 entry = self._map.get(path)
347 entry = self._map.get(path)
349 if entry is None:
348 if entry is None:
350 return DirstateItem()
349 return DirstateItem()
351 return entry
350 return entry
352
351
353 def __contains__(self, key):
352 def __contains__(self, key):
354 return key in self._map
353 return key in self._map
355
354
356 def __iter__(self):
355 def __iter__(self):
357 return iter(sorted(self._map))
356 return iter(sorted(self._map))
358
357
359 def items(self):
358 def items(self):
360 return pycompat.iteritems(self._map)
359 return pycompat.iteritems(self._map)
361
360
362 iteritems = items
361 iteritems = items
363
362
364 def parents(self):
363 def parents(self):
365 return [self._validate(p) for p in self._pl]
364 return [self._validate(p) for p in self._pl]
366
365
367 def p1(self):
366 def p1(self):
368 return self._validate(self._pl[0])
367 return self._validate(self._pl[0])
369
368
370 def p2(self):
369 def p2(self):
371 return self._validate(self._pl[1])
370 return self._validate(self._pl[1])
372
371
373 @property
372 @property
374 def in_merge(self):
373 def in_merge(self):
375 """True if a merge is in progress"""
374 """True if a merge is in progress"""
376 return self._pl[1] != self._nodeconstants.nullid
375 return self._pl[1] != self._nodeconstants.nullid
377
376
378 def branch(self):
377 def branch(self):
379 return encoding.tolocal(self._branch)
378 return encoding.tolocal(self._branch)
380
379
381 def setparents(self, p1, p2=None):
380 def setparents(self, p1, p2=None):
382 """Set dirstate parents to p1 and p2.
381 """Set dirstate parents to p1 and p2.
383
382
384 When moving from two parents to one, "merged" entries a
383 When moving from two parents to one, "merged" entries a
385 adjusted to normal and previous copy records discarded and
384 adjusted to normal and previous copy records discarded and
386 returned by the call.
385 returned by the call.
387
386
388 See localrepo.setparents()
387 See localrepo.setparents()
389 """
388 """
390 if p2 is None:
389 if p2 is None:
391 p2 = self._nodeconstants.nullid
390 p2 = self._nodeconstants.nullid
392 if self._parentwriters == 0:
391 if self._parentwriters == 0:
393 raise ValueError(
392 raise ValueError(
394 b"cannot set dirstate parent outside of "
393 b"cannot set dirstate parent outside of "
395 b"dirstate.parentchange context manager"
394 b"dirstate.parentchange context manager"
396 )
395 )
397
396
398 self._dirty = True
397 self._dirty = True
399 oldp2 = self._pl[1]
398 oldp2 = self._pl[1]
400 if self._origpl is None:
399 if self._origpl is None:
401 self._origpl = self._pl
400 self._origpl = self._pl
402 nullid = self._nodeconstants.nullid
401 nullid = self._nodeconstants.nullid
403 # True if we need to fold p2 related state back to a linear case
402 # True if we need to fold p2 related state back to a linear case
404 fold_p2 = oldp2 != nullid and p2 == nullid
403 fold_p2 = oldp2 != nullid and p2 == nullid
405 return self._map.setparents(p1, p2, fold_p2=fold_p2)
404 return self._map.setparents(p1, p2, fold_p2=fold_p2)
406
405
407 def setbranch(self, branch):
406 def setbranch(self, branch):
408 self.__class__._branch.set(self, encoding.fromlocal(branch))
407 self.__class__._branch.set(self, encoding.fromlocal(branch))
409 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
408 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
410 try:
409 try:
411 f.write(self._branch + b'\n')
410 f.write(self._branch + b'\n')
412 f.close()
411 f.close()
413
412
414 # make sure filecache has the correct stat info for _branch after
413 # make sure filecache has the correct stat info for _branch after
415 # replacing the underlying file
414 # replacing the underlying file
416 ce = self._filecache[b'_branch']
415 ce = self._filecache[b'_branch']
417 if ce:
416 if ce:
418 ce.refresh()
417 ce.refresh()
419 except: # re-raises
418 except: # re-raises
420 f.discard()
419 f.discard()
421 raise
420 raise
422
421
423 def invalidate(self):
422 def invalidate(self):
424 """Causes the next access to reread the dirstate.
423 """Causes the next access to reread the dirstate.
425
424
426 This is different from localrepo.invalidatedirstate() because it always
425 This is different from localrepo.invalidatedirstate() because it always
427 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
426 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
428 check whether the dirstate has changed before rereading it."""
427 check whether the dirstate has changed before rereading it."""
429
428
430 for a in ("_map", "_branch", "_ignore"):
429 for a in ("_map", "_branch", "_ignore"):
431 if a in self.__dict__:
430 if a in self.__dict__:
432 delattr(self, a)
431 delattr(self, a)
433 self._lastnormaltime = timestamp.zero()
434 self._dirty = False
432 self._dirty = False
435 self._parentwriters = 0
433 self._parentwriters = 0
436 self._origpl = None
434 self._origpl = None
437
435
438 def copy(self, source, dest):
436 def copy(self, source, dest):
439 """Mark dest as a copy of source. Unmark dest if source is None."""
437 """Mark dest as a copy of source. Unmark dest if source is None."""
440 if source == dest:
438 if source == dest:
441 return
439 return
442 self._dirty = True
440 self._dirty = True
443 if source is not None:
441 if source is not None:
444 self._map.copymap[dest] = source
442 self._map.copymap[dest] = source
445 else:
443 else:
446 self._map.copymap.pop(dest, None)
444 self._map.copymap.pop(dest, None)
447
445
448 def copied(self, file):
446 def copied(self, file):
449 return self._map.copymap.get(file, None)
447 return self._map.copymap.get(file, None)
450
448
451 def copies(self):
449 def copies(self):
452 return self._map.copymap
450 return self._map.copymap
453
451
454 @requires_no_parents_change
452 @requires_no_parents_change
455 def set_tracked(self, filename, reset_copy=False):
453 def set_tracked(self, filename, reset_copy=False):
456 """a "public" method for generic code to mark a file as tracked
454 """a "public" method for generic code to mark a file as tracked
457
455
458 This function is to be called outside of "update/merge" case. For
456 This function is to be called outside of "update/merge" case. For
459 example by a command like `hg add X`.
457 example by a command like `hg add X`.
460
458
461 if reset_copy is set, any existing copy information will be dropped.
459 if reset_copy is set, any existing copy information will be dropped.
462
460
463 return True the file was previously untracked, False otherwise.
461 return True the file was previously untracked, False otherwise.
464 """
462 """
465 self._dirty = True
463 self._dirty = True
466 entry = self._map.get(filename)
464 entry = self._map.get(filename)
467 if entry is None or not entry.tracked:
465 if entry is None or not entry.tracked:
468 self._check_new_tracked_filename(filename)
466 self._check_new_tracked_filename(filename)
469 pre_tracked = self._map.set_tracked(filename)
467 pre_tracked = self._map.set_tracked(filename)
470 if reset_copy:
468 if reset_copy:
471 self._map.copymap.pop(filename, None)
469 self._map.copymap.pop(filename, None)
472 return pre_tracked
470 return pre_tracked
473
471
474 @requires_no_parents_change
472 @requires_no_parents_change
475 def set_untracked(self, filename):
473 def set_untracked(self, filename):
476 """a "public" method for generic code to mark a file as untracked
474 """a "public" method for generic code to mark a file as untracked
477
475
478 This function is to be called outside of "update/merge" case. For
476 This function is to be called outside of "update/merge" case. For
479 example by a command like `hg remove X`.
477 example by a command like `hg remove X`.
480
478
481 return True the file was previously tracked, False otherwise.
479 return True the file was previously tracked, False otherwise.
482 """
480 """
483 ret = self._map.set_untracked(filename)
481 ret = self._map.set_untracked(filename)
484 if ret:
482 if ret:
485 self._dirty = True
483 self._dirty = True
486 return ret
484 return ret
487
485
488 @requires_no_parents_change
486 @requires_no_parents_change
489 def set_clean(self, filename, parentfiledata):
487 def set_clean(self, filename, parentfiledata):
490 """record that the current state of the file on disk is known to be clean"""
488 """record that the current state of the file on disk is known to be clean"""
491 self._dirty = True
489 self._dirty = True
492 if not self._map[filename].tracked:
490 if not self._map[filename].tracked:
493 self._check_new_tracked_filename(filename)
491 self._check_new_tracked_filename(filename)
494 (mode, size, mtime) = parentfiledata
492 (mode, size, mtime) = parentfiledata
495 self._map.set_clean(filename, mode, size, mtime)
493 self._map.set_clean(filename, mode, size, mtime)
496 if mtime > self._lastnormaltime:
497 # Remember the most recent modification timeslot for status(),
498 # to make sure we won't miss future size-preserving file content
499 # modifications that happen within the same timeslot.
500 self._lastnormaltime = mtime
501
494
502 @requires_no_parents_change
495 @requires_no_parents_change
503 def set_possibly_dirty(self, filename):
496 def set_possibly_dirty(self, filename):
504 """record that the current state of the file on disk is unknown"""
497 """record that the current state of the file on disk is unknown"""
505 self._dirty = True
498 self._dirty = True
506 self._map.set_possibly_dirty(filename)
499 self._map.set_possibly_dirty(filename)
507
500
508 @requires_parents_change
501 @requires_parents_change
509 def update_file_p1(
502 def update_file_p1(
510 self,
503 self,
511 filename,
504 filename,
512 p1_tracked,
505 p1_tracked,
513 ):
506 ):
514 """Set a file as tracked in the parent (or not)
507 """Set a file as tracked in the parent (or not)
515
508
516 This is to be called when adjust the dirstate to a new parent after an history
509 This is to be called when adjust the dirstate to a new parent after an history
517 rewriting operation.
510 rewriting operation.
518
511
519 It should not be called during a merge (p2 != nullid) and only within
512 It should not be called during a merge (p2 != nullid) and only within
520 a `with dirstate.parentchange():` context.
513 a `with dirstate.parentchange():` context.
521 """
514 """
522 if self.in_merge:
515 if self.in_merge:
523 msg = b'update_file_reference should not be called when merging'
516 msg = b'update_file_reference should not be called when merging'
524 raise error.ProgrammingError(msg)
517 raise error.ProgrammingError(msg)
525 entry = self._map.get(filename)
518 entry = self._map.get(filename)
526 if entry is None:
519 if entry is None:
527 wc_tracked = False
520 wc_tracked = False
528 else:
521 else:
529 wc_tracked = entry.tracked
522 wc_tracked = entry.tracked
530 if not (p1_tracked or wc_tracked):
523 if not (p1_tracked or wc_tracked):
531 # the file is no longer relevant to anyone
524 # the file is no longer relevant to anyone
532 if self._map.get(filename) is not None:
525 if self._map.get(filename) is not None:
533 self._map.reset_state(filename)
526 self._map.reset_state(filename)
534 self._dirty = True
527 self._dirty = True
535 elif (not p1_tracked) and wc_tracked:
528 elif (not p1_tracked) and wc_tracked:
536 if entry is not None and entry.added:
529 if entry is not None and entry.added:
537 return # avoid dropping copy information (maybe?)
530 return # avoid dropping copy information (maybe?)
538
531
539 self._map.reset_state(
532 self._map.reset_state(
540 filename,
533 filename,
541 wc_tracked,
534 wc_tracked,
542 p1_tracked,
535 p1_tracked,
543 # the underlying reference might have changed, we will have to
536 # the underlying reference might have changed, we will have to
544 # check it.
537 # check it.
545 has_meaningful_mtime=False,
538 has_meaningful_mtime=False,
546 )
539 )
547
540
548 @requires_parents_change
541 @requires_parents_change
549 def update_file(
542 def update_file(
550 self,
543 self,
551 filename,
544 filename,
552 wc_tracked,
545 wc_tracked,
553 p1_tracked,
546 p1_tracked,
554 p2_info=False,
547 p2_info=False,
555 possibly_dirty=False,
548 possibly_dirty=False,
556 parentfiledata=None,
549 parentfiledata=None,
557 ):
550 ):
558 """update the information about a file in the dirstate
551 """update the information about a file in the dirstate
559
552
560 This is to be called when the direstates parent changes to keep track
553 This is to be called when the direstates parent changes to keep track
561 of what is the file situation in regards to the working copy and its parent.
554 of what is the file situation in regards to the working copy and its parent.
562
555
563 This function must be called within a `dirstate.parentchange` context.
556 This function must be called within a `dirstate.parentchange` context.
564
557
565 note: the API is at an early stage and we might need to adjust it
558 note: the API is at an early stage and we might need to adjust it
566 depending of what information ends up being relevant and useful to
559 depending of what information ends up being relevant and useful to
567 other processing.
560 other processing.
568 """
561 """
569
562
570 # note: I do not think we need to double check name clash here since we
563 # note: I do not think we need to double check name clash here since we
571 # are in a update/merge case that should already have taken care of
564 # are in a update/merge case that should already have taken care of
572 # this. The test agrees
565 # this. The test agrees
573
566
574 self._dirty = True
567 self._dirty = True
575
568
576 self._map.reset_state(
569 self._map.reset_state(
577 filename,
570 filename,
578 wc_tracked,
571 wc_tracked,
579 p1_tracked,
572 p1_tracked,
580 p2_info=p2_info,
573 p2_info=p2_info,
581 has_meaningful_mtime=not possibly_dirty,
574 has_meaningful_mtime=not possibly_dirty,
582 parentfiledata=parentfiledata,
575 parentfiledata=parentfiledata,
583 )
576 )
584 if (
585 parentfiledata is not None
586 and parentfiledata[2] is not None
587 and parentfiledata[2] > self._lastnormaltime
588 ):
589 # Remember the most recent modification timeslot for status(),
590 # to make sure we won't miss future size-preserving file content
591 # modifications that happen within the same timeslot.
592 self._lastnormaltime = parentfiledata[2]
593
577
594 def _check_new_tracked_filename(self, filename):
578 def _check_new_tracked_filename(self, filename):
595 scmutil.checkfilename(filename)
579 scmutil.checkfilename(filename)
596 if self._map.hastrackeddir(filename):
580 if self._map.hastrackeddir(filename):
597 msg = _(b'directory %r already in dirstate')
581 msg = _(b'directory %r already in dirstate')
598 msg %= pycompat.bytestr(filename)
582 msg %= pycompat.bytestr(filename)
599 raise error.Abort(msg)
583 raise error.Abort(msg)
600 # shadows
584 # shadows
601 for d in pathutil.finddirs(filename):
585 for d in pathutil.finddirs(filename):
602 if self._map.hastrackeddir(d):
586 if self._map.hastrackeddir(d):
603 break
587 break
604 entry = self._map.get(d)
588 entry = self._map.get(d)
605 if entry is not None and not entry.removed:
589 if entry is not None and not entry.removed:
606 msg = _(b'file %r in dirstate clashes with %r')
590 msg = _(b'file %r in dirstate clashes with %r')
607 msg %= (pycompat.bytestr(d), pycompat.bytestr(filename))
591 msg %= (pycompat.bytestr(d), pycompat.bytestr(filename))
608 raise error.Abort(msg)
592 raise error.Abort(msg)
609
593
610 def _get_filedata(self, filename):
594 def _get_filedata(self, filename):
611 """returns"""
595 """returns"""
612 s = os.lstat(self._join(filename))
596 s = os.lstat(self._join(filename))
613 mode = s.st_mode
597 mode = s.st_mode
614 size = s.st_size
598 size = s.st_size
615 mtime = timestamp.mtime_of(s)
599 mtime = timestamp.mtime_of(s)
616 return (mode, size, mtime)
600 return (mode, size, mtime)
617
601
618 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
602 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
619 if exists is None:
603 if exists is None:
620 exists = os.path.lexists(os.path.join(self._root, path))
604 exists = os.path.lexists(os.path.join(self._root, path))
621 if not exists:
605 if not exists:
622 # Maybe a path component exists
606 # Maybe a path component exists
623 if not ignoremissing and b'/' in path:
607 if not ignoremissing and b'/' in path:
624 d, f = path.rsplit(b'/', 1)
608 d, f = path.rsplit(b'/', 1)
625 d = self._normalize(d, False, ignoremissing, None)
609 d = self._normalize(d, False, ignoremissing, None)
626 folded = d + b"/" + f
610 folded = d + b"/" + f
627 else:
611 else:
628 # No path components, preserve original case
612 # No path components, preserve original case
629 folded = path
613 folded = path
630 else:
614 else:
631 # recursively normalize leading directory components
615 # recursively normalize leading directory components
632 # against dirstate
616 # against dirstate
633 if b'/' in normed:
617 if b'/' in normed:
634 d, f = normed.rsplit(b'/', 1)
618 d, f = normed.rsplit(b'/', 1)
635 d = self._normalize(d, False, ignoremissing, True)
619 d = self._normalize(d, False, ignoremissing, True)
636 r = self._root + b"/" + d
620 r = self._root + b"/" + d
637 folded = d + b"/" + util.fspath(f, r)
621 folded = d + b"/" + util.fspath(f, r)
638 else:
622 else:
639 folded = util.fspath(normed, self._root)
623 folded = util.fspath(normed, self._root)
640 storemap[normed] = folded
624 storemap[normed] = folded
641
625
642 return folded
626 return folded
643
627
644 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
628 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
645 normed = util.normcase(path)
629 normed = util.normcase(path)
646 folded = self._map.filefoldmap.get(normed, None)
630 folded = self._map.filefoldmap.get(normed, None)
647 if folded is None:
631 if folded is None:
648 if isknown:
632 if isknown:
649 folded = path
633 folded = path
650 else:
634 else:
651 folded = self._discoverpath(
635 folded = self._discoverpath(
652 path, normed, ignoremissing, exists, self._map.filefoldmap
636 path, normed, ignoremissing, exists, self._map.filefoldmap
653 )
637 )
654 return folded
638 return folded
655
639
656 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
640 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
657 normed = util.normcase(path)
641 normed = util.normcase(path)
658 folded = self._map.filefoldmap.get(normed, None)
642 folded = self._map.filefoldmap.get(normed, None)
659 if folded is None:
643 if folded is None:
660 folded = self._map.dirfoldmap.get(normed, None)
644 folded = self._map.dirfoldmap.get(normed, None)
661 if folded is None:
645 if folded is None:
662 if isknown:
646 if isknown:
663 folded = path
647 folded = path
664 else:
648 else:
665 # store discovered result in dirfoldmap so that future
649 # store discovered result in dirfoldmap so that future
666 # normalizefile calls don't start matching directories
650 # normalizefile calls don't start matching directories
667 folded = self._discoverpath(
651 folded = self._discoverpath(
668 path, normed, ignoremissing, exists, self._map.dirfoldmap
652 path, normed, ignoremissing, exists, self._map.dirfoldmap
669 )
653 )
670 return folded
654 return folded
671
655
672 def normalize(self, path, isknown=False, ignoremissing=False):
656 def normalize(self, path, isknown=False, ignoremissing=False):
673 """
657 """
674 normalize the case of a pathname when on a casefolding filesystem
658 normalize the case of a pathname when on a casefolding filesystem
675
659
676 isknown specifies whether the filename came from walking the
660 isknown specifies whether the filename came from walking the
677 disk, to avoid extra filesystem access.
661 disk, to avoid extra filesystem access.
678
662
679 If ignoremissing is True, missing path are returned
663 If ignoremissing is True, missing path are returned
680 unchanged. Otherwise, we try harder to normalize possibly
664 unchanged. Otherwise, we try harder to normalize possibly
681 existing path components.
665 existing path components.
682
666
683 The normalized case is determined based on the following precedence:
667 The normalized case is determined based on the following precedence:
684
668
685 - version of name already stored in the dirstate
669 - version of name already stored in the dirstate
686 - version of name stored on disk
670 - version of name stored on disk
687 - version provided via command arguments
671 - version provided via command arguments
688 """
672 """
689
673
690 if self._checkcase:
674 if self._checkcase:
691 return self._normalize(path, isknown, ignoremissing)
675 return self._normalize(path, isknown, ignoremissing)
692 return path
676 return path
693
677
694 def clear(self):
678 def clear(self):
695 self._map.clear()
679 self._map.clear()
696 self._lastnormaltime = timestamp.zero()
697 self._dirty = True
680 self._dirty = True
698
681
699 def rebuild(self, parent, allfiles, changedfiles=None):
682 def rebuild(self, parent, allfiles, changedfiles=None):
700 if changedfiles is None:
683 if changedfiles is None:
701 # Rebuild entire dirstate
684 # Rebuild entire dirstate
702 to_lookup = allfiles
685 to_lookup = allfiles
703 to_drop = []
686 to_drop = []
704 lastnormaltime = self._lastnormaltime
705 self.clear()
687 self.clear()
706 self._lastnormaltime = lastnormaltime
707 elif len(changedfiles) < 10:
688 elif len(changedfiles) < 10:
708 # Avoid turning allfiles into a set, which can be expensive if it's
689 # Avoid turning allfiles into a set, which can be expensive if it's
709 # large.
690 # large.
710 to_lookup = []
691 to_lookup = []
711 to_drop = []
692 to_drop = []
712 for f in changedfiles:
693 for f in changedfiles:
713 if f in allfiles:
694 if f in allfiles:
714 to_lookup.append(f)
695 to_lookup.append(f)
715 else:
696 else:
716 to_drop.append(f)
697 to_drop.append(f)
717 else:
698 else:
718 changedfilesset = set(changedfiles)
699 changedfilesset = set(changedfiles)
719 to_lookup = changedfilesset & set(allfiles)
700 to_lookup = changedfilesset & set(allfiles)
720 to_drop = changedfilesset - to_lookup
701 to_drop = changedfilesset - to_lookup
721
702
722 if self._origpl is None:
703 if self._origpl is None:
723 self._origpl = self._pl
704 self._origpl = self._pl
724 self._map.setparents(parent, self._nodeconstants.nullid)
705 self._map.setparents(parent, self._nodeconstants.nullid)
725
706
726 for f in to_lookup:
707 for f in to_lookup:
727
708
728 if self.in_merge:
709 if self.in_merge:
729 self.set_tracked(f)
710 self.set_tracked(f)
730 else:
711 else:
731 self._map.reset_state(
712 self._map.reset_state(
732 f,
713 f,
733 wc_tracked=True,
714 wc_tracked=True,
734 p1_tracked=True,
715 p1_tracked=True,
735 )
716 )
736 for f in to_drop:
717 for f in to_drop:
737 self._map.reset_state(f)
718 self._map.reset_state(f)
738
719
739 self._dirty = True
720 self._dirty = True
740
721
741 def identity(self):
722 def identity(self):
742 """Return identity of dirstate itself to detect changing in storage
723 """Return identity of dirstate itself to detect changing in storage
743
724
744 If identity of previous dirstate is equal to this, writing
725 If identity of previous dirstate is equal to this, writing
745 changes based on the former dirstate out can keep consistency.
726 changes based on the former dirstate out can keep consistency.
746 """
727 """
747 return self._map.identity
728 return self._map.identity
748
729
749 def write(self, tr):
730 def write(self, tr):
750 if not self._dirty:
731 if not self._dirty:
751 return
732 return
752
733
753 filename = self._filename
734 filename = self._filename
754 if tr:
735 if tr:
755 # 'dirstate.write()' is not only for writing in-memory
736 # 'dirstate.write()' is not only for writing in-memory
756 # changes out, but also for dropping ambiguous timestamp.
737 # changes out, but also for dropping ambiguous timestamp.
757 # delayed writing re-raise "ambiguous timestamp issue".
738 # delayed writing re-raise "ambiguous timestamp issue".
758 # See also the wiki page below for detail:
739 # See also the wiki page below for detail:
759 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
740 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
760
741
761 # record when mtime start to be ambiguous
742 # record when mtime start to be ambiguous
762 now = timestamp.get_fs_now(self._opener)
743 now = timestamp.get_fs_now(self._opener)
763
744
764 # delay writing in-memory changes out
745 # delay writing in-memory changes out
765 tr.addfilegenerator(
746 tr.addfilegenerator(
766 b'dirstate',
747 b'dirstate',
767 (self._filename,),
748 (self._filename,),
768 lambda f: self._writedirstate(tr, f, now=now),
749 lambda f: self._writedirstate(tr, f, now=now),
769 location=b'plain',
750 location=b'plain',
770 )
751 )
771 return
752 return
772
753
773 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
754 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
774 self._writedirstate(tr, st)
755 self._writedirstate(tr, st)
775
756
776 def addparentchangecallback(self, category, callback):
757 def addparentchangecallback(self, category, callback):
777 """add a callback to be called when the wd parents are changed
758 """add a callback to be called when the wd parents are changed
778
759
779 Callback will be called with the following arguments:
760 Callback will be called with the following arguments:
780 dirstate, (oldp1, oldp2), (newp1, newp2)
761 dirstate, (oldp1, oldp2), (newp1, newp2)
781
762
782 Category is a unique identifier to allow overwriting an old callback
763 Category is a unique identifier to allow overwriting an old callback
783 with a newer callback.
764 with a newer callback.
784 """
765 """
785 self._plchangecallbacks[category] = callback
766 self._plchangecallbacks[category] = callback
786
767
787 def _writedirstate(self, tr, st, now=None):
768 def _writedirstate(self, tr, st, now=None):
788 # notify callbacks about parents change
769 # notify callbacks about parents change
789 if self._origpl is not None and self._origpl != self._pl:
770 if self._origpl is not None and self._origpl != self._pl:
790 for c, callback in sorted(
771 for c, callback in sorted(
791 pycompat.iteritems(self._plchangecallbacks)
772 pycompat.iteritems(self._plchangecallbacks)
792 ):
773 ):
793 callback(self, self._origpl, self._pl)
774 callback(self, self._origpl, self._pl)
794 self._origpl = None
775 self._origpl = None
795
776
796 if now is None:
777 if now is None:
797 # use the modification time of the newly created temporary file as the
778 # use the modification time of the newly created temporary file as the
798 # filesystem's notion of 'now'
779 # filesystem's notion of 'now'
799 now = timestamp.mtime_of(util.fstat(st))
780 now = timestamp.mtime_of(util.fstat(st))
800
781
801 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
782 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
802 # timestamp of each entries in dirstate, because of 'now > mtime'
783 # timestamp of each entries in dirstate, because of 'now > mtime'
803 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
784 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
804 if delaywrite > 0:
785 if delaywrite > 0:
805 # do we have any files to delay for?
786 # do we have any files to delay for?
806 for f, e in pycompat.iteritems(self._map):
787 for f, e in pycompat.iteritems(self._map):
807 if e.need_delay(now):
788 if e.need_delay(now):
808 import time # to avoid useless import
789 import time # to avoid useless import
809
790
810 # rather than sleep n seconds, sleep until the next
791 # rather than sleep n seconds, sleep until the next
811 # multiple of n seconds
792 # multiple of n seconds
812 clock = time.time()
793 clock = time.time()
813 start = int(clock) - (int(clock) % delaywrite)
794 start = int(clock) - (int(clock) % delaywrite)
814 end = start + delaywrite
795 end = start + delaywrite
815 time.sleep(end - clock)
796 time.sleep(end - clock)
816 # trust our estimate that the end is near now
797 # trust our estimate that the end is near now
817 now = timestamp.timestamp((end, 0))
798 now = timestamp.timestamp((end, 0))
818 break
799 break
819
800
820 self._map.write(tr, st, now)
801 self._map.write(tr, st, now)
821 self._lastnormaltime = timestamp.zero()
822 self._dirty = False
802 self._dirty = False
823
803
824 def _dirignore(self, f):
804 def _dirignore(self, f):
825 if self._ignore(f):
805 if self._ignore(f):
826 return True
806 return True
827 for p in pathutil.finddirs(f):
807 for p in pathutil.finddirs(f):
828 if self._ignore(p):
808 if self._ignore(p):
829 return True
809 return True
830 return False
810 return False
831
811
832 def _ignorefiles(self):
812 def _ignorefiles(self):
833 files = []
813 files = []
834 if os.path.exists(self._join(b'.hgignore')):
814 if os.path.exists(self._join(b'.hgignore')):
835 files.append(self._join(b'.hgignore'))
815 files.append(self._join(b'.hgignore'))
836 for name, path in self._ui.configitems(b"ui"):
816 for name, path in self._ui.configitems(b"ui"):
837 if name == b'ignore' or name.startswith(b'ignore.'):
817 if name == b'ignore' or name.startswith(b'ignore.'):
838 # we need to use os.path.join here rather than self._join
818 # we need to use os.path.join here rather than self._join
839 # because path is arbitrary and user-specified
819 # because path is arbitrary and user-specified
840 files.append(os.path.join(self._rootdir, util.expandpath(path)))
820 files.append(os.path.join(self._rootdir, util.expandpath(path)))
841 return files
821 return files
842
822
843 def _ignorefileandline(self, f):
823 def _ignorefileandline(self, f):
844 files = collections.deque(self._ignorefiles())
824 files = collections.deque(self._ignorefiles())
845 visited = set()
825 visited = set()
846 while files:
826 while files:
847 i = files.popleft()
827 i = files.popleft()
848 patterns = matchmod.readpatternfile(
828 patterns = matchmod.readpatternfile(
849 i, self._ui.warn, sourceinfo=True
829 i, self._ui.warn, sourceinfo=True
850 )
830 )
851 for pattern, lineno, line in patterns:
831 for pattern, lineno, line in patterns:
852 kind, p = matchmod._patsplit(pattern, b'glob')
832 kind, p = matchmod._patsplit(pattern, b'glob')
853 if kind == b"subinclude":
833 if kind == b"subinclude":
854 if p not in visited:
834 if p not in visited:
855 files.append(p)
835 files.append(p)
856 continue
836 continue
857 m = matchmod.match(
837 m = matchmod.match(
858 self._root, b'', [], [pattern], warn=self._ui.warn
838 self._root, b'', [], [pattern], warn=self._ui.warn
859 )
839 )
860 if m(f):
840 if m(f):
861 return (i, lineno, line)
841 return (i, lineno, line)
862 visited.add(i)
842 visited.add(i)
863 return (None, -1, b"")
843 return (None, -1, b"")
864
844
865 def _walkexplicit(self, match, subrepos):
845 def _walkexplicit(self, match, subrepos):
866 """Get stat data about the files explicitly specified by match.
846 """Get stat data about the files explicitly specified by match.
867
847
868 Return a triple (results, dirsfound, dirsnotfound).
848 Return a triple (results, dirsfound, dirsnotfound).
869 - results is a mapping from filename to stat result. It also contains
849 - results is a mapping from filename to stat result. It also contains
870 listings mapping subrepos and .hg to None.
850 listings mapping subrepos and .hg to None.
871 - dirsfound is a list of files found to be directories.
851 - dirsfound is a list of files found to be directories.
872 - dirsnotfound is a list of files that the dirstate thinks are
852 - dirsnotfound is a list of files that the dirstate thinks are
873 directories and that were not found."""
853 directories and that were not found."""
874
854
875 def badtype(mode):
855 def badtype(mode):
876 kind = _(b'unknown')
856 kind = _(b'unknown')
877 if stat.S_ISCHR(mode):
857 if stat.S_ISCHR(mode):
878 kind = _(b'character device')
858 kind = _(b'character device')
879 elif stat.S_ISBLK(mode):
859 elif stat.S_ISBLK(mode):
880 kind = _(b'block device')
860 kind = _(b'block device')
881 elif stat.S_ISFIFO(mode):
861 elif stat.S_ISFIFO(mode):
882 kind = _(b'fifo')
862 kind = _(b'fifo')
883 elif stat.S_ISSOCK(mode):
863 elif stat.S_ISSOCK(mode):
884 kind = _(b'socket')
864 kind = _(b'socket')
885 elif stat.S_ISDIR(mode):
865 elif stat.S_ISDIR(mode):
886 kind = _(b'directory')
866 kind = _(b'directory')
887 return _(b'unsupported file type (type is %s)') % kind
867 return _(b'unsupported file type (type is %s)') % kind
888
868
889 badfn = match.bad
869 badfn = match.bad
890 dmap = self._map
870 dmap = self._map
891 lstat = os.lstat
871 lstat = os.lstat
892 getkind = stat.S_IFMT
872 getkind = stat.S_IFMT
893 dirkind = stat.S_IFDIR
873 dirkind = stat.S_IFDIR
894 regkind = stat.S_IFREG
874 regkind = stat.S_IFREG
895 lnkkind = stat.S_IFLNK
875 lnkkind = stat.S_IFLNK
896 join = self._join
876 join = self._join
897 dirsfound = []
877 dirsfound = []
898 foundadd = dirsfound.append
878 foundadd = dirsfound.append
899 dirsnotfound = []
879 dirsnotfound = []
900 notfoundadd = dirsnotfound.append
880 notfoundadd = dirsnotfound.append
901
881
902 if not match.isexact() and self._checkcase:
882 if not match.isexact() and self._checkcase:
903 normalize = self._normalize
883 normalize = self._normalize
904 else:
884 else:
905 normalize = None
885 normalize = None
906
886
907 files = sorted(match.files())
887 files = sorted(match.files())
908 subrepos.sort()
888 subrepos.sort()
909 i, j = 0, 0
889 i, j = 0, 0
910 while i < len(files) and j < len(subrepos):
890 while i < len(files) and j < len(subrepos):
911 subpath = subrepos[j] + b"/"
891 subpath = subrepos[j] + b"/"
912 if files[i] < subpath:
892 if files[i] < subpath:
913 i += 1
893 i += 1
914 continue
894 continue
915 while i < len(files) and files[i].startswith(subpath):
895 while i < len(files) and files[i].startswith(subpath):
916 del files[i]
896 del files[i]
917 j += 1
897 j += 1
918
898
919 if not files or b'' in files:
899 if not files or b'' in files:
920 files = [b'']
900 files = [b'']
921 # constructing the foldmap is expensive, so don't do it for the
901 # constructing the foldmap is expensive, so don't do it for the
922 # common case where files is ['']
902 # common case where files is ['']
923 normalize = None
903 normalize = None
924 results = dict.fromkeys(subrepos)
904 results = dict.fromkeys(subrepos)
925 results[b'.hg'] = None
905 results[b'.hg'] = None
926
906
927 for ff in files:
907 for ff in files:
928 if normalize:
908 if normalize:
929 nf = normalize(ff, False, True)
909 nf = normalize(ff, False, True)
930 else:
910 else:
931 nf = ff
911 nf = ff
932 if nf in results:
912 if nf in results:
933 continue
913 continue
934
914
935 try:
915 try:
936 st = lstat(join(nf))
916 st = lstat(join(nf))
937 kind = getkind(st.st_mode)
917 kind = getkind(st.st_mode)
938 if kind == dirkind:
918 if kind == dirkind:
939 if nf in dmap:
919 if nf in dmap:
940 # file replaced by dir on disk but still in dirstate
920 # file replaced by dir on disk but still in dirstate
941 results[nf] = None
921 results[nf] = None
942 foundadd((nf, ff))
922 foundadd((nf, ff))
943 elif kind == regkind or kind == lnkkind:
923 elif kind == regkind or kind == lnkkind:
944 results[nf] = st
924 results[nf] = st
945 else:
925 else:
946 badfn(ff, badtype(kind))
926 badfn(ff, badtype(kind))
947 if nf in dmap:
927 if nf in dmap:
948 results[nf] = None
928 results[nf] = None
949 except OSError as inst: # nf not found on disk - it is dirstate only
929 except OSError as inst: # nf not found on disk - it is dirstate only
950 if nf in dmap: # does it exactly match a missing file?
930 if nf in dmap: # does it exactly match a missing file?
951 results[nf] = None
931 results[nf] = None
952 else: # does it match a missing directory?
932 else: # does it match a missing directory?
953 if self._map.hasdir(nf):
933 if self._map.hasdir(nf):
954 notfoundadd(nf)
934 notfoundadd(nf)
955 else:
935 else:
956 badfn(ff, encoding.strtolocal(inst.strerror))
936 badfn(ff, encoding.strtolocal(inst.strerror))
957
937
958 # match.files() may contain explicitly-specified paths that shouldn't
938 # match.files() may contain explicitly-specified paths that shouldn't
959 # be taken; drop them from the list of files found. dirsfound/notfound
939 # be taken; drop them from the list of files found. dirsfound/notfound
960 # aren't filtered here because they will be tested later.
940 # aren't filtered here because they will be tested later.
961 if match.anypats():
941 if match.anypats():
962 for f in list(results):
942 for f in list(results):
963 if f == b'.hg' or f in subrepos:
943 if f == b'.hg' or f in subrepos:
964 # keep sentinel to disable further out-of-repo walks
944 # keep sentinel to disable further out-of-repo walks
965 continue
945 continue
966 if not match(f):
946 if not match(f):
967 del results[f]
947 del results[f]
968
948
969 # Case insensitive filesystems cannot rely on lstat() failing to detect
949 # Case insensitive filesystems cannot rely on lstat() failing to detect
970 # a case-only rename. Prune the stat object for any file that does not
950 # a case-only rename. Prune the stat object for any file that does not
971 # match the case in the filesystem, if there are multiple files that
951 # match the case in the filesystem, if there are multiple files that
972 # normalize to the same path.
952 # normalize to the same path.
973 if match.isexact() and self._checkcase:
953 if match.isexact() and self._checkcase:
974 normed = {}
954 normed = {}
975
955
976 for f, st in pycompat.iteritems(results):
956 for f, st in pycompat.iteritems(results):
977 if st is None:
957 if st is None:
978 continue
958 continue
979
959
980 nc = util.normcase(f)
960 nc = util.normcase(f)
981 paths = normed.get(nc)
961 paths = normed.get(nc)
982
962
983 if paths is None:
963 if paths is None:
984 paths = set()
964 paths = set()
985 normed[nc] = paths
965 normed[nc] = paths
986
966
987 paths.add(f)
967 paths.add(f)
988
968
989 for norm, paths in pycompat.iteritems(normed):
969 for norm, paths in pycompat.iteritems(normed):
990 if len(paths) > 1:
970 if len(paths) > 1:
991 for path in paths:
971 for path in paths:
992 folded = self._discoverpath(
972 folded = self._discoverpath(
993 path, norm, True, None, self._map.dirfoldmap
973 path, norm, True, None, self._map.dirfoldmap
994 )
974 )
995 if path != folded:
975 if path != folded:
996 results[path] = None
976 results[path] = None
997
977
998 return results, dirsfound, dirsnotfound
978 return results, dirsfound, dirsnotfound
999
979
1000 def walk(self, match, subrepos, unknown, ignored, full=True):
980 def walk(self, match, subrepos, unknown, ignored, full=True):
1001 """
981 """
1002 Walk recursively through the directory tree, finding all files
982 Walk recursively through the directory tree, finding all files
1003 matched by match.
983 matched by match.
1004
984
1005 If full is False, maybe skip some known-clean files.
985 If full is False, maybe skip some known-clean files.
1006
986
1007 Return a dict mapping filename to stat-like object (either
987 Return a dict mapping filename to stat-like object (either
1008 mercurial.osutil.stat instance or return value of os.stat()).
988 mercurial.osutil.stat instance or return value of os.stat()).
1009
989
1010 """
990 """
1011 # full is a flag that extensions that hook into walk can use -- this
991 # full is a flag that extensions that hook into walk can use -- this
1012 # implementation doesn't use it at all. This satisfies the contract
992 # implementation doesn't use it at all. This satisfies the contract
1013 # because we only guarantee a "maybe".
993 # because we only guarantee a "maybe".
1014
994
1015 if ignored:
995 if ignored:
1016 ignore = util.never
996 ignore = util.never
1017 dirignore = util.never
997 dirignore = util.never
1018 elif unknown:
998 elif unknown:
1019 ignore = self._ignore
999 ignore = self._ignore
1020 dirignore = self._dirignore
1000 dirignore = self._dirignore
1021 else:
1001 else:
1022 # if not unknown and not ignored, drop dir recursion and step 2
1002 # if not unknown and not ignored, drop dir recursion and step 2
1023 ignore = util.always
1003 ignore = util.always
1024 dirignore = util.always
1004 dirignore = util.always
1025
1005
1026 matchfn = match.matchfn
1006 matchfn = match.matchfn
1027 matchalways = match.always()
1007 matchalways = match.always()
1028 matchtdir = match.traversedir
1008 matchtdir = match.traversedir
1029 dmap = self._map
1009 dmap = self._map
1030 listdir = util.listdir
1010 listdir = util.listdir
1031 lstat = os.lstat
1011 lstat = os.lstat
1032 dirkind = stat.S_IFDIR
1012 dirkind = stat.S_IFDIR
1033 regkind = stat.S_IFREG
1013 regkind = stat.S_IFREG
1034 lnkkind = stat.S_IFLNK
1014 lnkkind = stat.S_IFLNK
1035 join = self._join
1015 join = self._join
1036
1016
1037 exact = skipstep3 = False
1017 exact = skipstep3 = False
1038 if match.isexact(): # match.exact
1018 if match.isexact(): # match.exact
1039 exact = True
1019 exact = True
1040 dirignore = util.always # skip step 2
1020 dirignore = util.always # skip step 2
1041 elif match.prefix(): # match.match, no patterns
1021 elif match.prefix(): # match.match, no patterns
1042 skipstep3 = True
1022 skipstep3 = True
1043
1023
1044 if not exact and self._checkcase:
1024 if not exact and self._checkcase:
1045 normalize = self._normalize
1025 normalize = self._normalize
1046 normalizefile = self._normalizefile
1026 normalizefile = self._normalizefile
1047 skipstep3 = False
1027 skipstep3 = False
1048 else:
1028 else:
1049 normalize = self._normalize
1029 normalize = self._normalize
1050 normalizefile = None
1030 normalizefile = None
1051
1031
1052 # step 1: find all explicit files
1032 # step 1: find all explicit files
1053 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1033 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1054 if matchtdir:
1034 if matchtdir:
1055 for d in work:
1035 for d in work:
1056 matchtdir(d[0])
1036 matchtdir(d[0])
1057 for d in dirsnotfound:
1037 for d in dirsnotfound:
1058 matchtdir(d)
1038 matchtdir(d)
1059
1039
1060 skipstep3 = skipstep3 and not (work or dirsnotfound)
1040 skipstep3 = skipstep3 and not (work or dirsnotfound)
1061 work = [d for d in work if not dirignore(d[0])]
1041 work = [d for d in work if not dirignore(d[0])]
1062
1042
1063 # step 2: visit subdirectories
1043 # step 2: visit subdirectories
1064 def traverse(work, alreadynormed):
1044 def traverse(work, alreadynormed):
1065 wadd = work.append
1045 wadd = work.append
1066 while work:
1046 while work:
1067 tracing.counter('dirstate.walk work', len(work))
1047 tracing.counter('dirstate.walk work', len(work))
1068 nd = work.pop()
1048 nd = work.pop()
1069 visitentries = match.visitchildrenset(nd)
1049 visitentries = match.visitchildrenset(nd)
1070 if not visitentries:
1050 if not visitentries:
1071 continue
1051 continue
1072 if visitentries == b'this' or visitentries == b'all':
1052 if visitentries == b'this' or visitentries == b'all':
1073 visitentries = None
1053 visitentries = None
1074 skip = None
1054 skip = None
1075 if nd != b'':
1055 if nd != b'':
1076 skip = b'.hg'
1056 skip = b'.hg'
1077 try:
1057 try:
1078 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1058 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1079 entries = listdir(join(nd), stat=True, skip=skip)
1059 entries = listdir(join(nd), stat=True, skip=skip)
1080 except OSError as inst:
1060 except OSError as inst:
1081 if inst.errno in (errno.EACCES, errno.ENOENT):
1061 if inst.errno in (errno.EACCES, errno.ENOENT):
1082 match.bad(
1062 match.bad(
1083 self.pathto(nd), encoding.strtolocal(inst.strerror)
1063 self.pathto(nd), encoding.strtolocal(inst.strerror)
1084 )
1064 )
1085 continue
1065 continue
1086 raise
1066 raise
1087 for f, kind, st in entries:
1067 for f, kind, st in entries:
1088 # Some matchers may return files in the visitentries set,
1068 # Some matchers may return files in the visitentries set,
1089 # instead of 'this', if the matcher explicitly mentions them
1069 # instead of 'this', if the matcher explicitly mentions them
1090 # and is not an exactmatcher. This is acceptable; we do not
1070 # and is not an exactmatcher. This is acceptable; we do not
1091 # make any hard assumptions about file-or-directory below
1071 # make any hard assumptions about file-or-directory below
1092 # based on the presence of `f` in visitentries. If
1072 # based on the presence of `f` in visitentries. If
1093 # visitchildrenset returned a set, we can always skip the
1073 # visitchildrenset returned a set, we can always skip the
1094 # entries *not* in the set it provided regardless of whether
1074 # entries *not* in the set it provided regardless of whether
1095 # they're actually a file or a directory.
1075 # they're actually a file or a directory.
1096 if visitentries and f not in visitentries:
1076 if visitentries and f not in visitentries:
1097 continue
1077 continue
1098 if normalizefile:
1078 if normalizefile:
1099 # even though f might be a directory, we're only
1079 # even though f might be a directory, we're only
1100 # interested in comparing it to files currently in the
1080 # interested in comparing it to files currently in the
1101 # dmap -- therefore normalizefile is enough
1081 # dmap -- therefore normalizefile is enough
1102 nf = normalizefile(
1082 nf = normalizefile(
1103 nd and (nd + b"/" + f) or f, True, True
1083 nd and (nd + b"/" + f) or f, True, True
1104 )
1084 )
1105 else:
1085 else:
1106 nf = nd and (nd + b"/" + f) or f
1086 nf = nd and (nd + b"/" + f) or f
1107 if nf not in results:
1087 if nf not in results:
1108 if kind == dirkind:
1088 if kind == dirkind:
1109 if not ignore(nf):
1089 if not ignore(nf):
1110 if matchtdir:
1090 if matchtdir:
1111 matchtdir(nf)
1091 matchtdir(nf)
1112 wadd(nf)
1092 wadd(nf)
1113 if nf in dmap and (matchalways or matchfn(nf)):
1093 if nf in dmap and (matchalways or matchfn(nf)):
1114 results[nf] = None
1094 results[nf] = None
1115 elif kind == regkind or kind == lnkkind:
1095 elif kind == regkind or kind == lnkkind:
1116 if nf in dmap:
1096 if nf in dmap:
1117 if matchalways or matchfn(nf):
1097 if matchalways or matchfn(nf):
1118 results[nf] = st
1098 results[nf] = st
1119 elif (matchalways or matchfn(nf)) and not ignore(
1099 elif (matchalways or matchfn(nf)) and not ignore(
1120 nf
1100 nf
1121 ):
1101 ):
1122 # unknown file -- normalize if necessary
1102 # unknown file -- normalize if necessary
1123 if not alreadynormed:
1103 if not alreadynormed:
1124 nf = normalize(nf, False, True)
1104 nf = normalize(nf, False, True)
1125 results[nf] = st
1105 results[nf] = st
1126 elif nf in dmap and (matchalways or matchfn(nf)):
1106 elif nf in dmap and (matchalways or matchfn(nf)):
1127 results[nf] = None
1107 results[nf] = None
1128
1108
1129 for nd, d in work:
1109 for nd, d in work:
1130 # alreadynormed means that processwork doesn't have to do any
1110 # alreadynormed means that processwork doesn't have to do any
1131 # expensive directory normalization
1111 # expensive directory normalization
1132 alreadynormed = not normalize or nd == d
1112 alreadynormed = not normalize or nd == d
1133 traverse([d], alreadynormed)
1113 traverse([d], alreadynormed)
1134
1114
1135 for s in subrepos:
1115 for s in subrepos:
1136 del results[s]
1116 del results[s]
1137 del results[b'.hg']
1117 del results[b'.hg']
1138
1118
1139 # step 3: visit remaining files from dmap
1119 # step 3: visit remaining files from dmap
1140 if not skipstep3 and not exact:
1120 if not skipstep3 and not exact:
1141 # If a dmap file is not in results yet, it was either
1121 # If a dmap file is not in results yet, it was either
1142 # a) not matching matchfn b) ignored, c) missing, or d) under a
1122 # a) not matching matchfn b) ignored, c) missing, or d) under a
1143 # symlink directory.
1123 # symlink directory.
1144 if not results and matchalways:
1124 if not results and matchalways:
1145 visit = [f for f in dmap]
1125 visit = [f for f in dmap]
1146 else:
1126 else:
1147 visit = [f for f in dmap if f not in results and matchfn(f)]
1127 visit = [f for f in dmap if f not in results and matchfn(f)]
1148 visit.sort()
1128 visit.sort()
1149
1129
1150 if unknown:
1130 if unknown:
1151 # unknown == True means we walked all dirs under the roots
1131 # unknown == True means we walked all dirs under the roots
1152 # that wasn't ignored, and everything that matched was stat'ed
1132 # that wasn't ignored, and everything that matched was stat'ed
1153 # and is already in results.
1133 # and is already in results.
1154 # The rest must thus be ignored or under a symlink.
1134 # The rest must thus be ignored or under a symlink.
1155 audit_path = pathutil.pathauditor(self._root, cached=True)
1135 audit_path = pathutil.pathauditor(self._root, cached=True)
1156
1136
1157 for nf in iter(visit):
1137 for nf in iter(visit):
1158 # If a stat for the same file was already added with a
1138 # If a stat for the same file was already added with a
1159 # different case, don't add one for this, since that would
1139 # different case, don't add one for this, since that would
1160 # make it appear as if the file exists under both names
1140 # make it appear as if the file exists under both names
1161 # on disk.
1141 # on disk.
1162 if (
1142 if (
1163 normalizefile
1143 normalizefile
1164 and normalizefile(nf, True, True) in results
1144 and normalizefile(nf, True, True) in results
1165 ):
1145 ):
1166 results[nf] = None
1146 results[nf] = None
1167 # Report ignored items in the dmap as long as they are not
1147 # Report ignored items in the dmap as long as they are not
1168 # under a symlink directory.
1148 # under a symlink directory.
1169 elif audit_path.check(nf):
1149 elif audit_path.check(nf):
1170 try:
1150 try:
1171 results[nf] = lstat(join(nf))
1151 results[nf] = lstat(join(nf))
1172 # file was just ignored, no links, and exists
1152 # file was just ignored, no links, and exists
1173 except OSError:
1153 except OSError:
1174 # file doesn't exist
1154 # file doesn't exist
1175 results[nf] = None
1155 results[nf] = None
1176 else:
1156 else:
1177 # It's either missing or under a symlink directory
1157 # It's either missing or under a symlink directory
1178 # which we in this case report as missing
1158 # which we in this case report as missing
1179 results[nf] = None
1159 results[nf] = None
1180 else:
1160 else:
1181 # We may not have walked the full directory tree above,
1161 # We may not have walked the full directory tree above,
1182 # so stat and check everything we missed.
1162 # so stat and check everything we missed.
1183 iv = iter(visit)
1163 iv = iter(visit)
1184 for st in util.statfiles([join(i) for i in visit]):
1164 for st in util.statfiles([join(i) for i in visit]):
1185 results[next(iv)] = st
1165 results[next(iv)] = st
1186 return results
1166 return results
1187
1167
1188 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1168 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1189 # Force Rayon (Rust parallelism library) to respect the number of
1169 # Force Rayon (Rust parallelism library) to respect the number of
1190 # workers. This is a temporary workaround until Rust code knows
1170 # workers. This is a temporary workaround until Rust code knows
1191 # how to read the config file.
1171 # how to read the config file.
1192 numcpus = self._ui.configint(b"worker", b"numcpus")
1172 numcpus = self._ui.configint(b"worker", b"numcpus")
1193 if numcpus is not None:
1173 if numcpus is not None:
1194 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1174 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1195
1175
1196 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1176 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1197 if not workers_enabled:
1177 if not workers_enabled:
1198 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1178 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1199
1179
1200 (
1180 (
1201 lookup,
1181 lookup,
1202 modified,
1182 modified,
1203 added,
1183 added,
1204 removed,
1184 removed,
1205 deleted,
1185 deleted,
1206 clean,
1186 clean,
1207 ignored,
1187 ignored,
1208 unknown,
1188 unknown,
1209 warnings,
1189 warnings,
1210 bad,
1190 bad,
1211 traversed,
1191 traversed,
1212 dirty,
1192 dirty,
1213 ) = rustmod.status(
1193 ) = rustmod.status(
1214 self._map._map,
1194 self._map._map,
1215 matcher,
1195 matcher,
1216 self._rootdir,
1196 self._rootdir,
1217 self._ignorefiles(),
1197 self._ignorefiles(),
1218 self._checkexec,
1198 self._checkexec,
1219 self._lastnormaltime,
1220 bool(list_clean),
1199 bool(list_clean),
1221 bool(list_ignored),
1200 bool(list_ignored),
1222 bool(list_unknown),
1201 bool(list_unknown),
1223 bool(matcher.traversedir),
1202 bool(matcher.traversedir),
1224 )
1203 )
1225
1204
1226 self._dirty |= dirty
1205 self._dirty |= dirty
1227
1206
1228 if matcher.traversedir:
1207 if matcher.traversedir:
1229 for dir in traversed:
1208 for dir in traversed:
1230 matcher.traversedir(dir)
1209 matcher.traversedir(dir)
1231
1210
1232 if self._ui.warn:
1211 if self._ui.warn:
1233 for item in warnings:
1212 for item in warnings:
1234 if isinstance(item, tuple):
1213 if isinstance(item, tuple):
1235 file_path, syntax = item
1214 file_path, syntax = item
1236 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1215 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1237 file_path,
1216 file_path,
1238 syntax,
1217 syntax,
1239 )
1218 )
1240 self._ui.warn(msg)
1219 self._ui.warn(msg)
1241 else:
1220 else:
1242 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1221 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1243 self._ui.warn(
1222 self._ui.warn(
1244 msg
1223 msg
1245 % (
1224 % (
1246 pathutil.canonpath(
1225 pathutil.canonpath(
1247 self._rootdir, self._rootdir, item
1226 self._rootdir, self._rootdir, item
1248 ),
1227 ),
1249 b"No such file or directory",
1228 b"No such file or directory",
1250 )
1229 )
1251 )
1230 )
1252
1231
1253 for (fn, message) in bad:
1232 for (fn, message) in bad:
1254 matcher.bad(fn, encoding.strtolocal(message))
1233 matcher.bad(fn, encoding.strtolocal(message))
1255
1234
1256 status = scmutil.status(
1235 status = scmutil.status(
1257 modified=modified,
1236 modified=modified,
1258 added=added,
1237 added=added,
1259 removed=removed,
1238 removed=removed,
1260 deleted=deleted,
1239 deleted=deleted,
1261 unknown=unknown,
1240 unknown=unknown,
1262 ignored=ignored,
1241 ignored=ignored,
1263 clean=clean,
1242 clean=clean,
1264 )
1243 )
1265 return (lookup, status)
1244 return (lookup, status)
1266
1245
1267 def status(self, match, subrepos, ignored, clean, unknown):
1246 def status(self, match, subrepos, ignored, clean, unknown):
1268 """Determine the status of the working copy relative to the
1247 """Determine the status of the working copy relative to the
1269 dirstate and return a pair of (unsure, status), where status is of type
1248 dirstate and return a pair of (unsure, status), where status is of type
1270 scmutil.status and:
1249 scmutil.status and:
1271
1250
1272 unsure:
1251 unsure:
1273 files that might have been modified since the dirstate was
1252 files that might have been modified since the dirstate was
1274 written, but need to be read to be sure (size is the same
1253 written, but need to be read to be sure (size is the same
1275 but mtime differs)
1254 but mtime differs)
1276 status.modified:
1255 status.modified:
1277 files that have definitely been modified since the dirstate
1256 files that have definitely been modified since the dirstate
1278 was written (different size or mode)
1257 was written (different size or mode)
1279 status.clean:
1258 status.clean:
1280 files that have definitely not been modified since the
1259 files that have definitely not been modified since the
1281 dirstate was written
1260 dirstate was written
1282 """
1261 """
1283 listignored, listclean, listunknown = ignored, clean, unknown
1262 listignored, listclean, listunknown = ignored, clean, unknown
1284 lookup, modified, added, unknown, ignored = [], [], [], [], []
1263 lookup, modified, added, unknown, ignored = [], [], [], [], []
1285 removed, deleted, clean = [], [], []
1264 removed, deleted, clean = [], [], []
1286
1265
1287 dmap = self._map
1266 dmap = self._map
1288 dmap.preload()
1267 dmap.preload()
1289
1268
1290 use_rust = True
1269 use_rust = True
1291
1270
1292 allowed_matchers = (
1271 allowed_matchers = (
1293 matchmod.alwaysmatcher,
1272 matchmod.alwaysmatcher,
1294 matchmod.exactmatcher,
1273 matchmod.exactmatcher,
1295 matchmod.includematcher,
1274 matchmod.includematcher,
1296 )
1275 )
1297
1276
1298 if rustmod is None:
1277 if rustmod is None:
1299 use_rust = False
1278 use_rust = False
1300 elif self._checkcase:
1279 elif self._checkcase:
1301 # Case-insensitive filesystems are not handled yet
1280 # Case-insensitive filesystems are not handled yet
1302 use_rust = False
1281 use_rust = False
1303 elif subrepos:
1282 elif subrepos:
1304 use_rust = False
1283 use_rust = False
1305 elif sparse.enabled:
1284 elif sparse.enabled:
1306 use_rust = False
1285 use_rust = False
1307 elif not isinstance(match, allowed_matchers):
1286 elif not isinstance(match, allowed_matchers):
1308 # Some matchers have yet to be implemented
1287 # Some matchers have yet to be implemented
1309 use_rust = False
1288 use_rust = False
1310
1289
1311 # Get the time from the filesystem so we can disambiguate files that
1290 # Get the time from the filesystem so we can disambiguate files that
1312 # appear modified in the present or future.
1291 # appear modified in the present or future.
1313 try:
1292 try:
1314 mtime_boundary = timestamp.get_fs_now(self._opener)
1293 mtime_boundary = timestamp.get_fs_now(self._opener)
1315 except OSError:
1294 except OSError:
1316 # In largefiles or readonly context
1295 # In largefiles or readonly context
1317 mtime_boundary = None
1296 mtime_boundary = None
1318
1297
1319 if use_rust:
1298 if use_rust:
1320 try:
1299 try:
1321 res = self._rust_status(
1300 res = self._rust_status(
1322 match, listclean, listignored, listunknown
1301 match, listclean, listignored, listunknown
1323 )
1302 )
1324 return res + (mtime_boundary,)
1303 return res + (mtime_boundary,)
1325 except rustmod.FallbackError:
1304 except rustmod.FallbackError:
1326 pass
1305 pass
1327
1306
1328 def noop(f):
1307 def noop(f):
1329 pass
1308 pass
1330
1309
1331 dcontains = dmap.__contains__
1310 dcontains = dmap.__contains__
1332 dget = dmap.__getitem__
1311 dget = dmap.__getitem__
1333 ladd = lookup.append # aka "unsure"
1312 ladd = lookup.append # aka "unsure"
1334 madd = modified.append
1313 madd = modified.append
1335 aadd = added.append
1314 aadd = added.append
1336 uadd = unknown.append if listunknown else noop
1315 uadd = unknown.append if listunknown else noop
1337 iadd = ignored.append if listignored else noop
1316 iadd = ignored.append if listignored else noop
1338 radd = removed.append
1317 radd = removed.append
1339 dadd = deleted.append
1318 dadd = deleted.append
1340 cadd = clean.append if listclean else noop
1319 cadd = clean.append if listclean else noop
1341 mexact = match.exact
1320 mexact = match.exact
1342 dirignore = self._dirignore
1321 dirignore = self._dirignore
1343 checkexec = self._checkexec
1322 checkexec = self._checkexec
1344 checklink = self._checklink
1323 checklink = self._checklink
1345 copymap = self._map.copymap
1324 copymap = self._map.copymap
1346 lastnormaltime = self._lastnormaltime
1347
1325
1348 # We need to do full walks when either
1326 # We need to do full walks when either
1349 # - we're listing all clean files, or
1327 # - we're listing all clean files, or
1350 # - match.traversedir does something, because match.traversedir should
1328 # - match.traversedir does something, because match.traversedir should
1351 # be called for every dir in the working dir
1329 # be called for every dir in the working dir
1352 full = listclean or match.traversedir is not None
1330 full = listclean or match.traversedir is not None
1353 for fn, st in pycompat.iteritems(
1331 for fn, st in pycompat.iteritems(
1354 self.walk(match, subrepos, listunknown, listignored, full=full)
1332 self.walk(match, subrepos, listunknown, listignored, full=full)
1355 ):
1333 ):
1356 if not dcontains(fn):
1334 if not dcontains(fn):
1357 if (listignored or mexact(fn)) and dirignore(fn):
1335 if (listignored or mexact(fn)) and dirignore(fn):
1358 if listignored:
1336 if listignored:
1359 iadd(fn)
1337 iadd(fn)
1360 else:
1338 else:
1361 uadd(fn)
1339 uadd(fn)
1362 continue
1340 continue
1363
1341
1364 t = dget(fn)
1342 t = dget(fn)
1365 mode = t.mode
1343 mode = t.mode
1366 size = t.size
1344 size = t.size
1367
1345
1368 if not st and t.tracked:
1346 if not st and t.tracked:
1369 dadd(fn)
1347 dadd(fn)
1370 elif t.p2_info:
1348 elif t.p2_info:
1371 madd(fn)
1349 madd(fn)
1372 elif t.added:
1350 elif t.added:
1373 aadd(fn)
1351 aadd(fn)
1374 elif t.removed:
1352 elif t.removed:
1375 radd(fn)
1353 radd(fn)
1376 elif t.tracked:
1354 elif t.tracked:
1377 if not checklink and t.has_fallback_symlink:
1355 if not checklink and t.has_fallback_symlink:
1378 # If the file system does not support symlink, the mode
1356 # If the file system does not support symlink, the mode
1379 # might not be correctly stored in the dirstate, so do not
1357 # might not be correctly stored in the dirstate, so do not
1380 # trust it.
1358 # trust it.
1381 ladd(fn)
1359 ladd(fn)
1382 elif not checkexec and t.has_fallback_exec:
1360 elif not checkexec and t.has_fallback_exec:
1383 # If the file system does not support exec bits, the mode
1361 # If the file system does not support exec bits, the mode
1384 # might not be correctly stored in the dirstate, so do not
1362 # might not be correctly stored in the dirstate, so do not
1385 # trust it.
1363 # trust it.
1386 ladd(fn)
1364 ladd(fn)
1387 elif (
1365 elif (
1388 size >= 0
1366 size >= 0
1389 and (
1367 and (
1390 (size != st.st_size and size != st.st_size & _rangemask)
1368 (size != st.st_size and size != st.st_size & _rangemask)
1391 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1369 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1392 )
1370 )
1393 or fn in copymap
1371 or fn in copymap
1394 ):
1372 ):
1395 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1373 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1396 # issue6456: Size returned may be longer due to
1374 # issue6456: Size returned may be longer due to
1397 # encryption on EXT-4 fscrypt, undecided.
1375 # encryption on EXT-4 fscrypt, undecided.
1398 ladd(fn)
1376 ladd(fn)
1399 else:
1377 else:
1400 madd(fn)
1378 madd(fn)
1401 elif not t.mtime_likely_equal_to(timestamp.mtime_of(st)):
1379 elif not t.mtime_likely_equal_to(timestamp.mtime_of(st)):
1402 ladd(fn)
1380 # There might be a change in the future if for example the
1403 elif timestamp.mtime_of(st) == lastnormaltime:
1381 # internal clock is off, but this is a case where the issues
1404 # fn may have just been marked as normal and it may have
1382 # the user would face would be a lot worse and there is
1405 # changed in the same second without changing its size.
1383 # nothing we can really do.
1406 # This can happen if we quickly do multiple commits.
1407 # Force lookup, so we don't miss such a racy file change.
1408 ladd(fn)
1384 ladd(fn)
1409 elif listclean:
1385 elif listclean:
1410 cadd(fn)
1386 cadd(fn)
1411 status = scmutil.status(
1387 status = scmutil.status(
1412 modified, added, removed, deleted, unknown, ignored, clean
1388 modified, added, removed, deleted, unknown, ignored, clean
1413 )
1389 )
1414 return (lookup, status, mtime_boundary)
1390 return (lookup, status, mtime_boundary)
1415
1391
1416 def matches(self, match):
1392 def matches(self, match):
1417 """
1393 """
1418 return files in the dirstate (in whatever state) filtered by match
1394 return files in the dirstate (in whatever state) filtered by match
1419 """
1395 """
1420 dmap = self._map
1396 dmap = self._map
1421 if rustmod is not None:
1397 if rustmod is not None:
1422 dmap = self._map._map
1398 dmap = self._map._map
1423
1399
1424 if match.always():
1400 if match.always():
1425 return dmap.keys()
1401 return dmap.keys()
1426 files = match.files()
1402 files = match.files()
1427 if match.isexact():
1403 if match.isexact():
1428 # fast path -- filter the other way around, since typically files is
1404 # fast path -- filter the other way around, since typically files is
1429 # much smaller than dmap
1405 # much smaller than dmap
1430 return [f for f in files if f in dmap]
1406 return [f for f in files if f in dmap]
1431 if match.prefix() and all(fn in dmap for fn in files):
1407 if match.prefix() and all(fn in dmap for fn in files):
1432 # fast path -- all the values are known to be files, so just return
1408 # fast path -- all the values are known to be files, so just return
1433 # that
1409 # that
1434 return list(files)
1410 return list(files)
1435 return [f for f in dmap if match(f)]
1411 return [f for f in dmap if match(f)]
1436
1412
1437 def _actualfilename(self, tr):
1413 def _actualfilename(self, tr):
1438 if tr:
1414 if tr:
1439 return self._pendingfilename
1415 return self._pendingfilename
1440 else:
1416 else:
1441 return self._filename
1417 return self._filename
1442
1418
1443 def savebackup(self, tr, backupname):
1419 def savebackup(self, tr, backupname):
1444 '''Save current dirstate into backup file'''
1420 '''Save current dirstate into backup file'''
1445 filename = self._actualfilename(tr)
1421 filename = self._actualfilename(tr)
1446 assert backupname != filename
1422 assert backupname != filename
1447
1423
1448 # use '_writedirstate' instead of 'write' to write changes certainly,
1424 # use '_writedirstate' instead of 'write' to write changes certainly,
1449 # because the latter omits writing out if transaction is running.
1425 # because the latter omits writing out if transaction is running.
1450 # output file will be used to create backup of dirstate at this point.
1426 # output file will be used to create backup of dirstate at this point.
1451 if self._dirty or not self._opener.exists(filename):
1427 if self._dirty or not self._opener.exists(filename):
1452 self._writedirstate(
1428 self._writedirstate(
1453 tr,
1429 tr,
1454 self._opener(filename, b"w", atomictemp=True, checkambig=True),
1430 self._opener(filename, b"w", atomictemp=True, checkambig=True),
1455 )
1431 )
1456
1432
1457 if tr:
1433 if tr:
1458 # ensure that subsequent tr.writepending returns True for
1434 # ensure that subsequent tr.writepending returns True for
1459 # changes written out above, even if dirstate is never
1435 # changes written out above, even if dirstate is never
1460 # changed after this
1436 # changed after this
1461 tr.addfilegenerator(
1437 tr.addfilegenerator(
1462 b'dirstate',
1438 b'dirstate',
1463 (self._filename,),
1439 (self._filename,),
1464 lambda f: self._writedirstate(tr, f),
1440 lambda f: self._writedirstate(tr, f),
1465 location=b'plain',
1441 location=b'plain',
1466 )
1442 )
1467
1443
1468 # ensure that pending file written above is unlinked at
1444 # ensure that pending file written above is unlinked at
1469 # failure, even if tr.writepending isn't invoked until the
1445 # failure, even if tr.writepending isn't invoked until the
1470 # end of this transaction
1446 # end of this transaction
1471 tr.registertmp(filename, location=b'plain')
1447 tr.registertmp(filename, location=b'plain')
1472
1448
1473 self._opener.tryunlink(backupname)
1449 self._opener.tryunlink(backupname)
1474 # hardlink backup is okay because _writedirstate is always called
1450 # hardlink backup is okay because _writedirstate is always called
1475 # with an "atomictemp=True" file.
1451 # with an "atomictemp=True" file.
1476 util.copyfile(
1452 util.copyfile(
1477 self._opener.join(filename),
1453 self._opener.join(filename),
1478 self._opener.join(backupname),
1454 self._opener.join(backupname),
1479 hardlink=True,
1455 hardlink=True,
1480 )
1456 )
1481
1457
1482 def restorebackup(self, tr, backupname):
1458 def restorebackup(self, tr, backupname):
1483 '''Restore dirstate by backup file'''
1459 '''Restore dirstate by backup file'''
1484 # this "invalidate()" prevents "wlock.release()" from writing
1460 # this "invalidate()" prevents "wlock.release()" from writing
1485 # changes of dirstate out after restoring from backup file
1461 # changes of dirstate out after restoring from backup file
1486 self.invalidate()
1462 self.invalidate()
1487 filename = self._actualfilename(tr)
1463 filename = self._actualfilename(tr)
1488 o = self._opener
1464 o = self._opener
1489 if util.samefile(o.join(backupname), o.join(filename)):
1465 if util.samefile(o.join(backupname), o.join(filename)):
1490 o.unlink(backupname)
1466 o.unlink(backupname)
1491 else:
1467 else:
1492 o.rename(backupname, filename, checkambig=True)
1468 o.rename(backupname, filename, checkambig=True)
1493
1469
1494 def clearbackup(self, tr, backupname):
1470 def clearbackup(self, tr, backupname):
1495 '''Clear backup file'''
1471 '''Clear backup file'''
1496 self._opener.unlink(backupname)
1472 self._opener.unlink(backupname)
1497
1473
1498 def verify(self, m1, m2):
1474 def verify(self, m1, m2):
1499 """check the dirstate content again the parent manifest and yield errors"""
1475 """check the dirstate content again the parent manifest and yield errors"""
1500 missing_from_p1 = b"%s in state %s, but not in manifest1\n"
1476 missing_from_p1 = b"%s in state %s, but not in manifest1\n"
1501 unexpected_in_p1 = b"%s in state %s, but also in manifest1\n"
1477 unexpected_in_p1 = b"%s in state %s, but also in manifest1\n"
1502 missing_from_ps = b"%s in state %s, but not in either manifest\n"
1478 missing_from_ps = b"%s in state %s, but not in either manifest\n"
1503 missing_from_ds = b"%s in manifest1, but listed as state %s\n"
1479 missing_from_ds = b"%s in manifest1, but listed as state %s\n"
1504 for f, entry in self.items():
1480 for f, entry in self.items():
1505 state = entry.state
1481 state = entry.state
1506 if state in b"nr" and f not in m1:
1482 if state in b"nr" and f not in m1:
1507 yield (missing_from_p1, f, state)
1483 yield (missing_from_p1, f, state)
1508 if state in b"a" and f in m1:
1484 if state in b"a" and f in m1:
1509 yield (unexpected_in_p1, f, state)
1485 yield (unexpected_in_p1, f, state)
1510 if state in b"m" and f not in m1 and f not in m2:
1486 if state in b"m" and f not in m1 and f not in m2:
1511 yield (missing_from_ps, f, state)
1487 yield (missing_from_ps, f, state)
1512 for f in m1:
1488 for f in m1:
1513 state = self.get_entry(f).state
1489 state = self.get_entry(f).state
1514 if state not in b"nrm":
1490 if state not in b"nrm":
1515 yield (missing_from_ds, f, state)
1491 yield (missing_from_ds, f, state)
@@ -1,147 +1,142 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 //! Rust implementation of dirstate.status (dirstate.py).
8 //! Rust implementation of dirstate.status (dirstate.py).
9 //! It is currently missing a lot of functionality compared to the Python one
9 //! It is currently missing a lot of functionality compared to the Python one
10 //! and will only be triggered in narrow cases.
10 //! and will only be triggered in narrow cases.
11
11
12 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
12 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
13
13
14 use crate::{
14 use crate::{
15 dirstate::TruncatedTimestamp,
16 utils::hg_path::{HgPath, HgPathError},
15 utils::hg_path::{HgPath, HgPathError},
17 PatternError,
16 PatternError,
18 };
17 };
19
18
20 use std::{borrow::Cow, fmt};
19 use std::{borrow::Cow, fmt};
21
20
22 /// Wrong type of file from a `BadMatch`
21 /// Wrong type of file from a `BadMatch`
23 /// Note: a lot of those don't exist on all platforms.
22 /// Note: a lot of those don't exist on all platforms.
24 #[derive(Debug, Copy, Clone)]
23 #[derive(Debug, Copy, Clone)]
25 pub enum BadType {
24 pub enum BadType {
26 CharacterDevice,
25 CharacterDevice,
27 BlockDevice,
26 BlockDevice,
28 FIFO,
27 FIFO,
29 Socket,
28 Socket,
30 Directory,
29 Directory,
31 Unknown,
30 Unknown,
32 }
31 }
33
32
34 impl fmt::Display for BadType {
33 impl fmt::Display for BadType {
35 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
34 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
36 f.write_str(match self {
35 f.write_str(match self {
37 BadType::CharacterDevice => "character device",
36 BadType::CharacterDevice => "character device",
38 BadType::BlockDevice => "block device",
37 BadType::BlockDevice => "block device",
39 BadType::FIFO => "fifo",
38 BadType::FIFO => "fifo",
40 BadType::Socket => "socket",
39 BadType::Socket => "socket",
41 BadType::Directory => "directory",
40 BadType::Directory => "directory",
42 BadType::Unknown => "unknown",
41 BadType::Unknown => "unknown",
43 })
42 })
44 }
43 }
45 }
44 }
46
45
47 /// Was explicitly matched but cannot be found/accessed
46 /// Was explicitly matched but cannot be found/accessed
48 #[derive(Debug, Copy, Clone)]
47 #[derive(Debug, Copy, Clone)]
49 pub enum BadMatch {
48 pub enum BadMatch {
50 OsError(i32),
49 OsError(i32),
51 BadType(BadType),
50 BadType(BadType),
52 }
51 }
53
52
54 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait + 'static>`, so add
53 /// `Box<dyn Trait>` is syntactic sugar for `Box<dyn Trait + 'static>`, so add
55 /// an explicit lifetime here to not fight `'static` bounds "out of nowhere".
54 /// an explicit lifetime here to not fight `'static` bounds "out of nowhere".
56 pub type IgnoreFnType<'a> =
55 pub type IgnoreFnType<'a> =
57 Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>;
56 Box<dyn for<'r> Fn(&'r HgPath) -> bool + Sync + 'a>;
58
57
59 /// We have a good mix of owned (from directory traversal) and borrowed (from
58 /// We have a good mix of owned (from directory traversal) and borrowed (from
60 /// the dirstate/explicit) paths, this comes up a lot.
59 /// the dirstate/explicit) paths, this comes up a lot.
61 pub type HgPathCow<'a> = Cow<'a, HgPath>;
60 pub type HgPathCow<'a> = Cow<'a, HgPath>;
62
61
63 #[derive(Debug, Copy, Clone)]
62 #[derive(Debug, Copy, Clone)]
64 pub struct StatusOptions {
63 pub struct StatusOptions {
65 /// Remember the most recent modification timeslot for status, to make
66 /// sure we won't miss future size-preserving file content modifications
67 /// that happen within the same timeslot.
68 pub last_normal_time: TruncatedTimestamp,
69 /// Whether we are on a filesystem with UNIX-like exec flags
64 /// Whether we are on a filesystem with UNIX-like exec flags
70 pub check_exec: bool,
65 pub check_exec: bool,
71 pub list_clean: bool,
66 pub list_clean: bool,
72 pub list_unknown: bool,
67 pub list_unknown: bool,
73 pub list_ignored: bool,
68 pub list_ignored: bool,
74 /// Whether to collect traversed dirs for applying a callback later.
69 /// Whether to collect traversed dirs for applying a callback later.
75 /// Used by `hg purge` for example.
70 /// Used by `hg purge` for example.
76 pub collect_traversed_dirs: bool,
71 pub collect_traversed_dirs: bool,
77 }
72 }
78
73
79 #[derive(Debug, Default)]
74 #[derive(Debug, Default)]
80 pub struct DirstateStatus<'a> {
75 pub struct DirstateStatus<'a> {
81 /// Tracked files whose contents have changed since the parent revision
76 /// Tracked files whose contents have changed since the parent revision
82 pub modified: Vec<HgPathCow<'a>>,
77 pub modified: Vec<HgPathCow<'a>>,
83
78
84 /// Newly-tracked files that were not present in the parent
79 /// Newly-tracked files that were not present in the parent
85 pub added: Vec<HgPathCow<'a>>,
80 pub added: Vec<HgPathCow<'a>>,
86
81
87 /// Previously-tracked files that have been (re)moved with an hg command
82 /// Previously-tracked files that have been (re)moved with an hg command
88 pub removed: Vec<HgPathCow<'a>>,
83 pub removed: Vec<HgPathCow<'a>>,
89
84
90 /// (Still) tracked files that are missing, (re)moved with an non-hg
85 /// (Still) tracked files that are missing, (re)moved with an non-hg
91 /// command
86 /// command
92 pub deleted: Vec<HgPathCow<'a>>,
87 pub deleted: Vec<HgPathCow<'a>>,
93
88
94 /// Tracked files that are up to date with the parent.
89 /// Tracked files that are up to date with the parent.
95 /// Only pupulated if `StatusOptions::list_clean` is true.
90 /// Only pupulated if `StatusOptions::list_clean` is true.
96 pub clean: Vec<HgPathCow<'a>>,
91 pub clean: Vec<HgPathCow<'a>>,
97
92
98 /// Files in the working directory that are ignored with `.hgignore`.
93 /// Files in the working directory that are ignored with `.hgignore`.
99 /// Only pupulated if `StatusOptions::list_ignored` is true.
94 /// Only pupulated if `StatusOptions::list_ignored` is true.
100 pub ignored: Vec<HgPathCow<'a>>,
95 pub ignored: Vec<HgPathCow<'a>>,
101
96
102 /// Files in the working directory that are neither tracked nor ignored.
97 /// Files in the working directory that are neither tracked nor ignored.
103 /// Only pupulated if `StatusOptions::list_unknown` is true.
98 /// Only pupulated if `StatusOptions::list_unknown` is true.
104 pub unknown: Vec<HgPathCow<'a>>,
99 pub unknown: Vec<HgPathCow<'a>>,
105
100
106 /// Was explicitly matched but cannot be found/accessed
101 /// Was explicitly matched but cannot be found/accessed
107 pub bad: Vec<(HgPathCow<'a>, BadMatch)>,
102 pub bad: Vec<(HgPathCow<'a>, BadMatch)>,
108
103
109 /// Either clean or modified, but we can’t tell from filesystem metadata
104 /// Either clean or modified, but we can’t tell from filesystem metadata
110 /// alone. The file contents need to be read and compared with that in
105 /// alone. The file contents need to be read and compared with that in
111 /// the parent.
106 /// the parent.
112 pub unsure: Vec<HgPathCow<'a>>,
107 pub unsure: Vec<HgPathCow<'a>>,
113
108
114 /// Only filled if `collect_traversed_dirs` is `true`
109 /// Only filled if `collect_traversed_dirs` is `true`
115 pub traversed: Vec<HgPathCow<'a>>,
110 pub traversed: Vec<HgPathCow<'a>>,
116
111
117 /// Whether `status()` made changed to the `DirstateMap` that should be
112 /// Whether `status()` made changed to the `DirstateMap` that should be
118 /// written back to disk
113 /// written back to disk
119 pub dirty: bool,
114 pub dirty: bool,
120 }
115 }
121
116
122 #[derive(Debug, derive_more::From)]
117 #[derive(Debug, derive_more::From)]
123 pub enum StatusError {
118 pub enum StatusError {
124 /// Generic IO error
119 /// Generic IO error
125 IO(std::io::Error),
120 IO(std::io::Error),
126 /// An invalid path that cannot be represented in Mercurial was found
121 /// An invalid path that cannot be represented in Mercurial was found
127 Path(HgPathError),
122 Path(HgPathError),
128 /// An invalid "ignore" pattern was found
123 /// An invalid "ignore" pattern was found
129 Pattern(PatternError),
124 Pattern(PatternError),
130 /// Corrupted dirstate
125 /// Corrupted dirstate
131 DirstateV2ParseError(DirstateV2ParseError),
126 DirstateV2ParseError(DirstateV2ParseError),
132 }
127 }
133
128
134 pub type StatusResult<T> = Result<T, StatusError>;
129 pub type StatusResult<T> = Result<T, StatusError>;
135
130
136 impl fmt::Display for StatusError {
131 impl fmt::Display for StatusError {
137 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
132 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
138 match self {
133 match self {
139 StatusError::IO(error) => error.fmt(f),
134 StatusError::IO(error) => error.fmt(f),
140 StatusError::Path(error) => error.fmt(f),
135 StatusError::Path(error) => error.fmt(f),
141 StatusError::Pattern(error) => error.fmt(f),
136 StatusError::Pattern(error) => error.fmt(f),
142 StatusError::DirstateV2ParseError(_) => {
137 StatusError::DirstateV2ParseError(_) => {
143 f.write_str("dirstate-v2 parse error")
138 f.write_str("dirstate-v2 parse error")
144 }
139 }
145 }
140 }
146 }
141 }
147 }
142 }
@@ -1,756 +1,760 b''
1 use crate::dirstate::entry::TruncatedTimestamp;
1 use crate::dirstate::entry::TruncatedTimestamp;
2 use crate::dirstate::status::IgnoreFnType;
2 use crate::dirstate::status::IgnoreFnType;
3 use crate::dirstate_tree::dirstate_map::BorrowedPath;
3 use crate::dirstate_tree::dirstate_map::BorrowedPath;
4 use crate::dirstate_tree::dirstate_map::ChildNodesRef;
4 use crate::dirstate_tree::dirstate_map::ChildNodesRef;
5 use crate::dirstate_tree::dirstate_map::DirstateMap;
5 use crate::dirstate_tree::dirstate_map::DirstateMap;
6 use crate::dirstate_tree::dirstate_map::NodeData;
6 use crate::dirstate_tree::dirstate_map::NodeData;
7 use crate::dirstate_tree::dirstate_map::NodeRef;
7 use crate::dirstate_tree::dirstate_map::NodeRef;
8 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
8 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
9 use crate::matchers::get_ignore_function;
9 use crate::matchers::get_ignore_function;
10 use crate::matchers::Matcher;
10 use crate::matchers::Matcher;
11 use crate::utils::files::get_bytes_from_os_string;
11 use crate::utils::files::get_bytes_from_os_string;
12 use crate::utils::files::get_path_from_bytes;
12 use crate::utils::files::get_path_from_bytes;
13 use crate::utils::hg_path::HgPath;
13 use crate::utils::hg_path::HgPath;
14 use crate::BadMatch;
14 use crate::BadMatch;
15 use crate::DirstateStatus;
15 use crate::DirstateStatus;
16 use crate::EntryState;
16 use crate::EntryState;
17 use crate::HgPathBuf;
17 use crate::HgPathBuf;
18 use crate::PatternFileWarning;
18 use crate::PatternFileWarning;
19 use crate::StatusError;
19 use crate::StatusError;
20 use crate::StatusOptions;
20 use crate::StatusOptions;
21 use micro_timer::timed;
21 use micro_timer::timed;
22 use rayon::prelude::*;
22 use rayon::prelude::*;
23 use sha1::{Digest, Sha1};
23 use sha1::{Digest, Sha1};
24 use std::borrow::Cow;
24 use std::borrow::Cow;
25 use std::io;
25 use std::io;
26 use std::path::Path;
26 use std::path::Path;
27 use std::path::PathBuf;
27 use std::path::PathBuf;
28 use std::sync::Mutex;
28 use std::sync::Mutex;
29 use std::time::SystemTime;
29 use std::time::SystemTime;
30
30
31 /// Returns the status of the working directory compared to its parent
31 /// Returns the status of the working directory compared to its parent
32 /// changeset.
32 /// changeset.
33 ///
33 ///
34 /// This algorithm is based on traversing the filesystem tree (`fs` in function
34 /// This algorithm is based on traversing the filesystem tree (`fs` in function
35 /// and variable names) and dirstate tree at the same time. The core of this
35 /// and variable names) and dirstate tree at the same time. The core of this
36 /// traversal is the recursive `traverse_fs_directory_and_dirstate` function
36 /// traversal is the recursive `traverse_fs_directory_and_dirstate` function
37 /// and its use of `itertools::merge_join_by`. When reaching a path that only
37 /// and its use of `itertools::merge_join_by`. When reaching a path that only
38 /// exists in one of the two trees, depending on information requested by
38 /// exists in one of the two trees, depending on information requested by
39 /// `options` we may need to traverse the remaining subtree.
39 /// `options` we may need to traverse the remaining subtree.
40 #[timed]
40 #[timed]
41 pub fn status<'tree, 'on_disk: 'tree>(
41 pub fn status<'tree, 'on_disk: 'tree>(
42 dmap: &'tree mut DirstateMap<'on_disk>,
42 dmap: &'tree mut DirstateMap<'on_disk>,
43 matcher: &(dyn Matcher + Sync),
43 matcher: &(dyn Matcher + Sync),
44 root_dir: PathBuf,
44 root_dir: PathBuf,
45 ignore_files: Vec<PathBuf>,
45 ignore_files: Vec<PathBuf>,
46 options: StatusOptions,
46 options: StatusOptions,
47 ) -> Result<(DirstateStatus<'on_disk>, Vec<PatternFileWarning>), StatusError> {
47 ) -> Result<(DirstateStatus<'on_disk>, Vec<PatternFileWarning>), StatusError> {
48 let (ignore_fn, warnings, patterns_changed): (IgnoreFnType, _, _) =
48 let (ignore_fn, warnings, patterns_changed): (IgnoreFnType, _, _) =
49 if options.list_ignored || options.list_unknown {
49 if options.list_ignored || options.list_unknown {
50 let mut hasher = Sha1::new();
50 let mut hasher = Sha1::new();
51 let (ignore_fn, warnings) = get_ignore_function(
51 let (ignore_fn, warnings) = get_ignore_function(
52 ignore_files,
52 ignore_files,
53 &root_dir,
53 &root_dir,
54 &mut |pattern_bytes| hasher.update(pattern_bytes),
54 &mut |pattern_bytes| hasher.update(pattern_bytes),
55 )?;
55 )?;
56 let new_hash = *hasher.finalize().as_ref();
56 let new_hash = *hasher.finalize().as_ref();
57 let changed = new_hash != dmap.ignore_patterns_hash;
57 let changed = new_hash != dmap.ignore_patterns_hash;
58 dmap.ignore_patterns_hash = new_hash;
58 dmap.ignore_patterns_hash = new_hash;
59 (ignore_fn, warnings, Some(changed))
59 (ignore_fn, warnings, Some(changed))
60 } else {
60 } else {
61 (Box::new(|&_| true), vec![], None)
61 (Box::new(|&_| true), vec![], None)
62 };
62 };
63
63
64 let common = StatusCommon {
64 let common = StatusCommon {
65 dmap,
65 dmap,
66 options,
66 options,
67 matcher,
67 matcher,
68 ignore_fn,
68 ignore_fn,
69 outcome: Default::default(),
69 outcome: Default::default(),
70 ignore_patterns_have_changed: patterns_changed,
70 ignore_patterns_have_changed: patterns_changed,
71 new_cachable_directories: Default::default(),
71 new_cachable_directories: Default::default(),
72 outated_cached_directories: Default::default(),
72 outated_cached_directories: Default::default(),
73 filesystem_time_at_status_start: filesystem_now(&root_dir).ok(),
73 filesystem_time_at_status_start: filesystem_now(&root_dir).ok(),
74 };
74 };
75 let is_at_repo_root = true;
75 let is_at_repo_root = true;
76 let hg_path = &BorrowedPath::OnDisk(HgPath::new(""));
76 let hg_path = &BorrowedPath::OnDisk(HgPath::new(""));
77 let has_ignored_ancestor = false;
77 let has_ignored_ancestor = false;
78 let root_cached_mtime = None;
78 let root_cached_mtime = None;
79 let root_dir_metadata = None;
79 let root_dir_metadata = None;
80 // If the path we have for the repository root is a symlink, do follow it.
80 // If the path we have for the repository root is a symlink, do follow it.
81 // (As opposed to symlinks within the working directory which are not
81 // (As opposed to symlinks within the working directory which are not
82 // followed, using `std::fs::symlink_metadata`.)
82 // followed, using `std::fs::symlink_metadata`.)
83 common.traverse_fs_directory_and_dirstate(
83 common.traverse_fs_directory_and_dirstate(
84 has_ignored_ancestor,
84 has_ignored_ancestor,
85 dmap.root.as_ref(),
85 dmap.root.as_ref(),
86 hg_path,
86 hg_path,
87 &root_dir,
87 &root_dir,
88 root_dir_metadata,
88 root_dir_metadata,
89 root_cached_mtime,
89 root_cached_mtime,
90 is_at_repo_root,
90 is_at_repo_root,
91 )?;
91 )?;
92 let mut outcome = common.outcome.into_inner().unwrap();
92 let mut outcome = common.outcome.into_inner().unwrap();
93 let new_cachable = common.new_cachable_directories.into_inner().unwrap();
93 let new_cachable = common.new_cachable_directories.into_inner().unwrap();
94 let outdated = common.outated_cached_directories.into_inner().unwrap();
94 let outdated = common.outated_cached_directories.into_inner().unwrap();
95
95
96 outcome.dirty = common.ignore_patterns_have_changed == Some(true)
96 outcome.dirty = common.ignore_patterns_have_changed == Some(true)
97 || !outdated.is_empty()
97 || !outdated.is_empty()
98 || !new_cachable.is_empty();
98 || !new_cachable.is_empty();
99
99
100 // Remove outdated mtimes before adding new mtimes, in case a given
100 // Remove outdated mtimes before adding new mtimes, in case a given
101 // directory is both
101 // directory is both
102 for path in &outdated {
102 for path in &outdated {
103 let node = dmap.get_or_insert(path)?;
103 let node = dmap.get_or_insert(path)?;
104 if let NodeData::CachedDirectory { .. } = &node.data {
104 if let NodeData::CachedDirectory { .. } = &node.data {
105 node.data = NodeData::None
105 node.data = NodeData::None
106 }
106 }
107 }
107 }
108 for (path, mtime) in &new_cachable {
108 for (path, mtime) in &new_cachable {
109 let node = dmap.get_or_insert(path)?;
109 let node = dmap.get_or_insert(path)?;
110 match &node.data {
110 match &node.data {
111 NodeData::Entry(_) => {} // Don’t overwrite an entry
111 NodeData::Entry(_) => {} // Don’t overwrite an entry
112 NodeData::CachedDirectory { .. } | NodeData::None => {
112 NodeData::CachedDirectory { .. } | NodeData::None => {
113 node.data = NodeData::CachedDirectory { mtime: *mtime }
113 node.data = NodeData::CachedDirectory { mtime: *mtime }
114 }
114 }
115 }
115 }
116 }
116 }
117
117
118 Ok((outcome, warnings))
118 Ok((outcome, warnings))
119 }
119 }
120
120
121 /// Bag of random things needed by various parts of the algorithm. Reduces the
121 /// Bag of random things needed by various parts of the algorithm. Reduces the
122 /// number of parameters passed to functions.
122 /// number of parameters passed to functions.
123 struct StatusCommon<'a, 'tree, 'on_disk: 'tree> {
123 struct StatusCommon<'a, 'tree, 'on_disk: 'tree> {
124 dmap: &'tree DirstateMap<'on_disk>,
124 dmap: &'tree DirstateMap<'on_disk>,
125 options: StatusOptions,
125 options: StatusOptions,
126 matcher: &'a (dyn Matcher + Sync),
126 matcher: &'a (dyn Matcher + Sync),
127 ignore_fn: IgnoreFnType<'a>,
127 ignore_fn: IgnoreFnType<'a>,
128 outcome: Mutex<DirstateStatus<'on_disk>>,
128 outcome: Mutex<DirstateStatus<'on_disk>>,
129 new_cachable_directories:
129 new_cachable_directories:
130 Mutex<Vec<(Cow<'on_disk, HgPath>, TruncatedTimestamp)>>,
130 Mutex<Vec<(Cow<'on_disk, HgPath>, TruncatedTimestamp)>>,
131 outated_cached_directories: Mutex<Vec<Cow<'on_disk, HgPath>>>,
131 outated_cached_directories: Mutex<Vec<Cow<'on_disk, HgPath>>>,
132
132
133 /// Whether ignore files like `.hgignore` have changed since the previous
133 /// Whether ignore files like `.hgignore` have changed since the previous
134 /// time a `status()` call wrote their hash to the dirstate. `None` means
134 /// time a `status()` call wrote their hash to the dirstate. `None` means
135 /// we don’t know as this run doesn’t list either ignored or uknown files
135 /// we don’t know as this run doesn’t list either ignored or uknown files
136 /// and therefore isn’t reading `.hgignore`.
136 /// and therefore isn’t reading `.hgignore`.
137 ignore_patterns_have_changed: Option<bool>,
137 ignore_patterns_have_changed: Option<bool>,
138
138
139 /// The current time at the start of the `status()` algorithm, as measured
139 /// The current time at the start of the `status()` algorithm, as measured
140 /// and possibly truncated by the filesystem.
140 /// and possibly truncated by the filesystem.
141 filesystem_time_at_status_start: Option<SystemTime>,
141 filesystem_time_at_status_start: Option<SystemTime>,
142 }
142 }
143
143
144 impl<'a, 'tree, 'on_disk> StatusCommon<'a, 'tree, 'on_disk> {
144 impl<'a, 'tree, 'on_disk> StatusCommon<'a, 'tree, 'on_disk> {
145 fn read_dir(
145 fn read_dir(
146 &self,
146 &self,
147 hg_path: &HgPath,
147 hg_path: &HgPath,
148 fs_path: &Path,
148 fs_path: &Path,
149 is_at_repo_root: bool,
149 is_at_repo_root: bool,
150 ) -> Result<Vec<DirEntry>, ()> {
150 ) -> Result<Vec<DirEntry>, ()> {
151 DirEntry::read_dir(fs_path, is_at_repo_root)
151 DirEntry::read_dir(fs_path, is_at_repo_root)
152 .map_err(|error| self.io_error(error, hg_path))
152 .map_err(|error| self.io_error(error, hg_path))
153 }
153 }
154
154
155 fn io_error(&self, error: std::io::Error, hg_path: &HgPath) {
155 fn io_error(&self, error: std::io::Error, hg_path: &HgPath) {
156 let errno = error.raw_os_error().expect("expected real OS error");
156 let errno = error.raw_os_error().expect("expected real OS error");
157 self.outcome
157 self.outcome
158 .lock()
158 .lock()
159 .unwrap()
159 .unwrap()
160 .bad
160 .bad
161 .push((hg_path.to_owned().into(), BadMatch::OsError(errno)))
161 .push((hg_path.to_owned().into(), BadMatch::OsError(errno)))
162 }
162 }
163
163
164 fn check_for_outdated_directory_cache(
164 fn check_for_outdated_directory_cache(
165 &self,
165 &self,
166 dirstate_node: &NodeRef<'tree, 'on_disk>,
166 dirstate_node: &NodeRef<'tree, 'on_disk>,
167 ) -> Result<(), DirstateV2ParseError> {
167 ) -> Result<(), DirstateV2ParseError> {
168 if self.ignore_patterns_have_changed == Some(true)
168 if self.ignore_patterns_have_changed == Some(true)
169 && dirstate_node.cached_directory_mtime()?.is_some()
169 && dirstate_node.cached_directory_mtime()?.is_some()
170 {
170 {
171 self.outated_cached_directories.lock().unwrap().push(
171 self.outated_cached_directories.lock().unwrap().push(
172 dirstate_node
172 dirstate_node
173 .full_path_borrowed(self.dmap.on_disk)?
173 .full_path_borrowed(self.dmap.on_disk)?
174 .detach_from_tree(),
174 .detach_from_tree(),
175 )
175 )
176 }
176 }
177 Ok(())
177 Ok(())
178 }
178 }
179
179
180 /// If this returns true, we can get accurate results by only using
180 /// If this returns true, we can get accurate results by only using
181 /// `symlink_metadata` for child nodes that exist in the dirstate and don’t
181 /// `symlink_metadata` for child nodes that exist in the dirstate and don’t
182 /// need to call `read_dir`.
182 /// need to call `read_dir`.
183 fn can_skip_fs_readdir(
183 fn can_skip_fs_readdir(
184 &self,
184 &self,
185 directory_metadata: Option<&std::fs::Metadata>,
185 directory_metadata: Option<&std::fs::Metadata>,
186 cached_directory_mtime: Option<TruncatedTimestamp>,
186 cached_directory_mtime: Option<TruncatedTimestamp>,
187 ) -> bool {
187 ) -> bool {
188 if !self.options.list_unknown && !self.options.list_ignored {
188 if !self.options.list_unknown && !self.options.list_ignored {
189 // All states that we care about listing have corresponding
189 // All states that we care about listing have corresponding
190 // dirstate entries.
190 // dirstate entries.
191 // This happens for example with `hg status -mard`.
191 // This happens for example with `hg status -mard`.
192 return true;
192 return true;
193 }
193 }
194 if !self.options.list_ignored
194 if !self.options.list_ignored
195 && self.ignore_patterns_have_changed == Some(false)
195 && self.ignore_patterns_have_changed == Some(false)
196 {
196 {
197 if let Some(cached_mtime) = cached_directory_mtime {
197 if let Some(cached_mtime) = cached_directory_mtime {
198 // The dirstate contains a cached mtime for this directory, set
198 // The dirstate contains a cached mtime for this directory, set
199 // by a previous run of the `status` algorithm which found this
199 // by a previous run of the `status` algorithm which found this
200 // directory eligible for `read_dir` caching.
200 // directory eligible for `read_dir` caching.
201 if let Some(meta) = directory_metadata {
201 if let Some(meta) = directory_metadata {
202 if cached_mtime
202 if cached_mtime
203 .likely_equal_to_mtime_of(meta)
203 .likely_equal_to_mtime_of(meta)
204 .unwrap_or(false)
204 .unwrap_or(false)
205 {
205 {
206 // The mtime of that directory has not changed
206 // The mtime of that directory has not changed
207 // since then, which means that the results of
207 // since then, which means that the results of
208 // `read_dir` should also be unchanged.
208 // `read_dir` should also be unchanged.
209 return true;
209 return true;
210 }
210 }
211 }
211 }
212 }
212 }
213 }
213 }
214 false
214 false
215 }
215 }
216
216
217 /// Returns whether all child entries of the filesystem directory have a
217 /// Returns whether all child entries of the filesystem directory have a
218 /// corresponding dirstate node or are ignored.
218 /// corresponding dirstate node or are ignored.
219 fn traverse_fs_directory_and_dirstate(
219 fn traverse_fs_directory_and_dirstate(
220 &self,
220 &self,
221 has_ignored_ancestor: bool,
221 has_ignored_ancestor: bool,
222 dirstate_nodes: ChildNodesRef<'tree, 'on_disk>,
222 dirstate_nodes: ChildNodesRef<'tree, 'on_disk>,
223 directory_hg_path: &BorrowedPath<'tree, 'on_disk>,
223 directory_hg_path: &BorrowedPath<'tree, 'on_disk>,
224 directory_fs_path: &Path,
224 directory_fs_path: &Path,
225 directory_metadata: Option<&std::fs::Metadata>,
225 directory_metadata: Option<&std::fs::Metadata>,
226 cached_directory_mtime: Option<TruncatedTimestamp>,
226 cached_directory_mtime: Option<TruncatedTimestamp>,
227 is_at_repo_root: bool,
227 is_at_repo_root: bool,
228 ) -> Result<bool, DirstateV2ParseError> {
228 ) -> Result<bool, DirstateV2ParseError> {
229 if self.can_skip_fs_readdir(directory_metadata, cached_directory_mtime)
229 if self.can_skip_fs_readdir(directory_metadata, cached_directory_mtime)
230 {
230 {
231 dirstate_nodes
231 dirstate_nodes
232 .par_iter()
232 .par_iter()
233 .map(|dirstate_node| {
233 .map(|dirstate_node| {
234 let fs_path = directory_fs_path.join(get_path_from_bytes(
234 let fs_path = directory_fs_path.join(get_path_from_bytes(
235 dirstate_node.base_name(self.dmap.on_disk)?.as_bytes(),
235 dirstate_node.base_name(self.dmap.on_disk)?.as_bytes(),
236 ));
236 ));
237 match std::fs::symlink_metadata(&fs_path) {
237 match std::fs::symlink_metadata(&fs_path) {
238 Ok(fs_metadata) => self.traverse_fs_and_dirstate(
238 Ok(fs_metadata) => self.traverse_fs_and_dirstate(
239 &fs_path,
239 &fs_path,
240 &fs_metadata,
240 &fs_metadata,
241 dirstate_node,
241 dirstate_node,
242 has_ignored_ancestor,
242 has_ignored_ancestor,
243 ),
243 ),
244 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
244 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
245 self.traverse_dirstate_only(dirstate_node)
245 self.traverse_dirstate_only(dirstate_node)
246 }
246 }
247 Err(error) => {
247 Err(error) => {
248 let hg_path =
248 let hg_path =
249 dirstate_node.full_path(self.dmap.on_disk)?;
249 dirstate_node.full_path(self.dmap.on_disk)?;
250 Ok(self.io_error(error, hg_path))
250 Ok(self.io_error(error, hg_path))
251 }
251 }
252 }
252 }
253 })
253 })
254 .collect::<Result<_, _>>()?;
254 .collect::<Result<_, _>>()?;
255
255
256 // We don’t know, so conservatively say this isn’t the case
256 // We don’t know, so conservatively say this isn’t the case
257 let children_all_have_dirstate_node_or_are_ignored = false;
257 let children_all_have_dirstate_node_or_are_ignored = false;
258
258
259 return Ok(children_all_have_dirstate_node_or_are_ignored);
259 return Ok(children_all_have_dirstate_node_or_are_ignored);
260 }
260 }
261
261
262 let mut fs_entries = if let Ok(entries) = self.read_dir(
262 let mut fs_entries = if let Ok(entries) = self.read_dir(
263 directory_hg_path,
263 directory_hg_path,
264 directory_fs_path,
264 directory_fs_path,
265 is_at_repo_root,
265 is_at_repo_root,
266 ) {
266 ) {
267 entries
267 entries
268 } else {
268 } else {
269 // Treat an unreadable directory (typically because of insufficient
269 // Treat an unreadable directory (typically because of insufficient
270 // permissions) like an empty directory. `self.read_dir` has
270 // permissions) like an empty directory. `self.read_dir` has
271 // already called `self.io_error` so a warning will be emitted.
271 // already called `self.io_error` so a warning will be emitted.
272 Vec::new()
272 Vec::new()
273 };
273 };
274
274
275 // `merge_join_by` requires both its input iterators to be sorted:
275 // `merge_join_by` requires both its input iterators to be sorted:
276
276
277 let dirstate_nodes = dirstate_nodes.sorted();
277 let dirstate_nodes = dirstate_nodes.sorted();
278 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
278 // `sort_unstable_by_key` doesn’t allow keys borrowing from the value:
279 // https://github.com/rust-lang/rust/issues/34162
279 // https://github.com/rust-lang/rust/issues/34162
280 fs_entries.sort_unstable_by(|e1, e2| e1.base_name.cmp(&e2.base_name));
280 fs_entries.sort_unstable_by(|e1, e2| e1.base_name.cmp(&e2.base_name));
281
281
282 // Propagate here any error that would happen inside the comparison
282 // Propagate here any error that would happen inside the comparison
283 // callback below
283 // callback below
284 for dirstate_node in &dirstate_nodes {
284 for dirstate_node in &dirstate_nodes {
285 dirstate_node.base_name(self.dmap.on_disk)?;
285 dirstate_node.base_name(self.dmap.on_disk)?;
286 }
286 }
287 itertools::merge_join_by(
287 itertools::merge_join_by(
288 dirstate_nodes,
288 dirstate_nodes,
289 &fs_entries,
289 &fs_entries,
290 |dirstate_node, fs_entry| {
290 |dirstate_node, fs_entry| {
291 // This `unwrap` never panics because we already propagated
291 // This `unwrap` never panics because we already propagated
292 // those errors above
292 // those errors above
293 dirstate_node
293 dirstate_node
294 .base_name(self.dmap.on_disk)
294 .base_name(self.dmap.on_disk)
295 .unwrap()
295 .unwrap()
296 .cmp(&fs_entry.base_name)
296 .cmp(&fs_entry.base_name)
297 },
297 },
298 )
298 )
299 .par_bridge()
299 .par_bridge()
300 .map(|pair| {
300 .map(|pair| {
301 use itertools::EitherOrBoth::*;
301 use itertools::EitherOrBoth::*;
302 let has_dirstate_node_or_is_ignored;
302 let has_dirstate_node_or_is_ignored;
303 match pair {
303 match pair {
304 Both(dirstate_node, fs_entry) => {
304 Both(dirstate_node, fs_entry) => {
305 self.traverse_fs_and_dirstate(
305 self.traverse_fs_and_dirstate(
306 &fs_entry.full_path,
306 &fs_entry.full_path,
307 &fs_entry.metadata,
307 &fs_entry.metadata,
308 dirstate_node,
308 dirstate_node,
309 has_ignored_ancestor,
309 has_ignored_ancestor,
310 )?;
310 )?;
311 has_dirstate_node_or_is_ignored = true
311 has_dirstate_node_or_is_ignored = true
312 }
312 }
313 Left(dirstate_node) => {
313 Left(dirstate_node) => {
314 self.traverse_dirstate_only(dirstate_node)?;
314 self.traverse_dirstate_only(dirstate_node)?;
315 has_dirstate_node_or_is_ignored = true;
315 has_dirstate_node_or_is_ignored = true;
316 }
316 }
317 Right(fs_entry) => {
317 Right(fs_entry) => {
318 has_dirstate_node_or_is_ignored = self.traverse_fs_only(
318 has_dirstate_node_or_is_ignored = self.traverse_fs_only(
319 has_ignored_ancestor,
319 has_ignored_ancestor,
320 directory_hg_path,
320 directory_hg_path,
321 fs_entry,
321 fs_entry,
322 )
322 )
323 }
323 }
324 }
324 }
325 Ok(has_dirstate_node_or_is_ignored)
325 Ok(has_dirstate_node_or_is_ignored)
326 })
326 })
327 .try_reduce(|| true, |a, b| Ok(a && b))
327 .try_reduce(|| true, |a, b| Ok(a && b))
328 }
328 }
329
329
330 fn traverse_fs_and_dirstate(
330 fn traverse_fs_and_dirstate(
331 &self,
331 &self,
332 fs_path: &Path,
332 fs_path: &Path,
333 fs_metadata: &std::fs::Metadata,
333 fs_metadata: &std::fs::Metadata,
334 dirstate_node: NodeRef<'tree, 'on_disk>,
334 dirstate_node: NodeRef<'tree, 'on_disk>,
335 has_ignored_ancestor: bool,
335 has_ignored_ancestor: bool,
336 ) -> Result<(), DirstateV2ParseError> {
336 ) -> Result<(), DirstateV2ParseError> {
337 self.check_for_outdated_directory_cache(&dirstate_node)?;
337 self.check_for_outdated_directory_cache(&dirstate_node)?;
338 let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?;
338 let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?;
339 let file_type = fs_metadata.file_type();
339 let file_type = fs_metadata.file_type();
340 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
340 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
341 if !file_or_symlink {
341 if !file_or_symlink {
342 // If we previously had a file here, it was removed (with
342 // If we previously had a file here, it was removed (with
343 // `hg rm` or similar) or deleted before it could be
343 // `hg rm` or similar) or deleted before it could be
344 // replaced by a directory or something else.
344 // replaced by a directory or something else.
345 self.mark_removed_or_deleted_if_file(
345 self.mark_removed_or_deleted_if_file(
346 &hg_path,
346 &hg_path,
347 dirstate_node.state()?,
347 dirstate_node.state()?,
348 );
348 );
349 }
349 }
350 if file_type.is_dir() {
350 if file_type.is_dir() {
351 if self.options.collect_traversed_dirs {
351 if self.options.collect_traversed_dirs {
352 self.outcome
352 self.outcome
353 .lock()
353 .lock()
354 .unwrap()
354 .unwrap()
355 .traversed
355 .traversed
356 .push(hg_path.detach_from_tree())
356 .push(hg_path.detach_from_tree())
357 }
357 }
358 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path);
358 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(hg_path);
359 let is_at_repo_root = false;
359 let is_at_repo_root = false;
360 let children_all_have_dirstate_node_or_are_ignored = self
360 let children_all_have_dirstate_node_or_are_ignored = self
361 .traverse_fs_directory_and_dirstate(
361 .traverse_fs_directory_and_dirstate(
362 is_ignored,
362 is_ignored,
363 dirstate_node.children(self.dmap.on_disk)?,
363 dirstate_node.children(self.dmap.on_disk)?,
364 hg_path,
364 hg_path,
365 fs_path,
365 fs_path,
366 Some(fs_metadata),
366 Some(fs_metadata),
367 dirstate_node.cached_directory_mtime()?,
367 dirstate_node.cached_directory_mtime()?,
368 is_at_repo_root,
368 is_at_repo_root,
369 )?;
369 )?;
370 self.maybe_save_directory_mtime(
370 self.maybe_save_directory_mtime(
371 children_all_have_dirstate_node_or_are_ignored,
371 children_all_have_dirstate_node_or_are_ignored,
372 fs_metadata,
372 fs_metadata,
373 dirstate_node,
373 dirstate_node,
374 )?
374 )?
375 } else {
375 } else {
376 if file_or_symlink && self.matcher.matches(hg_path) {
376 if file_or_symlink && self.matcher.matches(hg_path) {
377 if let Some(state) = dirstate_node.state()? {
377 if let Some(state) = dirstate_node.state()? {
378 match state {
378 match state {
379 EntryState::Added => self
379 EntryState::Added => self
380 .outcome
380 .outcome
381 .lock()
381 .lock()
382 .unwrap()
382 .unwrap()
383 .added
383 .added
384 .push(hg_path.detach_from_tree()),
384 .push(hg_path.detach_from_tree()),
385 EntryState::Removed => self
385 EntryState::Removed => self
386 .outcome
386 .outcome
387 .lock()
387 .lock()
388 .unwrap()
388 .unwrap()
389 .removed
389 .removed
390 .push(hg_path.detach_from_tree()),
390 .push(hg_path.detach_from_tree()),
391 EntryState::Merged => self
391 EntryState::Merged => self
392 .outcome
392 .outcome
393 .lock()
393 .lock()
394 .unwrap()
394 .unwrap()
395 .modified
395 .modified
396 .push(hg_path.detach_from_tree()),
396 .push(hg_path.detach_from_tree()),
397 EntryState::Normal => self
397 EntryState::Normal => self
398 .handle_normal_file(&dirstate_node, fs_metadata)?,
398 .handle_normal_file(&dirstate_node, fs_metadata)?,
399 }
399 }
400 } else {
400 } else {
401 // `node.entry.is_none()` indicates a "directory"
401 // `node.entry.is_none()` indicates a "directory"
402 // node, but the filesystem has a file
402 // node, but the filesystem has a file
403 self.mark_unknown_or_ignored(
403 self.mark_unknown_or_ignored(
404 has_ignored_ancestor,
404 has_ignored_ancestor,
405 hg_path,
405 hg_path,
406 );
406 );
407 }
407 }
408 }
408 }
409
409
410 for child_node in dirstate_node.children(self.dmap.on_disk)?.iter()
410 for child_node in dirstate_node.children(self.dmap.on_disk)?.iter()
411 {
411 {
412 self.traverse_dirstate_only(child_node)?
412 self.traverse_dirstate_only(child_node)?
413 }
413 }
414 }
414 }
415 Ok(())
415 Ok(())
416 }
416 }
417
417
418 fn maybe_save_directory_mtime(
418 fn maybe_save_directory_mtime(
419 &self,
419 &self,
420 children_all_have_dirstate_node_or_are_ignored: bool,
420 children_all_have_dirstate_node_or_are_ignored: bool,
421 directory_metadata: &std::fs::Metadata,
421 directory_metadata: &std::fs::Metadata,
422 dirstate_node: NodeRef<'tree, 'on_disk>,
422 dirstate_node: NodeRef<'tree, 'on_disk>,
423 ) -> Result<(), DirstateV2ParseError> {
423 ) -> Result<(), DirstateV2ParseError> {
424 if children_all_have_dirstate_node_or_are_ignored {
424 if children_all_have_dirstate_node_or_are_ignored {
425 // All filesystem directory entries from `read_dir` have a
425 // All filesystem directory entries from `read_dir` have a
426 // corresponding node in the dirstate, so we can reconstitute the
426 // corresponding node in the dirstate, so we can reconstitute the
427 // names of those entries without calling `read_dir` again.
427 // names of those entries without calling `read_dir` again.
428 if let (Some(status_start), Ok(directory_mtime)) = (
428 if let (Some(status_start), Ok(directory_mtime)) = (
429 &self.filesystem_time_at_status_start,
429 &self.filesystem_time_at_status_start,
430 directory_metadata.modified(),
430 directory_metadata.modified(),
431 ) {
431 ) {
432 // Although the Rust standard library’s `SystemTime` type
432 // Although the Rust standard library’s `SystemTime` type
433 // has nanosecond precision, the times reported for a
433 // has nanosecond precision, the times reported for a
434 // directory’s (or file’s) modified time may have lower
434 // directory’s (or file’s) modified time may have lower
435 // resolution based on the filesystem (for example ext3
435 // resolution based on the filesystem (for example ext3
436 // only stores integer seconds), kernel (see
436 // only stores integer seconds), kernel (see
437 // https://stackoverflow.com/a/14393315/1162888), etc.
437 // https://stackoverflow.com/a/14393315/1162888), etc.
438 if &directory_mtime >= status_start {
438 if &directory_mtime >= status_start {
439 // The directory was modified too recently, don’t cache its
439 // The directory was modified too recently, don’t cache its
440 // `read_dir` results.
440 // `read_dir` results.
441 //
441 //
442 // A timeline like this is possible:
442 // A timeline like this is possible:
443 //
443 //
444 // 1. A change to this directory (direct child was
444 // 1. A change to this directory (direct child was
445 // added or removed) cause its mtime to be set
445 // added or removed) cause its mtime to be set
446 // (possibly truncated) to `directory_mtime`
446 // (possibly truncated) to `directory_mtime`
447 // 2. This `status` algorithm calls `read_dir`
447 // 2. This `status` algorithm calls `read_dir`
448 // 3. An other change is made to the same directory is
448 // 3. An other change is made to the same directory is
449 // made so that calling `read_dir` agin would give
449 // made so that calling `read_dir` agin would give
450 // different results, but soon enough after 1. that
450 // different results, but soon enough after 1. that
451 // the mtime stays the same
451 // the mtime stays the same
452 //
452 //
453 // On a system where the time resolution poor, this
453 // On a system where the time resolution poor, this
454 // scenario is not unlikely if all three steps are caused
454 // scenario is not unlikely if all three steps are caused
455 // by the same script.
455 // by the same script.
456 } else {
456 } else {
457 // We’ve observed (through `status_start`) that time has
457 // We’ve observed (through `status_start`) that time has
458 // “progressed” since `directory_mtime`, so any further
458 // “progressed” since `directory_mtime`, so any further
459 // change to this directory is extremely likely to cause a
459 // change to this directory is extremely likely to cause a
460 // different mtime.
460 // different mtime.
461 //
461 //
462 // Having the same mtime again is not entirely impossible
462 // Having the same mtime again is not entirely impossible
463 // since the system clock is not monotonous. It could jump
463 // since the system clock is not monotonous. It could jump
464 // backward to some point before `directory_mtime`, then a
464 // backward to some point before `directory_mtime`, then a
465 // directory change could potentially happen during exactly
465 // directory change could potentially happen during exactly
466 // the wrong tick.
466 // the wrong tick.
467 //
467 //
468 // We deem this scenario (unlike the previous one) to be
468 // We deem this scenario (unlike the previous one) to be
469 // unlikely enough in practice.
469 // unlikely enough in practice.
470 let truncated = TruncatedTimestamp::from(directory_mtime);
470 let truncated = TruncatedTimestamp::from(directory_mtime);
471 let is_up_to_date = if let Some(cached) =
471 let is_up_to_date = if let Some(cached) =
472 dirstate_node.cached_directory_mtime()?
472 dirstate_node.cached_directory_mtime()?
473 {
473 {
474 cached.likely_equal(truncated)
474 cached.likely_equal(truncated)
475 } else {
475 } else {
476 false
476 false
477 };
477 };
478 if !is_up_to_date {
478 if !is_up_to_date {
479 let hg_path = dirstate_node
479 let hg_path = dirstate_node
480 .full_path_borrowed(self.dmap.on_disk)?
480 .full_path_borrowed(self.dmap.on_disk)?
481 .detach_from_tree();
481 .detach_from_tree();
482 self.new_cachable_directories
482 self.new_cachable_directories
483 .lock()
483 .lock()
484 .unwrap()
484 .unwrap()
485 .push((hg_path, truncated))
485 .push((hg_path, truncated))
486 }
486 }
487 }
487 }
488 }
488 }
489 }
489 }
490 Ok(())
490 Ok(())
491 }
491 }
492
492
493 /// A file with `EntryState::Normal` in the dirstate was found in the
493 /// A file with `EntryState::Normal` in the dirstate was found in the
494 /// filesystem
494 /// filesystem
495 fn handle_normal_file(
495 fn handle_normal_file(
496 &self,
496 &self,
497 dirstate_node: &NodeRef<'tree, 'on_disk>,
497 dirstate_node: &NodeRef<'tree, 'on_disk>,
498 fs_metadata: &std::fs::Metadata,
498 fs_metadata: &std::fs::Metadata,
499 ) -> Result<(), DirstateV2ParseError> {
499 ) -> Result<(), DirstateV2ParseError> {
500 // Keep the low 31 bits
500 // Keep the low 31 bits
501 fn truncate_u64(value: u64) -> i32 {
501 fn truncate_u64(value: u64) -> i32 {
502 (value & 0x7FFF_FFFF) as i32
502 (value & 0x7FFF_FFFF) as i32
503 }
503 }
504
504
505 let entry = dirstate_node
505 let entry = dirstate_node
506 .entry()?
506 .entry()?
507 .expect("handle_normal_file called with entry-less node");
507 .expect("handle_normal_file called with entry-less node");
508 let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?;
508 let hg_path = &dirstate_node.full_path_borrowed(self.dmap.on_disk)?;
509 let mode_changed =
509 let mode_changed =
510 || self.options.check_exec && entry.mode_changed(fs_metadata);
510 || self.options.check_exec && entry.mode_changed(fs_metadata);
511 let size = entry.size();
511 let size = entry.size();
512 let size_changed = size != truncate_u64(fs_metadata.len());
512 let size_changed = size != truncate_u64(fs_metadata.len());
513 if size >= 0 && size_changed && fs_metadata.file_type().is_symlink() {
513 if size >= 0 && size_changed && fs_metadata.file_type().is_symlink() {
514 // issue6456: Size returned may be longer due to encryption
514 // issue6456: Size returned may be longer due to encryption
515 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
515 // on EXT-4 fscrypt. TODO maybe only do it on EXT4?
516 self.outcome
516 self.outcome
517 .lock()
517 .lock()
518 .unwrap()
518 .unwrap()
519 .unsure
519 .unsure
520 .push(hg_path.detach_from_tree())
520 .push(hg_path.detach_from_tree())
521 } else if dirstate_node.has_copy_source()
521 } else if dirstate_node.has_copy_source()
522 || entry.is_from_other_parent()
522 || entry.is_from_other_parent()
523 || (size >= 0 && (size_changed || mode_changed()))
523 || (size >= 0 && (size_changed || mode_changed()))
524 {
524 {
525 self.outcome
525 self.outcome
526 .lock()
526 .lock()
527 .unwrap()
527 .unwrap()
528 .modified
528 .modified
529 .push(hg_path.detach_from_tree())
529 .push(hg_path.detach_from_tree())
530 } else {
530 } else {
531 let mtime_looks_clean;
531 let mtime_looks_clean;
532 if let Some(dirstate_mtime) = entry.truncated_mtime() {
532 if let Some(dirstate_mtime) = entry.truncated_mtime() {
533 let fs_mtime = TruncatedTimestamp::for_mtime_of(fs_metadata)
533 let fs_mtime = TruncatedTimestamp::for_mtime_of(fs_metadata)
534 .expect("OS/libc does not support mtime?");
534 .expect("OS/libc does not support mtime?");
535 // There might be a change in the future if for example the
536 // internal clock become off while process run, but this is a
537 // case where the issues the user would face
538 // would be a lot worse and there is nothing we
539 // can really do.
535 mtime_looks_clean = fs_mtime.likely_equal(dirstate_mtime)
540 mtime_looks_clean = fs_mtime.likely_equal(dirstate_mtime)
536 && !fs_mtime.likely_equal(self.options.last_normal_time)
537 } else {
541 } else {
538 // No mtime in the dirstate entry
542 // No mtime in the dirstate entry
539 mtime_looks_clean = false
543 mtime_looks_clean = false
540 };
544 };
541 if !mtime_looks_clean {
545 if !mtime_looks_clean {
542 self.outcome
546 self.outcome
543 .lock()
547 .lock()
544 .unwrap()
548 .unwrap()
545 .unsure
549 .unsure
546 .push(hg_path.detach_from_tree())
550 .push(hg_path.detach_from_tree())
547 } else if self.options.list_clean {
551 } else if self.options.list_clean {
548 self.outcome
552 self.outcome
549 .lock()
553 .lock()
550 .unwrap()
554 .unwrap()
551 .clean
555 .clean
552 .push(hg_path.detach_from_tree())
556 .push(hg_path.detach_from_tree())
553 }
557 }
554 }
558 }
555 Ok(())
559 Ok(())
556 }
560 }
557
561
558 /// A node in the dirstate tree has no corresponding filesystem entry
562 /// A node in the dirstate tree has no corresponding filesystem entry
559 fn traverse_dirstate_only(
563 fn traverse_dirstate_only(
560 &self,
564 &self,
561 dirstate_node: NodeRef<'tree, 'on_disk>,
565 dirstate_node: NodeRef<'tree, 'on_disk>,
562 ) -> Result<(), DirstateV2ParseError> {
566 ) -> Result<(), DirstateV2ParseError> {
563 self.check_for_outdated_directory_cache(&dirstate_node)?;
567 self.check_for_outdated_directory_cache(&dirstate_node)?;
564 self.mark_removed_or_deleted_if_file(
568 self.mark_removed_or_deleted_if_file(
565 &dirstate_node.full_path_borrowed(self.dmap.on_disk)?,
569 &dirstate_node.full_path_borrowed(self.dmap.on_disk)?,
566 dirstate_node.state()?,
570 dirstate_node.state()?,
567 );
571 );
568 dirstate_node
572 dirstate_node
569 .children(self.dmap.on_disk)?
573 .children(self.dmap.on_disk)?
570 .par_iter()
574 .par_iter()
571 .map(|child_node| self.traverse_dirstate_only(child_node))
575 .map(|child_node| self.traverse_dirstate_only(child_node))
572 .collect()
576 .collect()
573 }
577 }
574
578
575 /// A node in the dirstate tree has no corresponding *file* on the
579 /// A node in the dirstate tree has no corresponding *file* on the
576 /// filesystem
580 /// filesystem
577 ///
581 ///
578 /// Does nothing on a "directory" node
582 /// Does nothing on a "directory" node
579 fn mark_removed_or_deleted_if_file(
583 fn mark_removed_or_deleted_if_file(
580 &self,
584 &self,
581 hg_path: &BorrowedPath<'tree, 'on_disk>,
585 hg_path: &BorrowedPath<'tree, 'on_disk>,
582 dirstate_node_state: Option<EntryState>,
586 dirstate_node_state: Option<EntryState>,
583 ) {
587 ) {
584 if let Some(state) = dirstate_node_state {
588 if let Some(state) = dirstate_node_state {
585 if self.matcher.matches(hg_path) {
589 if self.matcher.matches(hg_path) {
586 if let EntryState::Removed = state {
590 if let EntryState::Removed = state {
587 self.outcome
591 self.outcome
588 .lock()
592 .lock()
589 .unwrap()
593 .unwrap()
590 .removed
594 .removed
591 .push(hg_path.detach_from_tree())
595 .push(hg_path.detach_from_tree())
592 } else {
596 } else {
593 self.outcome
597 self.outcome
594 .lock()
598 .lock()
595 .unwrap()
599 .unwrap()
596 .deleted
600 .deleted
597 .push(hg_path.detach_from_tree())
601 .push(hg_path.detach_from_tree())
598 }
602 }
599 }
603 }
600 }
604 }
601 }
605 }
602
606
603 /// Something in the filesystem has no corresponding dirstate node
607 /// Something in the filesystem has no corresponding dirstate node
604 ///
608 ///
605 /// Returns whether that path is ignored
609 /// Returns whether that path is ignored
606 fn traverse_fs_only(
610 fn traverse_fs_only(
607 &self,
611 &self,
608 has_ignored_ancestor: bool,
612 has_ignored_ancestor: bool,
609 directory_hg_path: &HgPath,
613 directory_hg_path: &HgPath,
610 fs_entry: &DirEntry,
614 fs_entry: &DirEntry,
611 ) -> bool {
615 ) -> bool {
612 let hg_path = directory_hg_path.join(&fs_entry.base_name);
616 let hg_path = directory_hg_path.join(&fs_entry.base_name);
613 let file_type = fs_entry.metadata.file_type();
617 let file_type = fs_entry.metadata.file_type();
614 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
618 let file_or_symlink = file_type.is_file() || file_type.is_symlink();
615 if file_type.is_dir() {
619 if file_type.is_dir() {
616 let is_ignored =
620 let is_ignored =
617 has_ignored_ancestor || (self.ignore_fn)(&hg_path);
621 has_ignored_ancestor || (self.ignore_fn)(&hg_path);
618 let traverse_children = if is_ignored {
622 let traverse_children = if is_ignored {
619 // Descendants of an ignored directory are all ignored
623 // Descendants of an ignored directory are all ignored
620 self.options.list_ignored
624 self.options.list_ignored
621 } else {
625 } else {
622 // Descendants of an unknown directory may be either unknown or
626 // Descendants of an unknown directory may be either unknown or
623 // ignored
627 // ignored
624 self.options.list_unknown || self.options.list_ignored
628 self.options.list_unknown || self.options.list_ignored
625 };
629 };
626 if traverse_children {
630 if traverse_children {
627 let is_at_repo_root = false;
631 let is_at_repo_root = false;
628 if let Ok(children_fs_entries) = self.read_dir(
632 if let Ok(children_fs_entries) = self.read_dir(
629 &hg_path,
633 &hg_path,
630 &fs_entry.full_path,
634 &fs_entry.full_path,
631 is_at_repo_root,
635 is_at_repo_root,
632 ) {
636 ) {
633 children_fs_entries.par_iter().for_each(|child_fs_entry| {
637 children_fs_entries.par_iter().for_each(|child_fs_entry| {
634 self.traverse_fs_only(
638 self.traverse_fs_only(
635 is_ignored,
639 is_ignored,
636 &hg_path,
640 &hg_path,
637 child_fs_entry,
641 child_fs_entry,
638 );
642 );
639 })
643 })
640 }
644 }
641 }
645 }
642 if self.options.collect_traversed_dirs {
646 if self.options.collect_traversed_dirs {
643 self.outcome.lock().unwrap().traversed.push(hg_path.into())
647 self.outcome.lock().unwrap().traversed.push(hg_path.into())
644 }
648 }
645 is_ignored
649 is_ignored
646 } else {
650 } else {
647 if file_or_symlink {
651 if file_or_symlink {
648 if self.matcher.matches(&hg_path) {
652 if self.matcher.matches(&hg_path) {
649 self.mark_unknown_or_ignored(
653 self.mark_unknown_or_ignored(
650 has_ignored_ancestor,
654 has_ignored_ancestor,
651 &BorrowedPath::InMemory(&hg_path),
655 &BorrowedPath::InMemory(&hg_path),
652 )
656 )
653 } else {
657 } else {
654 // We haven’t computed whether this path is ignored. It
658 // We haven’t computed whether this path is ignored. It
655 // might not be, and a future run of status might have a
659 // might not be, and a future run of status might have a
656 // different matcher that matches it. So treat it as not
660 // different matcher that matches it. So treat it as not
657 // ignored. That is, inhibit readdir caching of the parent
661 // ignored. That is, inhibit readdir caching of the parent
658 // directory.
662 // directory.
659 false
663 false
660 }
664 }
661 } else {
665 } else {
662 // This is neither a directory, a plain file, or a symlink.
666 // This is neither a directory, a plain file, or a symlink.
663 // Treat it like an ignored file.
667 // Treat it like an ignored file.
664 true
668 true
665 }
669 }
666 }
670 }
667 }
671 }
668
672
669 /// Returns whether that path is ignored
673 /// Returns whether that path is ignored
670 fn mark_unknown_or_ignored(
674 fn mark_unknown_or_ignored(
671 &self,
675 &self,
672 has_ignored_ancestor: bool,
676 has_ignored_ancestor: bool,
673 hg_path: &BorrowedPath<'_, 'on_disk>,
677 hg_path: &BorrowedPath<'_, 'on_disk>,
674 ) -> bool {
678 ) -> bool {
675 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path);
679 let is_ignored = has_ignored_ancestor || (self.ignore_fn)(&hg_path);
676 if is_ignored {
680 if is_ignored {
677 if self.options.list_ignored {
681 if self.options.list_ignored {
678 self.outcome
682 self.outcome
679 .lock()
683 .lock()
680 .unwrap()
684 .unwrap()
681 .ignored
685 .ignored
682 .push(hg_path.detach_from_tree())
686 .push(hg_path.detach_from_tree())
683 }
687 }
684 } else {
688 } else {
685 if self.options.list_unknown {
689 if self.options.list_unknown {
686 self.outcome
690 self.outcome
687 .lock()
691 .lock()
688 .unwrap()
692 .unwrap()
689 .unknown
693 .unknown
690 .push(hg_path.detach_from_tree())
694 .push(hg_path.detach_from_tree())
691 }
695 }
692 }
696 }
693 is_ignored
697 is_ignored
694 }
698 }
695 }
699 }
696
700
697 struct DirEntry {
701 struct DirEntry {
698 base_name: HgPathBuf,
702 base_name: HgPathBuf,
699 full_path: PathBuf,
703 full_path: PathBuf,
700 metadata: std::fs::Metadata,
704 metadata: std::fs::Metadata,
701 }
705 }
702
706
703 impl DirEntry {
707 impl DirEntry {
704 /// Returns **unsorted** entries in the given directory, with name and
708 /// Returns **unsorted** entries in the given directory, with name and
705 /// metadata.
709 /// metadata.
706 ///
710 ///
707 /// If a `.hg` sub-directory is encountered:
711 /// If a `.hg` sub-directory is encountered:
708 ///
712 ///
709 /// * At the repository root, ignore that sub-directory
713 /// * At the repository root, ignore that sub-directory
710 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty
714 /// * Elsewhere, we’re listing the content of a sub-repo. Return an empty
711 /// list instead.
715 /// list instead.
712 fn read_dir(path: &Path, is_at_repo_root: bool) -> io::Result<Vec<Self>> {
716 fn read_dir(path: &Path, is_at_repo_root: bool) -> io::Result<Vec<Self>> {
713 let mut results = Vec::new();
717 let mut results = Vec::new();
714 for entry in path.read_dir()? {
718 for entry in path.read_dir()? {
715 let entry = entry?;
719 let entry = entry?;
716 let metadata = entry.metadata()?;
720 let metadata = entry.metadata()?;
717 let name = get_bytes_from_os_string(entry.file_name());
721 let name = get_bytes_from_os_string(entry.file_name());
718 // FIXME don't do this when cached
722 // FIXME don't do this when cached
719 if name == b".hg" {
723 if name == b".hg" {
720 if is_at_repo_root {
724 if is_at_repo_root {
721 // Skip the repo’s own .hg (might be a symlink)
725 // Skip the repo’s own .hg (might be a symlink)
722 continue;
726 continue;
723 } else if metadata.is_dir() {
727 } else if metadata.is_dir() {
724 // A .hg sub-directory at another location means a subrepo,
728 // A .hg sub-directory at another location means a subrepo,
725 // skip it entirely.
729 // skip it entirely.
726 return Ok(Vec::new());
730 return Ok(Vec::new());
727 }
731 }
728 }
732 }
729 results.push(DirEntry {
733 results.push(DirEntry {
730 base_name: name.into(),
734 base_name: name.into(),
731 full_path: entry.path(),
735 full_path: entry.path(),
732 metadata,
736 metadata,
733 })
737 })
734 }
738 }
735 Ok(results)
739 Ok(results)
736 }
740 }
737 }
741 }
738
742
739 /// Return the `mtime` of a temporary file newly-created in the `.hg` directory
743 /// Return the `mtime` of a temporary file newly-created in the `.hg` directory
740 /// of the give repository.
744 /// of the give repository.
741 ///
745 ///
742 /// This is similar to `SystemTime::now()`, with the result truncated to the
746 /// This is similar to `SystemTime::now()`, with the result truncated to the
743 /// same time resolution as other files’ modification times. Using `.hg`
747 /// same time resolution as other files’ modification times. Using `.hg`
744 /// instead of the system’s default temporary directory (such as `/tmp`) makes
748 /// instead of the system’s default temporary directory (such as `/tmp`) makes
745 /// it more likely the temporary file is in the same disk partition as contents
749 /// it more likely the temporary file is in the same disk partition as contents
746 /// of the working directory, which can matter since different filesystems may
750 /// of the working directory, which can matter since different filesystems may
747 /// store timestamps with different resolutions.
751 /// store timestamps with different resolutions.
748 ///
752 ///
749 /// This may fail, typically if we lack write permissions. In that case we
753 /// This may fail, typically if we lack write permissions. In that case we
750 /// should continue the `status()` algoritm anyway and consider the current
754 /// should continue the `status()` algoritm anyway and consider the current
751 /// date/time to be unknown.
755 /// date/time to be unknown.
752 fn filesystem_now(repo_root: &Path) -> Result<SystemTime, io::Error> {
756 fn filesystem_now(repo_root: &Path) -> Result<SystemTime, io::Error> {
753 tempfile::tempfile_in(repo_root.join(".hg"))?
757 tempfile::tempfile_in(repo_root.join(".hg"))?
754 .metadata()?
758 .metadata()?
755 .modified()
759 .modified()
756 }
760 }
@@ -1,71 +1,70 b''
1 // dirstate.rs
1 // dirstate.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::dirstate` module provided by the
8 //! Bindings for the `hg::dirstate` module provided by the
9 //! `hg-core` package.
9 //! `hg-core` package.
10 //!
10 //!
11 //! From Python, this will be seen as `mercurial.rustext.dirstate`
11 //! From Python, this will be seen as `mercurial.rustext.dirstate`
12 mod copymap;
12 mod copymap;
13 mod dirs_multiset;
13 mod dirs_multiset;
14 mod dirstate_map;
14 mod dirstate_map;
15 mod item;
15 mod item;
16 mod status;
16 mod status;
17 use self::item::DirstateItem;
17 use self::item::DirstateItem;
18 use crate::{
18 use crate::{
19 dirstate::{
19 dirstate::{
20 dirs_multiset::Dirs, dirstate_map::DirstateMap, status::status_wrapper,
20 dirs_multiset::Dirs, dirstate_map::DirstateMap, status::status_wrapper,
21 },
21 },
22 exceptions,
22 exceptions,
23 };
23 };
24 use cpython::{PyBytes, PyDict, PyList, PyModule, PyObject, PyResult, Python};
24 use cpython::{PyBytes, PyDict, PyList, PyModule, PyObject, PyResult, Python};
25 use hg::dirstate_tree::on_disk::V2_FORMAT_MARKER;
25 use hg::dirstate_tree::on_disk::V2_FORMAT_MARKER;
26
26
27 /// Create the module, with `__package__` given from parent
27 /// Create the module, with `__package__` given from parent
28 pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> {
28 pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> {
29 let dotted_name = &format!("{}.dirstate", package);
29 let dotted_name = &format!("{}.dirstate", package);
30 let m = PyModule::new(py, dotted_name)?;
30 let m = PyModule::new(py, dotted_name)?;
31
31
32 env_logger::init();
32 env_logger::init();
33
33
34 m.add(py, "__package__", package)?;
34 m.add(py, "__package__", package)?;
35 m.add(py, "__doc__", "Dirstate - Rust implementation")?;
35 m.add(py, "__doc__", "Dirstate - Rust implementation")?;
36
36
37 m.add(
37 m.add(
38 py,
38 py,
39 "FallbackError",
39 "FallbackError",
40 py.get_type::<exceptions::FallbackError>(),
40 py.get_type::<exceptions::FallbackError>(),
41 )?;
41 )?;
42 m.add_class::<Dirs>(py)?;
42 m.add_class::<Dirs>(py)?;
43 m.add_class::<DirstateMap>(py)?;
43 m.add_class::<DirstateMap>(py)?;
44 m.add_class::<DirstateItem>(py)?;
44 m.add_class::<DirstateItem>(py)?;
45 m.add(py, "V2_FORMAT_MARKER", PyBytes::new(py, V2_FORMAT_MARKER))?;
45 m.add(py, "V2_FORMAT_MARKER", PyBytes::new(py, V2_FORMAT_MARKER))?;
46 m.add(
46 m.add(
47 py,
47 py,
48 "status",
48 "status",
49 py_fn!(
49 py_fn!(
50 py,
50 py,
51 status_wrapper(
51 status_wrapper(
52 dmap: DirstateMap,
52 dmap: DirstateMap,
53 root_dir: PyObject,
53 root_dir: PyObject,
54 matcher: PyObject,
54 matcher: PyObject,
55 ignorefiles: PyList,
55 ignorefiles: PyList,
56 check_exec: bool,
56 check_exec: bool,
57 last_normal_time: (u32, u32),
58 list_clean: bool,
57 list_clean: bool,
59 list_ignored: bool,
58 list_ignored: bool,
60 list_unknown: bool,
59 list_unknown: bool,
61 collect_traversed_dirs: bool
60 collect_traversed_dirs: bool
62 )
61 )
63 ),
62 ),
64 )?;
63 )?;
65
64
66 let sys = PyModule::import(py, "sys")?;
65 let sys = PyModule::import(py, "sys")?;
67 let sys_modules: PyDict = sys.get(py, "modules")?.extract(py)?;
66 let sys_modules: PyDict = sys.get(py, "modules")?.extract(py)?;
68 sys_modules.set_item(py, dotted_name, &m)?;
67 sys_modules.set_item(py, dotted_name, &m)?;
69
68
70 Ok(m)
69 Ok(m)
71 }
70 }
@@ -1,301 +1,295 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::item::timestamp;
13 use crate::{dirstate::DirstateMap, exceptions::FallbackError};
12 use crate::{dirstate::DirstateMap, exceptions::FallbackError};
14 use cpython::exc::OSError;
13 use cpython::exc::OSError;
15 use cpython::{
14 use cpython::{
16 exc::ValueError, ObjectProtocol, PyBytes, PyErr, PyList, PyObject,
15 exc::ValueError, ObjectProtocol, PyBytes, PyErr, PyList, PyObject,
17 PyResult, PyTuple, Python, PythonObject, ToPyObject,
16 PyResult, PyTuple, Python, PythonObject, ToPyObject,
18 };
17 };
19 use hg::{
18 use hg::{
20 matchers::{AlwaysMatcher, FileMatcher, IncludeMatcher},
19 matchers::{AlwaysMatcher, FileMatcher, IncludeMatcher},
21 parse_pattern_syntax,
20 parse_pattern_syntax,
22 utils::{
21 utils::{
23 files::{get_bytes_from_path, get_path_from_bytes},
22 files::{get_bytes_from_path, get_path_from_bytes},
24 hg_path::{HgPath, HgPathBuf},
23 hg_path::{HgPath, HgPathBuf},
25 },
24 },
26 BadMatch, DirstateStatus, IgnorePattern, PatternFileWarning, StatusError,
25 BadMatch, DirstateStatus, IgnorePattern, PatternFileWarning, StatusError,
27 StatusOptions,
26 StatusOptions,
28 };
27 };
29 use std::borrow::Borrow;
28 use std::borrow::Borrow;
30
29
31 /// This will be useless once trait impls for collection are added to `PyBytes`
30 /// This will be useless once trait impls for collection are added to `PyBytes`
32 /// upstream.
31 /// upstream.
33 fn collect_pybytes_list(
32 fn collect_pybytes_list(
34 py: Python,
33 py: Python,
35 collection: &[impl AsRef<HgPath>],
34 collection: &[impl AsRef<HgPath>],
36 ) -> PyList {
35 ) -> PyList {
37 let list = PyList::new(py, &[]);
36 let list = PyList::new(py, &[]);
38
37
39 for path in collection.iter() {
38 for path in collection.iter() {
40 list.append(
39 list.append(
41 py,
40 py,
42 PyBytes::new(py, path.as_ref().as_bytes()).into_object(),
41 PyBytes::new(py, path.as_ref().as_bytes()).into_object(),
43 )
42 )
44 }
43 }
45
44
46 list
45 list
47 }
46 }
48
47
49 fn collect_bad_matches(
48 fn collect_bad_matches(
50 py: Python,
49 py: Python,
51 collection: &[(impl AsRef<HgPath>, BadMatch)],
50 collection: &[(impl AsRef<HgPath>, BadMatch)],
52 ) -> PyResult<PyList> {
51 ) -> PyResult<PyList> {
53 let list = PyList::new(py, &[]);
52 let list = PyList::new(py, &[]);
54
53
55 let os = py.import("os")?;
54 let os = py.import("os")?;
56 let get_error_message = |code: i32| -> PyResult<_> {
55 let get_error_message = |code: i32| -> PyResult<_> {
57 os.call(
56 os.call(
58 py,
57 py,
59 "strerror",
58 "strerror",
60 PyTuple::new(py, &[code.to_py_object(py).into_object()]),
59 PyTuple::new(py, &[code.to_py_object(py).into_object()]),
61 None,
60 None,
62 )
61 )
63 };
62 };
64
63
65 for (path, bad_match) in collection.iter() {
64 for (path, bad_match) in collection.iter() {
66 let message = match bad_match {
65 let message = match bad_match {
67 BadMatch::OsError(code) => get_error_message(*code)?,
66 BadMatch::OsError(code) => get_error_message(*code)?,
68 BadMatch::BadType(bad_type) => format!(
67 BadMatch::BadType(bad_type) => format!(
69 "unsupported file type (type is {})",
68 "unsupported file type (type is {})",
70 bad_type.to_string()
69 bad_type.to_string()
71 )
70 )
72 .to_py_object(py)
71 .to_py_object(py)
73 .into_object(),
72 .into_object(),
74 };
73 };
75 list.append(
74 list.append(
76 py,
75 py,
77 (PyBytes::new(py, path.as_ref().as_bytes()), message)
76 (PyBytes::new(py, path.as_ref().as_bytes()), message)
78 .to_py_object(py)
77 .to_py_object(py)
79 .into_object(),
78 .into_object(),
80 )
79 )
81 }
80 }
82
81
83 Ok(list)
82 Ok(list)
84 }
83 }
85
84
86 fn handle_fallback(py: Python, err: StatusError) -> PyErr {
85 fn handle_fallback(py: Python, err: StatusError) -> PyErr {
87 match err {
86 match err {
88 StatusError::Pattern(e) => {
87 StatusError::Pattern(e) => {
89 let as_string = e.to_string();
88 let as_string = e.to_string();
90 log::trace!("Rust status fallback: `{}`", &as_string);
89 log::trace!("Rust status fallback: `{}`", &as_string);
91
90
92 PyErr::new::<FallbackError, _>(py, &as_string)
91 PyErr::new::<FallbackError, _>(py, &as_string)
93 }
92 }
94 StatusError::IO(e) => PyErr::new::<OSError, _>(py, e.to_string()),
93 StatusError::IO(e) => PyErr::new::<OSError, _>(py, e.to_string()),
95 e => PyErr::new::<ValueError, _>(py, e.to_string()),
94 e => PyErr::new::<ValueError, _>(py, e.to_string()),
96 }
95 }
97 }
96 }
98
97
99 pub fn status_wrapper(
98 pub fn status_wrapper(
100 py: Python,
99 py: Python,
101 dmap: DirstateMap,
100 dmap: DirstateMap,
102 matcher: PyObject,
101 matcher: PyObject,
103 root_dir: PyObject,
102 root_dir: PyObject,
104 ignore_files: PyList,
103 ignore_files: PyList,
105 check_exec: bool,
104 check_exec: bool,
106 last_normal_time: (u32, u32),
107 list_clean: bool,
105 list_clean: bool,
108 list_ignored: bool,
106 list_ignored: bool,
109 list_unknown: bool,
107 list_unknown: bool,
110 collect_traversed_dirs: bool,
108 collect_traversed_dirs: bool,
111 ) -> PyResult<PyTuple> {
109 ) -> PyResult<PyTuple> {
112 let last_normal_time = timestamp(py, last_normal_time)?;
113 let bytes = root_dir.extract::<PyBytes>(py)?;
110 let bytes = root_dir.extract::<PyBytes>(py)?;
114 let root_dir = get_path_from_bytes(bytes.data(py));
111 let root_dir = get_path_from_bytes(bytes.data(py));
115
112
116 let dmap: DirstateMap = dmap.to_py_object(py);
113 let dmap: DirstateMap = dmap.to_py_object(py);
117 let mut dmap = dmap.get_inner_mut(py);
114 let mut dmap = dmap.get_inner_mut(py);
118
115
119 let ignore_files: PyResult<Vec<_>> = ignore_files
116 let ignore_files: PyResult<Vec<_>> = ignore_files
120 .iter(py)
117 .iter(py)
121 .map(|b| {
118 .map(|b| {
122 let file = b.extract::<PyBytes>(py)?;
119 let file = b.extract::<PyBytes>(py)?;
123 Ok(get_path_from_bytes(file.data(py)).to_owned())
120 Ok(get_path_from_bytes(file.data(py)).to_owned())
124 })
121 })
125 .collect();
122 .collect();
126 let ignore_files = ignore_files?;
123 let ignore_files = ignore_files?;
127
124
128 match matcher.get_type(py).name(py).borrow() {
125 match matcher.get_type(py).name(py).borrow() {
129 "alwaysmatcher" => {
126 "alwaysmatcher" => {
130 let matcher = AlwaysMatcher;
127 let matcher = AlwaysMatcher;
131 let (status_res, warnings) = dmap
128 let (status_res, warnings) = dmap
132 .status(
129 .status(
133 &matcher,
130 &matcher,
134 root_dir.to_path_buf(),
131 root_dir.to_path_buf(),
135 ignore_files,
132 ignore_files,
136 StatusOptions {
133 StatusOptions {
137 check_exec,
134 check_exec,
138 last_normal_time,
139 list_clean,
135 list_clean,
140 list_ignored,
136 list_ignored,
141 list_unknown,
137 list_unknown,
142 collect_traversed_dirs,
138 collect_traversed_dirs,
143 },
139 },
144 )
140 )
145 .map_err(|e| handle_fallback(py, e))?;
141 .map_err(|e| handle_fallback(py, e))?;
146 build_response(py, status_res, warnings)
142 build_response(py, status_res, warnings)
147 }
143 }
148 "exactmatcher" => {
144 "exactmatcher" => {
149 let files = matcher.call_method(
145 let files = matcher.call_method(
150 py,
146 py,
151 "files",
147 "files",
152 PyTuple::new(py, &[]),
148 PyTuple::new(py, &[]),
153 None,
149 None,
154 )?;
150 )?;
155 let files: PyList = files.cast_into(py)?;
151 let files: PyList = files.cast_into(py)?;
156 let files: PyResult<Vec<HgPathBuf>> = files
152 let files: PyResult<Vec<HgPathBuf>> = files
157 .iter(py)
153 .iter(py)
158 .map(|f| {
154 .map(|f| {
159 Ok(HgPathBuf::from_bytes(
155 Ok(HgPathBuf::from_bytes(
160 f.extract::<PyBytes>(py)?.data(py),
156 f.extract::<PyBytes>(py)?.data(py),
161 ))
157 ))
162 })
158 })
163 .collect();
159 .collect();
164
160
165 let files = files?;
161 let files = files?;
166 let matcher = FileMatcher::new(files.as_ref())
162 let matcher = FileMatcher::new(files.as_ref())
167 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
163 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
168 let (status_res, warnings) = dmap
164 let (status_res, warnings) = dmap
169 .status(
165 .status(
170 &matcher,
166 &matcher,
171 root_dir.to_path_buf(),
167 root_dir.to_path_buf(),
172 ignore_files,
168 ignore_files,
173 StatusOptions {
169 StatusOptions {
174 check_exec,
170 check_exec,
175 last_normal_time,
176 list_clean,
171 list_clean,
177 list_ignored,
172 list_ignored,
178 list_unknown,
173 list_unknown,
179 collect_traversed_dirs,
174 collect_traversed_dirs,
180 },
175 },
181 )
176 )
182 .map_err(|e| handle_fallback(py, e))?;
177 .map_err(|e| handle_fallback(py, e))?;
183 build_response(py, status_res, warnings)
178 build_response(py, status_res, warnings)
184 }
179 }
185 "includematcher" => {
180 "includematcher" => {
186 // Get the patterns from Python even though most of them are
181 // Get the patterns from Python even though most of them are
187 // redundant with those we will parse later on, as they include
182 // redundant with those we will parse later on, as they include
188 // those passed from the command line.
183 // those passed from the command line.
189 let ignore_patterns: PyResult<Vec<_>> = matcher
184 let ignore_patterns: PyResult<Vec<_>> = matcher
190 .getattr(py, "_kindpats")?
185 .getattr(py, "_kindpats")?
191 .iter(py)?
186 .iter(py)?
192 .map(|k| {
187 .map(|k| {
193 let k = k?;
188 let k = k?;
194 let syntax = parse_pattern_syntax(
189 let syntax = parse_pattern_syntax(
195 &[
190 &[
196 k.get_item(py, 0)?
191 k.get_item(py, 0)?
197 .extract::<PyBytes>(py)?
192 .extract::<PyBytes>(py)?
198 .data(py),
193 .data(py),
199 &b":"[..],
194 &b":"[..],
200 ]
195 ]
201 .concat(),
196 .concat(),
202 )
197 )
203 .map_err(|e| {
198 .map_err(|e| {
204 handle_fallback(py, StatusError::Pattern(e))
199 handle_fallback(py, StatusError::Pattern(e))
205 })?;
200 })?;
206 let pattern = k.get_item(py, 1)?.extract::<PyBytes>(py)?;
201 let pattern = k.get_item(py, 1)?.extract::<PyBytes>(py)?;
207 let pattern = pattern.data(py);
202 let pattern = pattern.data(py);
208 let source = k.get_item(py, 2)?.extract::<PyBytes>(py)?;
203 let source = k.get_item(py, 2)?.extract::<PyBytes>(py)?;
209 let source = get_path_from_bytes(source.data(py));
204 let source = get_path_from_bytes(source.data(py));
210 let new = IgnorePattern::new(syntax, pattern, source);
205 let new = IgnorePattern::new(syntax, pattern, source);
211 Ok(new)
206 Ok(new)
212 })
207 })
213 .collect();
208 .collect();
214
209
215 let ignore_patterns = ignore_patterns?;
210 let ignore_patterns = ignore_patterns?;
216
211
217 let matcher = IncludeMatcher::new(ignore_patterns)
212 let matcher = IncludeMatcher::new(ignore_patterns)
218 .map_err(|e| handle_fallback(py, e.into()))?;
213 .map_err(|e| handle_fallback(py, e.into()))?;
219
214
220 let (status_res, warnings) = dmap
215 let (status_res, warnings) = dmap
221 .status(
216 .status(
222 &matcher,
217 &matcher,
223 root_dir.to_path_buf(),
218 root_dir.to_path_buf(),
224 ignore_files,
219 ignore_files,
225 StatusOptions {
220 StatusOptions {
226 check_exec,
221 check_exec,
227 last_normal_time,
228 list_clean,
222 list_clean,
229 list_ignored,
223 list_ignored,
230 list_unknown,
224 list_unknown,
231 collect_traversed_dirs,
225 collect_traversed_dirs,
232 },
226 },
233 )
227 )
234 .map_err(|e| handle_fallback(py, e))?;
228 .map_err(|e| handle_fallback(py, e))?;
235
229
236 build_response(py, status_res, warnings)
230 build_response(py, status_res, warnings)
237 }
231 }
238 e => Err(PyErr::new::<ValueError, _>(
232 e => Err(PyErr::new::<ValueError, _>(
239 py,
233 py,
240 format!("Unsupported matcher {}", e),
234 format!("Unsupported matcher {}", e),
241 )),
235 )),
242 }
236 }
243 }
237 }
244
238
245 fn build_response(
239 fn build_response(
246 py: Python,
240 py: Python,
247 status_res: DirstateStatus,
241 status_res: DirstateStatus,
248 warnings: Vec<PatternFileWarning>,
242 warnings: Vec<PatternFileWarning>,
249 ) -> PyResult<PyTuple> {
243 ) -> PyResult<PyTuple> {
250 let modified = collect_pybytes_list(py, status_res.modified.as_ref());
244 let modified = collect_pybytes_list(py, status_res.modified.as_ref());
251 let added = collect_pybytes_list(py, status_res.added.as_ref());
245 let added = collect_pybytes_list(py, status_res.added.as_ref());
252 let removed = collect_pybytes_list(py, status_res.removed.as_ref());
246 let removed = collect_pybytes_list(py, status_res.removed.as_ref());
253 let deleted = collect_pybytes_list(py, status_res.deleted.as_ref());
247 let deleted = collect_pybytes_list(py, status_res.deleted.as_ref());
254 let clean = collect_pybytes_list(py, status_res.clean.as_ref());
248 let clean = collect_pybytes_list(py, status_res.clean.as_ref());
255 let ignored = collect_pybytes_list(py, status_res.ignored.as_ref());
249 let ignored = collect_pybytes_list(py, status_res.ignored.as_ref());
256 let unknown = collect_pybytes_list(py, status_res.unknown.as_ref());
250 let unknown = collect_pybytes_list(py, status_res.unknown.as_ref());
257 let unsure = collect_pybytes_list(py, status_res.unsure.as_ref());
251 let unsure = collect_pybytes_list(py, status_res.unsure.as_ref());
258 let bad = collect_bad_matches(py, status_res.bad.as_ref())?;
252 let bad = collect_bad_matches(py, status_res.bad.as_ref())?;
259 let traversed = collect_pybytes_list(py, status_res.traversed.as_ref());
253 let traversed = collect_pybytes_list(py, status_res.traversed.as_ref());
260 let dirty = status_res.dirty.to_py_object(py);
254 let dirty = status_res.dirty.to_py_object(py);
261 let py_warnings = PyList::new(py, &[]);
255 let py_warnings = PyList::new(py, &[]);
262 for warning in warnings.iter() {
256 for warning in warnings.iter() {
263 // We use duck-typing on the Python side for dispatch, good enough for
257 // We use duck-typing on the Python side for dispatch, good enough for
264 // now.
258 // now.
265 match warning {
259 match warning {
266 PatternFileWarning::InvalidSyntax(file, syn) => {
260 PatternFileWarning::InvalidSyntax(file, syn) => {
267 py_warnings.append(
261 py_warnings.append(
268 py,
262 py,
269 (
263 (
270 PyBytes::new(py, &get_bytes_from_path(&file)),
264 PyBytes::new(py, &get_bytes_from_path(&file)),
271 PyBytes::new(py, syn),
265 PyBytes::new(py, syn),
272 )
266 )
273 .to_py_object(py)
267 .to_py_object(py)
274 .into_object(),
268 .into_object(),
275 );
269 );
276 }
270 }
277 PatternFileWarning::NoSuchFile(file) => py_warnings.append(
271 PatternFileWarning::NoSuchFile(file) => py_warnings.append(
278 py,
272 py,
279 PyBytes::new(py, &get_bytes_from_path(&file)).into_object(),
273 PyBytes::new(py, &get_bytes_from_path(&file)).into_object(),
280 ),
274 ),
281 }
275 }
282 }
276 }
283
277
284 Ok(PyTuple::new(
278 Ok(PyTuple::new(
285 py,
279 py,
286 &[
280 &[
287 unsure.into_object(),
281 unsure.into_object(),
288 modified.into_object(),
282 modified.into_object(),
289 added.into_object(),
283 added.into_object(),
290 removed.into_object(),
284 removed.into_object(),
291 deleted.into_object(),
285 deleted.into_object(),
292 clean.into_object(),
286 clean.into_object(),
293 ignored.into_object(),
287 ignored.into_object(),
294 unknown.into_object(),
288 unknown.into_object(),
295 py_warnings.into_object(),
289 py_warnings.into_object(),
296 bad.into_object(),
290 bad.into_object(),
297 traversed.into_object(),
291 traversed.into_object(),
298 dirty.into_object(),
292 dirty.into_object(),
299 ][..],
293 ][..],
300 ))
294 ))
301 }
295 }
@@ -1,400 +1,396 b''
1 // status.rs
1 // status.rs
2 //
2 //
3 // Copyright 2020, Georges Racinet <georges.racinets@octobus.net>
3 // Copyright 2020, Georges Racinet <georges.racinets@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 use crate::error::CommandError;
8 use crate::error::CommandError;
9 use crate::ui::Ui;
9 use crate::ui::Ui;
10 use crate::utils::path_utils::relativize_paths;
10 use crate::utils::path_utils::relativize_paths;
11 use clap::{Arg, SubCommand};
11 use clap::{Arg, SubCommand};
12 use format_bytes::format_bytes;
12 use format_bytes::format_bytes;
13 use hg;
13 use hg;
14 use hg::config::Config;
14 use hg::config::Config;
15 use hg::dirstate::{has_exec_bit, TruncatedTimestamp};
15 use hg::dirstate::has_exec_bit;
16 use hg::errors::HgError;
16 use hg::errors::HgError;
17 use hg::manifest::Manifest;
17 use hg::manifest::Manifest;
18 use hg::matchers::AlwaysMatcher;
18 use hg::matchers::AlwaysMatcher;
19 use hg::repo::Repo;
19 use hg::repo::Repo;
20 use hg::utils::files::get_bytes_from_os_string;
20 use hg::utils::files::get_bytes_from_os_string;
21 use hg::utils::hg_path::{hg_path_to_os_string, HgPath};
21 use hg::utils::hg_path::{hg_path_to_os_string, HgPath};
22 use hg::{HgPathCow, StatusOptions};
22 use hg::{HgPathCow, StatusOptions};
23 use log::{info, warn};
23 use log::{info, warn};
24
24
25 pub const HELP_TEXT: &str = "
25 pub const HELP_TEXT: &str = "
26 Show changed files in the working directory
26 Show changed files in the working directory
27
27
28 This is a pure Rust version of `hg status`.
28 This is a pure Rust version of `hg status`.
29
29
30 Some options might be missing, check the list below.
30 Some options might be missing, check the list below.
31 ";
31 ";
32
32
33 pub fn args() -> clap::App<'static, 'static> {
33 pub fn args() -> clap::App<'static, 'static> {
34 SubCommand::with_name("status")
34 SubCommand::with_name("status")
35 .alias("st")
35 .alias("st")
36 .about(HELP_TEXT)
36 .about(HELP_TEXT)
37 .arg(
37 .arg(
38 Arg::with_name("all")
38 Arg::with_name("all")
39 .help("show status of all files")
39 .help("show status of all files")
40 .short("-A")
40 .short("-A")
41 .long("--all"),
41 .long("--all"),
42 )
42 )
43 .arg(
43 .arg(
44 Arg::with_name("modified")
44 Arg::with_name("modified")
45 .help("show only modified files")
45 .help("show only modified files")
46 .short("-m")
46 .short("-m")
47 .long("--modified"),
47 .long("--modified"),
48 )
48 )
49 .arg(
49 .arg(
50 Arg::with_name("added")
50 Arg::with_name("added")
51 .help("show only added files")
51 .help("show only added files")
52 .short("-a")
52 .short("-a")
53 .long("--added"),
53 .long("--added"),
54 )
54 )
55 .arg(
55 .arg(
56 Arg::with_name("removed")
56 Arg::with_name("removed")
57 .help("show only removed files")
57 .help("show only removed files")
58 .short("-r")
58 .short("-r")
59 .long("--removed"),
59 .long("--removed"),
60 )
60 )
61 .arg(
61 .arg(
62 Arg::with_name("clean")
62 Arg::with_name("clean")
63 .help("show only clean files")
63 .help("show only clean files")
64 .short("-c")
64 .short("-c")
65 .long("--clean"),
65 .long("--clean"),
66 )
66 )
67 .arg(
67 .arg(
68 Arg::with_name("deleted")
68 Arg::with_name("deleted")
69 .help("show only deleted files")
69 .help("show only deleted files")
70 .short("-d")
70 .short("-d")
71 .long("--deleted"),
71 .long("--deleted"),
72 )
72 )
73 .arg(
73 .arg(
74 Arg::with_name("unknown")
74 Arg::with_name("unknown")
75 .help("show only unknown (not tracked) files")
75 .help("show only unknown (not tracked) files")
76 .short("-u")
76 .short("-u")
77 .long("--unknown"),
77 .long("--unknown"),
78 )
78 )
79 .arg(
79 .arg(
80 Arg::with_name("ignored")
80 Arg::with_name("ignored")
81 .help("show only ignored files")
81 .help("show only ignored files")
82 .short("-i")
82 .short("-i")
83 .long("--ignored"),
83 .long("--ignored"),
84 )
84 )
85 .arg(
85 .arg(
86 Arg::with_name("no-status")
86 Arg::with_name("no-status")
87 .help("hide status prefix")
87 .help("hide status prefix")
88 .short("-n")
88 .short("-n")
89 .long("--no-status"),
89 .long("--no-status"),
90 )
90 )
91 }
91 }
92
92
93 /// Pure data type allowing the caller to specify file states to display
93 /// Pure data type allowing the caller to specify file states to display
94 #[derive(Copy, Clone, Debug)]
94 #[derive(Copy, Clone, Debug)]
95 pub struct DisplayStates {
95 pub struct DisplayStates {
96 pub modified: bool,
96 pub modified: bool,
97 pub added: bool,
97 pub added: bool,
98 pub removed: bool,
98 pub removed: bool,
99 pub clean: bool,
99 pub clean: bool,
100 pub deleted: bool,
100 pub deleted: bool,
101 pub unknown: bool,
101 pub unknown: bool,
102 pub ignored: bool,
102 pub ignored: bool,
103 }
103 }
104
104
105 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
105 pub const DEFAULT_DISPLAY_STATES: DisplayStates = DisplayStates {
106 modified: true,
106 modified: true,
107 added: true,
107 added: true,
108 removed: true,
108 removed: true,
109 clean: false,
109 clean: false,
110 deleted: true,
110 deleted: true,
111 unknown: true,
111 unknown: true,
112 ignored: false,
112 ignored: false,
113 };
113 };
114
114
115 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
115 pub const ALL_DISPLAY_STATES: DisplayStates = DisplayStates {
116 modified: true,
116 modified: true,
117 added: true,
117 added: true,
118 removed: true,
118 removed: true,
119 clean: true,
119 clean: true,
120 deleted: true,
120 deleted: true,
121 unknown: true,
121 unknown: true,
122 ignored: true,
122 ignored: true,
123 };
123 };
124
124
125 impl DisplayStates {
125 impl DisplayStates {
126 pub fn is_empty(&self) -> bool {
126 pub fn is_empty(&self) -> bool {
127 !(self.modified
127 !(self.modified
128 || self.added
128 || self.added
129 || self.removed
129 || self.removed
130 || self.clean
130 || self.clean
131 || self.deleted
131 || self.deleted
132 || self.unknown
132 || self.unknown
133 || self.ignored)
133 || self.ignored)
134 }
134 }
135 }
135 }
136
136
137 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
137 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
138 let status_enabled_default = false;
138 let status_enabled_default = false;
139 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
139 let status_enabled = invocation.config.get_option(b"rhg", b"status")?;
140 if !status_enabled.unwrap_or(status_enabled_default) {
140 if !status_enabled.unwrap_or(status_enabled_default) {
141 return Err(CommandError::unsupported(
141 return Err(CommandError::unsupported(
142 "status is experimental in rhg (enable it with 'rhg.status = true' \
142 "status is experimental in rhg (enable it with 'rhg.status = true' \
143 or enable fallback with 'rhg.on-unsupported = fallback')"
143 or enable fallback with 'rhg.on-unsupported = fallback')"
144 ));
144 ));
145 }
145 }
146
146
147 // TODO: lift these limitations
147 // TODO: lift these limitations
148 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
148 if invocation.config.get_bool(b"ui", b"tweakdefaults")? {
149 return Err(CommandError::unsupported(
149 return Err(CommandError::unsupported(
150 "ui.tweakdefaults is not yet supported with rhg status",
150 "ui.tweakdefaults is not yet supported with rhg status",
151 ));
151 ));
152 }
152 }
153 if invocation.config.get_bool(b"ui", b"statuscopies")? {
153 if invocation.config.get_bool(b"ui", b"statuscopies")? {
154 return Err(CommandError::unsupported(
154 return Err(CommandError::unsupported(
155 "ui.statuscopies is not yet supported with rhg status",
155 "ui.statuscopies is not yet supported with rhg status",
156 ));
156 ));
157 }
157 }
158 if invocation
158 if invocation
159 .config
159 .config
160 .get(b"commands", b"status.terse")
160 .get(b"commands", b"status.terse")
161 .is_some()
161 .is_some()
162 {
162 {
163 return Err(CommandError::unsupported(
163 return Err(CommandError::unsupported(
164 "status.terse is not yet supported with rhg status",
164 "status.terse is not yet supported with rhg status",
165 ));
165 ));
166 }
166 }
167
167
168 let ui = invocation.ui;
168 let ui = invocation.ui;
169 let config = invocation.config;
169 let config = invocation.config;
170 let args = invocation.subcommand_args;
170 let args = invocation.subcommand_args;
171 let display_states = if args.is_present("all") {
171 let display_states = if args.is_present("all") {
172 // TODO when implementing `--quiet`: it excludes clean files
172 // TODO when implementing `--quiet`: it excludes clean files
173 // from `--all`
173 // from `--all`
174 ALL_DISPLAY_STATES
174 ALL_DISPLAY_STATES
175 } else {
175 } else {
176 let requested = DisplayStates {
176 let requested = DisplayStates {
177 modified: args.is_present("modified"),
177 modified: args.is_present("modified"),
178 added: args.is_present("added"),
178 added: args.is_present("added"),
179 removed: args.is_present("removed"),
179 removed: args.is_present("removed"),
180 clean: args.is_present("clean"),
180 clean: args.is_present("clean"),
181 deleted: args.is_present("deleted"),
181 deleted: args.is_present("deleted"),
182 unknown: args.is_present("unknown"),
182 unknown: args.is_present("unknown"),
183 ignored: args.is_present("ignored"),
183 ignored: args.is_present("ignored"),
184 };
184 };
185 if requested.is_empty() {
185 if requested.is_empty() {
186 DEFAULT_DISPLAY_STATES
186 DEFAULT_DISPLAY_STATES
187 } else {
187 } else {
188 requested
188 requested
189 }
189 }
190 };
190 };
191 let no_status = args.is_present("no-status");
191 let no_status = args.is_present("no-status");
192
192
193 let repo = invocation.repo?;
193 let repo = invocation.repo?;
194 let mut dmap = repo.dirstate_map_mut()?;
194 let mut dmap = repo.dirstate_map_mut()?;
195
195
196 let options = StatusOptions {
196 let options = StatusOptions {
197 // TODO should be provided by the dirstate parsing and
198 // hence be stored on dmap. Using a value that assumes we aren't
199 // below the time resolution granularity of the FS and the
200 // dirstate.
201 last_normal_time: TruncatedTimestamp::new_truncate(0, 0),
202 // we're currently supporting file systems with exec flags only
197 // we're currently supporting file systems with exec flags only
203 // anyway
198 // anyway
204 check_exec: true,
199 check_exec: true,
205 list_clean: display_states.clean,
200 list_clean: display_states.clean,
206 list_unknown: display_states.unknown,
201 list_unknown: display_states.unknown,
207 list_ignored: display_states.ignored,
202 list_ignored: display_states.ignored,
208 collect_traversed_dirs: false,
203 collect_traversed_dirs: false,
209 };
204 };
210 let ignore_file = repo.working_directory_vfs().join(".hgignore"); // TODO hardcoded
205 let ignore_file = repo.working_directory_vfs().join(".hgignore"); // TODO hardcoded
211 let (mut ds_status, pattern_warnings) = dmap.status(
206 let (mut ds_status, pattern_warnings) = dmap.status(
212 &AlwaysMatcher,
207 &AlwaysMatcher,
213 repo.working_directory_path().to_owned(),
208 repo.working_directory_path().to_owned(),
214 vec![ignore_file],
209 vec![ignore_file],
215 options,
210 options,
216 )?;
211 )?;
217 if !pattern_warnings.is_empty() {
212 if !pattern_warnings.is_empty() {
218 warn!("Pattern warnings: {:?}", &pattern_warnings);
213 warn!("Pattern warnings: {:?}", &pattern_warnings);
219 }
214 }
220
215
221 if !ds_status.bad.is_empty() {
216 if !ds_status.bad.is_empty() {
222 warn!("Bad matches {:?}", &(ds_status.bad))
217 warn!("Bad matches {:?}", &(ds_status.bad))
223 }
218 }
224 if !ds_status.unsure.is_empty() {
219 if !ds_status.unsure.is_empty() {
225 info!(
220 info!(
226 "Files to be rechecked by retrieval from filelog: {:?}",
221 "Files to be rechecked by retrieval from filelog: {:?}",
227 &ds_status.unsure
222 &ds_status.unsure
228 );
223 );
229 }
224 }
230 if !ds_status.unsure.is_empty()
225 if !ds_status.unsure.is_empty()
231 && (display_states.modified || display_states.clean)
226 && (display_states.modified || display_states.clean)
232 {
227 {
233 let p1 = repo.dirstate_parents()?.p1;
228 let p1 = repo.dirstate_parents()?.p1;
234 let manifest = repo.manifest_for_node(p1).map_err(|e| {
229 let manifest = repo.manifest_for_node(p1).map_err(|e| {
235 CommandError::from((e, &*format!("{:x}", p1.short())))
230 CommandError::from((e, &*format!("{:x}", p1.short())))
236 })?;
231 })?;
237 for to_check in ds_status.unsure {
232 for to_check in ds_status.unsure {
238 if unsure_is_modified(repo, &manifest, &to_check)? {
233 if unsure_is_modified(repo, &manifest, &to_check)? {
239 if display_states.modified {
234 if display_states.modified {
240 ds_status.modified.push(to_check);
235 ds_status.modified.push(to_check);
241 }
236 }
242 } else {
237 } else {
243 if display_states.clean {
238 if display_states.clean {
244 ds_status.clean.push(to_check);
239 ds_status.clean.push(to_check);
245 }
240 }
246 }
241 }
247 }
242 }
248 }
243 }
249 if display_states.modified {
244 if display_states.modified {
250 display_status_paths(
245 display_status_paths(
251 ui,
246 ui,
252 repo,
247 repo,
253 config,
248 config,
254 no_status,
249 no_status,
255 &mut ds_status.modified,
250 &mut ds_status.modified,
256 b"M",
251 b"M",
257 )?;
252 )?;
258 }
253 }
259 if display_states.added {
254 if display_states.added {
260 display_status_paths(
255 display_status_paths(
261 ui,
256 ui,
262 repo,
257 repo,
263 config,
258 config,
264 no_status,
259 no_status,
265 &mut ds_status.added,
260 &mut ds_status.added,
266 b"A",
261 b"A",
267 )?;
262 )?;
268 }
263 }
269 if display_states.removed {
264 if display_states.removed {
270 display_status_paths(
265 display_status_paths(
271 ui,
266 ui,
272 repo,
267 repo,
273 config,
268 config,
274 no_status,
269 no_status,
275 &mut ds_status.removed,
270 &mut ds_status.removed,
276 b"R",
271 b"R",
277 )?;
272 )?;
278 }
273 }
279 if display_states.deleted {
274 if display_states.deleted {
280 display_status_paths(
275 display_status_paths(
281 ui,
276 ui,
282 repo,
277 repo,
283 config,
278 config,
284 no_status,
279 no_status,
285 &mut ds_status.deleted,
280 &mut ds_status.deleted,
286 b"!",
281 b"!",
287 )?;
282 )?;
288 }
283 }
289 if display_states.unknown {
284 if display_states.unknown {
290 display_status_paths(
285 display_status_paths(
291 ui,
286 ui,
292 repo,
287 repo,
293 config,
288 config,
294 no_status,
289 no_status,
295 &mut ds_status.unknown,
290 &mut ds_status.unknown,
296 b"?",
291 b"?",
297 )?;
292 )?;
298 }
293 }
299 if display_states.ignored {
294 if display_states.ignored {
300 display_status_paths(
295 display_status_paths(
301 ui,
296 ui,
302 repo,
297 repo,
303 config,
298 config,
304 no_status,
299 no_status,
305 &mut ds_status.ignored,
300 &mut ds_status.ignored,
306 b"I",
301 b"I",
307 )?;
302 )?;
308 }
303 }
309 if display_states.clean {
304 if display_states.clean {
310 display_status_paths(
305 display_status_paths(
311 ui,
306 ui,
312 repo,
307 repo,
313 config,
308 config,
314 no_status,
309 no_status,
315 &mut ds_status.clean,
310 &mut ds_status.clean,
316 b"C",
311 b"C",
317 )?;
312 )?;
318 }
313 }
319 Ok(())
314 Ok(())
320 }
315 }
321
316
322 // Probably more elegant to use a Deref or Borrow trait rather than
317 // Probably more elegant to use a Deref or Borrow trait rather than
323 // harcode HgPathBuf, but probably not really useful at this point
318 // harcode HgPathBuf, but probably not really useful at this point
324 fn display_status_paths(
319 fn display_status_paths(
325 ui: &Ui,
320 ui: &Ui,
326 repo: &Repo,
321 repo: &Repo,
327 config: &Config,
322 config: &Config,
328 no_status: bool,
323 no_status: bool,
329 paths: &mut [HgPathCow],
324 paths: &mut [HgPathCow],
330 status_prefix: &[u8],
325 status_prefix: &[u8],
331 ) -> Result<(), CommandError> {
326 ) -> Result<(), CommandError> {
332 paths.sort_unstable();
327 paths.sort_unstable();
333 let mut relative: bool = config.get_bool(b"ui", b"relative-paths")?;
328 let mut relative: bool = config.get_bool(b"ui", b"relative-paths")?;
334 relative = config
329 relative = config
335 .get_option(b"commands", b"status.relative")?
330 .get_option(b"commands", b"status.relative")?
336 .unwrap_or(relative);
331 .unwrap_or(relative);
337 let print_path = |path: &[u8]| {
332 let print_path = |path: &[u8]| {
338 // TODO optim, probably lots of unneeded copies here, especially
333 // TODO optim, probably lots of unneeded copies here, especially
339 // if out stream is buffered
334 // if out stream is buffered
340 if no_status {
335 if no_status {
341 ui.write_stdout(&format_bytes!(b"{}\n", path))
336 ui.write_stdout(&format_bytes!(b"{}\n", path))
342 } else {
337 } else {
343 ui.write_stdout(&format_bytes!(b"{} {}\n", status_prefix, path))
338 ui.write_stdout(&format_bytes!(b"{} {}\n", status_prefix, path))
344 }
339 }
345 };
340 };
346
341
347 if relative && !ui.plain() {
342 if relative && !ui.plain() {
348 relativize_paths(repo, paths.iter().map(Ok), |path| {
343 relativize_paths(repo, paths.iter().map(Ok), |path| {
349 print_path(&path)
344 print_path(&path)
350 })?;
345 })?;
351 } else {
346 } else {
352 for path in paths {
347 for path in paths {
353 print_path(path.as_bytes())?
348 print_path(path.as_bytes())?
354 }
349 }
355 }
350 }
356 Ok(())
351 Ok(())
357 }
352 }
358
353
359 /// Check if a file is modified by comparing actual repo store and file system.
354 /// Check if a file is modified by comparing actual repo store and file system.
360 ///
355 ///
361 /// This meant to be used for those that the dirstate cannot resolve, due
356 /// This meant to be used for those that the dirstate cannot resolve, due
362 /// to time resolution limits.
357 /// to time resolution limits.
363 fn unsure_is_modified(
358 fn unsure_is_modified(
364 repo: &Repo,
359 repo: &Repo,
365 manifest: &Manifest,
360 manifest: &Manifest,
366 hg_path: &HgPath,
361 hg_path: &HgPath,
367 ) -> Result<bool, HgError> {
362 ) -> Result<bool, HgError> {
368 let vfs = repo.working_directory_vfs();
363 let vfs = repo.working_directory_vfs();
369 let fs_path = hg_path_to_os_string(hg_path).expect("HgPath conversion");
364 let fs_path = hg_path_to_os_string(hg_path).expect("HgPath conversion");
370 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
365 let fs_metadata = vfs.symlink_metadata(&fs_path)?;
371 let is_symlink = fs_metadata.file_type().is_symlink();
366 let is_symlink = fs_metadata.file_type().is_symlink();
372 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the dirstate
367 // TODO: Also account for `FALLBACK_SYMLINK` and `FALLBACK_EXEC` from the
368 // dirstate
373 let fs_flags = if is_symlink {
369 let fs_flags = if is_symlink {
374 Some(b'l')
370 Some(b'l')
375 } else if has_exec_bit(&fs_metadata) {
371 } else if has_exec_bit(&fs_metadata) {
376 Some(b'x')
372 Some(b'x')
377 } else {
373 } else {
378 None
374 None
379 };
375 };
380
376
381 let entry = manifest
377 let entry = manifest
382 .find_file(hg_path)?
378 .find_file(hg_path)?
383 .expect("ambgious file not in p1");
379 .expect("ambgious file not in p1");
384 if entry.flags != fs_flags {
380 if entry.flags != fs_flags {
385 return Ok(true);
381 return Ok(true);
386 }
382 }
387 let filelog = repo.filelog(hg_path)?;
383 let filelog = repo.filelog(hg_path)?;
388 let filelog_entry =
384 let filelog_entry =
389 filelog.data_for_node(entry.node_id()?).map_err(|_| {
385 filelog.data_for_node(entry.node_id()?).map_err(|_| {
390 HgError::corrupted("filelog missing node from manifest")
386 HgError::corrupted("filelog missing node from manifest")
391 })?;
387 })?;
392 let contents_in_p1 = filelog_entry.data()?;
388 let contents_in_p1 = filelog_entry.data()?;
393
389
394 let fs_contents = if is_symlink {
390 let fs_contents = if is_symlink {
395 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
391 get_bytes_from_os_string(vfs.read_link(fs_path)?.into_os_string())
396 } else {
392 } else {
397 vfs.read(fs_path)?
393 vfs.read(fs_path)?
398 };
394 };
399 return Ok(contents_in_p1 != &*fs_contents);
395 return Ok(contents_in_p1 != &*fs_contents);
400 }
396 }
General Comments 0
You need to be logged in to leave comments. Login now