##// END OF EJS Templates
rust-dirstatemap: don't read the dirstate when requesting parents...
Raphaël Gomès -
r45358:18e36ff8 stable
parent child Browse files
Show More
@@ -1,1901 +1,1915 b''
1 # dirstate.py - working directory tracking for mercurial
1 # dirstate.py - working directory tracking for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@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 .node import nullid
17 from .node import nullid
18 from .pycompat import delattr
18 from .pycompat import delattr
19
19
20 from hgdemandimport import tracing
20 from hgdemandimport import tracing
21
21
22 from . import (
22 from . import (
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 txnutil,
31 txnutil,
32 util,
32 util,
33 )
33 )
34
34
35 from .interfaces import (
35 from .interfaces import (
36 dirstate as intdirstate,
36 dirstate as intdirstate,
37 util as interfaceutil,
37 util as interfaceutil,
38 )
38 )
39
39
40 parsers = policy.importmod('parsers')
40 parsers = policy.importmod('parsers')
41 rustmod = policy.importrust('dirstate')
41 rustmod = policy.importrust('dirstate')
42
42
43 propertycache = util.propertycache
43 propertycache = util.propertycache
44 filecache = scmutil.filecache
44 filecache = scmutil.filecache
45 _rangemask = 0x7FFFFFFF
45 _rangemask = 0x7FFFFFFF
46
46
47 dirstatetuple = parsers.dirstatetuple
47 dirstatetuple = parsers.dirstatetuple
48
48
49
49
50 class repocache(filecache):
50 class repocache(filecache):
51 """filecache for files in .hg/"""
51 """filecache for files in .hg/"""
52
52
53 def join(self, obj, fname):
53 def join(self, obj, fname):
54 return obj._opener.join(fname)
54 return obj._opener.join(fname)
55
55
56
56
57 class rootcache(filecache):
57 class rootcache(filecache):
58 """filecache for files in the repository root"""
58 """filecache for files in the repository root"""
59
59
60 def join(self, obj, fname):
60 def join(self, obj, fname):
61 return obj._join(fname)
61 return obj._join(fname)
62
62
63
63
64 def _getfsnow(vfs):
64 def _getfsnow(vfs):
65 '''Get "now" timestamp on filesystem'''
65 '''Get "now" timestamp on filesystem'''
66 tmpfd, tmpname = vfs.mkstemp()
66 tmpfd, tmpname = vfs.mkstemp()
67 try:
67 try:
68 return os.fstat(tmpfd)[stat.ST_MTIME]
68 return os.fstat(tmpfd)[stat.ST_MTIME]
69 finally:
69 finally:
70 os.close(tmpfd)
70 os.close(tmpfd)
71 vfs.unlink(tmpname)
71 vfs.unlink(tmpname)
72
72
73
73
74 @interfaceutil.implementer(intdirstate.idirstate)
74 @interfaceutil.implementer(intdirstate.idirstate)
75 class dirstate(object):
75 class dirstate(object):
76 def __init__(self, opener, ui, root, validate, sparsematchfn):
76 def __init__(self, opener, ui, root, validate, sparsematchfn):
77 '''Create a new dirstate object.
77 '''Create a new dirstate object.
78
78
79 opener is an open()-like callable that can be used to open the
79 opener is an open()-like callable that can be used to open the
80 dirstate file; root is the root of the directory tracked by
80 dirstate file; root is the root of the directory tracked by
81 the dirstate.
81 the dirstate.
82 '''
82 '''
83 self._opener = opener
83 self._opener = opener
84 self._validate = validate
84 self._validate = validate
85 self._root = root
85 self._root = root
86 self._sparsematchfn = sparsematchfn
86 self._sparsematchfn = sparsematchfn
87 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
87 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
88 # UNC path pointing to root share (issue4557)
88 # UNC path pointing to root share (issue4557)
89 self._rootdir = pathutil.normasprefix(root)
89 self._rootdir = pathutil.normasprefix(root)
90 self._dirty = False
90 self._dirty = False
91 self._lastnormaltime = 0
91 self._lastnormaltime = 0
92 self._ui = ui
92 self._ui = ui
93 self._filecache = {}
93 self._filecache = {}
94 self._parentwriters = 0
94 self._parentwriters = 0
95 self._filename = b'dirstate'
95 self._filename = b'dirstate'
96 self._pendingfilename = b'%s.pending' % self._filename
96 self._pendingfilename = b'%s.pending' % self._filename
97 self._plchangecallbacks = {}
97 self._plchangecallbacks = {}
98 self._origpl = None
98 self._origpl = None
99 self._updatedfiles = set()
99 self._updatedfiles = set()
100 self._mapcls = dirstatemap
100 self._mapcls = dirstatemap
101 # Access and cache cwd early, so we don't access it for the first time
101 # Access and cache cwd early, so we don't access it for the first time
102 # after a working-copy update caused it to not exist (accessing it then
102 # after a working-copy update caused it to not exist (accessing it then
103 # raises an exception).
103 # raises an exception).
104 self._cwd
104 self._cwd
105
105
106 @contextlib.contextmanager
106 @contextlib.contextmanager
107 def parentchange(self):
107 def parentchange(self):
108 '''Context manager for handling dirstate parents.
108 '''Context manager for handling dirstate parents.
109
109
110 If an exception occurs in the scope of the context manager,
110 If an exception occurs in the scope of the context manager,
111 the incoherent dirstate won't be written when wlock is
111 the incoherent dirstate won't be written when wlock is
112 released.
112 released.
113 '''
113 '''
114 self._parentwriters += 1
114 self._parentwriters += 1
115 yield
115 yield
116 # Typically we want the "undo" step of a context manager in a
116 # Typically we want the "undo" step of a context manager in a
117 # finally block so it happens even when an exception
117 # finally block so it happens even when an exception
118 # occurs. In this case, however, we only want to decrement
118 # occurs. In this case, however, we only want to decrement
119 # parentwriters if the code in the with statement exits
119 # parentwriters if the code in the with statement exits
120 # normally, so we don't have a try/finally here on purpose.
120 # normally, so we don't have a try/finally here on purpose.
121 self._parentwriters -= 1
121 self._parentwriters -= 1
122
122
123 def pendingparentchange(self):
123 def pendingparentchange(self):
124 '''Returns true if the dirstate is in the middle of a set of changes
124 '''Returns true if the dirstate is in the middle of a set of changes
125 that modify the dirstate parent.
125 that modify the dirstate parent.
126 '''
126 '''
127 return self._parentwriters > 0
127 return self._parentwriters > 0
128
128
129 @propertycache
129 @propertycache
130 def _map(self):
130 def _map(self):
131 """Return the dirstate contents (see documentation for dirstatemap)."""
131 """Return the dirstate contents (see documentation for dirstatemap)."""
132 self._map = self._mapcls(self._ui, self._opener, self._root)
132 self._map = self._mapcls(self._ui, self._opener, self._root)
133 return self._map
133 return self._map
134
134
135 @property
135 @property
136 def _sparsematcher(self):
136 def _sparsematcher(self):
137 """The matcher for the sparse checkout.
137 """The matcher for the sparse checkout.
138
138
139 The working directory may not include every file from a manifest. The
139 The working directory may not include every file from a manifest. The
140 matcher obtained by this property will match a path if it is to be
140 matcher obtained by this property will match a path if it is to be
141 included in the working directory.
141 included in the working directory.
142 """
142 """
143 # TODO there is potential to cache this property. For now, the matcher
143 # TODO there is potential to cache this property. For now, the matcher
144 # is resolved on every access. (But the called function does use a
144 # is resolved on every access. (But the called function does use a
145 # cache to keep the lookup fast.)
145 # cache to keep the lookup fast.)
146 return self._sparsematchfn()
146 return self._sparsematchfn()
147
147
148 @repocache(b'branch')
148 @repocache(b'branch')
149 def _branch(self):
149 def _branch(self):
150 try:
150 try:
151 return self._opener.read(b"branch").strip() or b"default"
151 return self._opener.read(b"branch").strip() or b"default"
152 except IOError as inst:
152 except IOError as inst:
153 if inst.errno != errno.ENOENT:
153 if inst.errno != errno.ENOENT:
154 raise
154 raise
155 return b"default"
155 return b"default"
156
156
157 @property
157 @property
158 def _pl(self):
158 def _pl(self):
159 return self._map.parents()
159 return self._map.parents()
160
160
161 def hasdir(self, d):
161 def hasdir(self, d):
162 return self._map.hastrackeddir(d)
162 return self._map.hastrackeddir(d)
163
163
164 @rootcache(b'.hgignore')
164 @rootcache(b'.hgignore')
165 def _ignore(self):
165 def _ignore(self):
166 files = self._ignorefiles()
166 files = self._ignorefiles()
167 if not files:
167 if not files:
168 return matchmod.never()
168 return matchmod.never()
169
169
170 pats = [b'include:%s' % f for f in files]
170 pats = [b'include:%s' % f for f in files]
171 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
171 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
172
172
173 @propertycache
173 @propertycache
174 def _slash(self):
174 def _slash(self):
175 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
175 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
176
176
177 @propertycache
177 @propertycache
178 def _checklink(self):
178 def _checklink(self):
179 return util.checklink(self._root)
179 return util.checklink(self._root)
180
180
181 @propertycache
181 @propertycache
182 def _checkexec(self):
182 def _checkexec(self):
183 return util.checkexec(self._root)
183 return util.checkexec(self._root)
184
184
185 @propertycache
185 @propertycache
186 def _checkcase(self):
186 def _checkcase(self):
187 return not util.fscasesensitive(self._join(b'.hg'))
187 return not util.fscasesensitive(self._join(b'.hg'))
188
188
189 def _join(self, f):
189 def _join(self, f):
190 # much faster than os.path.join()
190 # much faster than os.path.join()
191 # it's safe because f is always a relative path
191 # it's safe because f is always a relative path
192 return self._rootdir + f
192 return self._rootdir + f
193
193
194 def flagfunc(self, buildfallback):
194 def flagfunc(self, buildfallback):
195 if self._checklink and self._checkexec:
195 if self._checklink and self._checkexec:
196
196
197 def f(x):
197 def f(x):
198 try:
198 try:
199 st = os.lstat(self._join(x))
199 st = os.lstat(self._join(x))
200 if util.statislink(st):
200 if util.statislink(st):
201 return b'l'
201 return b'l'
202 if util.statisexec(st):
202 if util.statisexec(st):
203 return b'x'
203 return b'x'
204 except OSError:
204 except OSError:
205 pass
205 pass
206 return b''
206 return b''
207
207
208 return f
208 return f
209
209
210 fallback = buildfallback()
210 fallback = buildfallback()
211 if self._checklink:
211 if self._checklink:
212
212
213 def f(x):
213 def f(x):
214 if os.path.islink(self._join(x)):
214 if os.path.islink(self._join(x)):
215 return b'l'
215 return b'l'
216 if b'x' in fallback(x):
216 if b'x' in fallback(x):
217 return b'x'
217 return b'x'
218 return b''
218 return b''
219
219
220 return f
220 return f
221 if self._checkexec:
221 if self._checkexec:
222
222
223 def f(x):
223 def f(x):
224 if b'l' in fallback(x):
224 if b'l' in fallback(x):
225 return b'l'
225 return b'l'
226 if util.isexec(self._join(x)):
226 if util.isexec(self._join(x)):
227 return b'x'
227 return b'x'
228 return b''
228 return b''
229
229
230 return f
230 return f
231 else:
231 else:
232 return fallback
232 return fallback
233
233
234 @propertycache
234 @propertycache
235 def _cwd(self):
235 def _cwd(self):
236 # internal config: ui.forcecwd
236 # internal config: ui.forcecwd
237 forcecwd = self._ui.config(b'ui', b'forcecwd')
237 forcecwd = self._ui.config(b'ui', b'forcecwd')
238 if forcecwd:
238 if forcecwd:
239 return forcecwd
239 return forcecwd
240 return encoding.getcwd()
240 return encoding.getcwd()
241
241
242 def getcwd(self):
242 def getcwd(self):
243 '''Return the path from which a canonical path is calculated.
243 '''Return the path from which a canonical path is calculated.
244
244
245 This path should be used to resolve file patterns or to convert
245 This path should be used to resolve file patterns or to convert
246 canonical paths back to file paths for display. It shouldn't be
246 canonical paths back to file paths for display. It shouldn't be
247 used to get real file paths. Use vfs functions instead.
247 used to get real file paths. Use vfs functions instead.
248 '''
248 '''
249 cwd = self._cwd
249 cwd = self._cwd
250 if cwd == self._root:
250 if cwd == self._root:
251 return b''
251 return b''
252 # self._root ends with a path separator if self._root is '/' or 'C:\'
252 # self._root ends with a path separator if self._root is '/' or 'C:\'
253 rootsep = self._root
253 rootsep = self._root
254 if not util.endswithsep(rootsep):
254 if not util.endswithsep(rootsep):
255 rootsep += pycompat.ossep
255 rootsep += pycompat.ossep
256 if cwd.startswith(rootsep):
256 if cwd.startswith(rootsep):
257 return cwd[len(rootsep) :]
257 return cwd[len(rootsep) :]
258 else:
258 else:
259 # we're outside the repo. return an absolute path.
259 # we're outside the repo. return an absolute path.
260 return cwd
260 return cwd
261
261
262 def pathto(self, f, cwd=None):
262 def pathto(self, f, cwd=None):
263 if cwd is None:
263 if cwd is None:
264 cwd = self.getcwd()
264 cwd = self.getcwd()
265 path = util.pathto(self._root, cwd, f)
265 path = util.pathto(self._root, cwd, f)
266 if self._slash:
266 if self._slash:
267 return util.pconvert(path)
267 return util.pconvert(path)
268 return path
268 return path
269
269
270 def __getitem__(self, key):
270 def __getitem__(self, key):
271 '''Return the current state of key (a filename) in the dirstate.
271 '''Return the current state of key (a filename) in the dirstate.
272
272
273 States are:
273 States are:
274 n normal
274 n normal
275 m needs merging
275 m needs merging
276 r marked for removal
276 r marked for removal
277 a marked for addition
277 a marked for addition
278 ? not tracked
278 ? not tracked
279 '''
279 '''
280 return self._map.get(key, (b"?",))[0]
280 return self._map.get(key, (b"?",))[0]
281
281
282 def __contains__(self, key):
282 def __contains__(self, key):
283 return key in self._map
283 return key in self._map
284
284
285 def __iter__(self):
285 def __iter__(self):
286 return iter(sorted(self._map))
286 return iter(sorted(self._map))
287
287
288 def items(self):
288 def items(self):
289 return pycompat.iteritems(self._map)
289 return pycompat.iteritems(self._map)
290
290
291 iteritems = items
291 iteritems = items
292
292
293 def parents(self):
293 def parents(self):
294 return [self._validate(p) for p in self._pl]
294 return [self._validate(p) for p in self._pl]
295
295
296 def p1(self):
296 def p1(self):
297 return self._validate(self._pl[0])
297 return self._validate(self._pl[0])
298
298
299 def p2(self):
299 def p2(self):
300 return self._validate(self._pl[1])
300 return self._validate(self._pl[1])
301
301
302 def branch(self):
302 def branch(self):
303 return encoding.tolocal(self._branch)
303 return encoding.tolocal(self._branch)
304
304
305 def setparents(self, p1, p2=nullid):
305 def setparents(self, p1, p2=nullid):
306 """Set dirstate parents to p1 and p2.
306 """Set dirstate parents to p1 and p2.
307
307
308 When moving from two parents to one, 'm' merged entries a
308 When moving from two parents to one, 'm' merged entries a
309 adjusted to normal and previous copy records discarded and
309 adjusted to normal and previous copy records discarded and
310 returned by the call.
310 returned by the call.
311
311
312 See localrepo.setparents()
312 See localrepo.setparents()
313 """
313 """
314 if self._parentwriters == 0:
314 if self._parentwriters == 0:
315 raise ValueError(
315 raise ValueError(
316 b"cannot set dirstate parent outside of "
316 b"cannot set dirstate parent outside of "
317 b"dirstate.parentchange context manager"
317 b"dirstate.parentchange context manager"
318 )
318 )
319
319
320 self._dirty = True
320 self._dirty = True
321 oldp2 = self._pl[1]
321 oldp2 = self._pl[1]
322 if self._origpl is None:
322 if self._origpl is None:
323 self._origpl = self._pl
323 self._origpl = self._pl
324 self._map.setparents(p1, p2)
324 self._map.setparents(p1, p2)
325 copies = {}
325 copies = {}
326 if oldp2 != nullid and p2 == nullid:
326 if oldp2 != nullid and p2 == nullid:
327 candidatefiles = self._map.nonnormalset.union(
327 candidatefiles = self._map.nonnormalset.union(
328 self._map.otherparentset
328 self._map.otherparentset
329 )
329 )
330 for f in candidatefiles:
330 for f in candidatefiles:
331 s = self._map.get(f)
331 s = self._map.get(f)
332 if s is None:
332 if s is None:
333 continue
333 continue
334
334
335 # Discard 'm' markers when moving away from a merge state
335 # Discard 'm' markers when moving away from a merge state
336 if s[0] == b'm':
336 if s[0] == b'm':
337 source = self._map.copymap.get(f)
337 source = self._map.copymap.get(f)
338 if source:
338 if source:
339 copies[f] = source
339 copies[f] = source
340 self.normallookup(f)
340 self.normallookup(f)
341 # Also fix up otherparent markers
341 # Also fix up otherparent markers
342 elif s[0] == b'n' and s[2] == -2:
342 elif s[0] == b'n' and s[2] == -2:
343 source = self._map.copymap.get(f)
343 source = self._map.copymap.get(f)
344 if source:
344 if source:
345 copies[f] = source
345 copies[f] = source
346 self.add(f)
346 self.add(f)
347 return copies
347 return copies
348
348
349 def setbranch(self, branch):
349 def setbranch(self, branch):
350 self.__class__._branch.set(self, encoding.fromlocal(branch))
350 self.__class__._branch.set(self, encoding.fromlocal(branch))
351 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
351 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
352 try:
352 try:
353 f.write(self._branch + b'\n')
353 f.write(self._branch + b'\n')
354 f.close()
354 f.close()
355
355
356 # make sure filecache has the correct stat info for _branch after
356 # make sure filecache has the correct stat info for _branch after
357 # replacing the underlying file
357 # replacing the underlying file
358 ce = self._filecache[b'_branch']
358 ce = self._filecache[b'_branch']
359 if ce:
359 if ce:
360 ce.refresh()
360 ce.refresh()
361 except: # re-raises
361 except: # re-raises
362 f.discard()
362 f.discard()
363 raise
363 raise
364
364
365 def invalidate(self):
365 def invalidate(self):
366 '''Causes the next access to reread the dirstate.
366 '''Causes the next access to reread the dirstate.
367
367
368 This is different from localrepo.invalidatedirstate() because it always
368 This is different from localrepo.invalidatedirstate() because it always
369 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
369 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
370 check whether the dirstate has changed before rereading it.'''
370 check whether the dirstate has changed before rereading it.'''
371
371
372 for a in ("_map", "_branch", "_ignore"):
372 for a in ("_map", "_branch", "_ignore"):
373 if a in self.__dict__:
373 if a in self.__dict__:
374 delattr(self, a)
374 delattr(self, a)
375 self._lastnormaltime = 0
375 self._lastnormaltime = 0
376 self._dirty = False
376 self._dirty = False
377 self._updatedfiles.clear()
377 self._updatedfiles.clear()
378 self._parentwriters = 0
378 self._parentwriters = 0
379 self._origpl = None
379 self._origpl = None
380
380
381 def copy(self, source, dest):
381 def copy(self, source, dest):
382 """Mark dest as a copy of source. Unmark dest if source is None."""
382 """Mark dest as a copy of source. Unmark dest if source is None."""
383 if source == dest:
383 if source == dest:
384 return
384 return
385 self._dirty = True
385 self._dirty = True
386 if source is not None:
386 if source is not None:
387 self._map.copymap[dest] = source
387 self._map.copymap[dest] = source
388 self._updatedfiles.add(source)
388 self._updatedfiles.add(source)
389 self._updatedfiles.add(dest)
389 self._updatedfiles.add(dest)
390 elif self._map.copymap.pop(dest, None):
390 elif self._map.copymap.pop(dest, None):
391 self._updatedfiles.add(dest)
391 self._updatedfiles.add(dest)
392
392
393 def copied(self, file):
393 def copied(self, file):
394 return self._map.copymap.get(file, None)
394 return self._map.copymap.get(file, None)
395
395
396 def copies(self):
396 def copies(self):
397 return self._map.copymap
397 return self._map.copymap
398
398
399 def _addpath(self, f, state, mode, size, mtime):
399 def _addpath(self, f, state, mode, size, mtime):
400 oldstate = self[f]
400 oldstate = self[f]
401 if state == b'a' or oldstate == b'r':
401 if state == b'a' or oldstate == b'r':
402 scmutil.checkfilename(f)
402 scmutil.checkfilename(f)
403 if self._map.hastrackeddir(f):
403 if self._map.hastrackeddir(f):
404 raise error.Abort(
404 raise error.Abort(
405 _(b'directory %r already in dirstate') % pycompat.bytestr(f)
405 _(b'directory %r already in dirstate') % pycompat.bytestr(f)
406 )
406 )
407 # shadows
407 # shadows
408 for d in pathutil.finddirs(f):
408 for d in pathutil.finddirs(f):
409 if self._map.hastrackeddir(d):
409 if self._map.hastrackeddir(d):
410 break
410 break
411 entry = self._map.get(d)
411 entry = self._map.get(d)
412 if entry is not None and entry[0] != b'r':
412 if entry is not None and entry[0] != b'r':
413 raise error.Abort(
413 raise error.Abort(
414 _(b'file %r in dirstate clashes with %r')
414 _(b'file %r in dirstate clashes with %r')
415 % (pycompat.bytestr(d), pycompat.bytestr(f))
415 % (pycompat.bytestr(d), pycompat.bytestr(f))
416 )
416 )
417 self._dirty = True
417 self._dirty = True
418 self._updatedfiles.add(f)
418 self._updatedfiles.add(f)
419 self._map.addfile(f, oldstate, state, mode, size, mtime)
419 self._map.addfile(f, oldstate, state, mode, size, mtime)
420
420
421 def normal(self, f, parentfiledata=None):
421 def normal(self, f, parentfiledata=None):
422 '''Mark a file normal and clean.
422 '''Mark a file normal and clean.
423
423
424 parentfiledata: (mode, size, mtime) of the clean file
424 parentfiledata: (mode, size, mtime) of the clean file
425
425
426 parentfiledata should be computed from memory (for mode,
426 parentfiledata should be computed from memory (for mode,
427 size), as or close as possible from the point where we
427 size), as or close as possible from the point where we
428 determined the file was clean, to limit the risk of the
428 determined the file was clean, to limit the risk of the
429 file having been changed by an external process between the
429 file having been changed by an external process between the
430 moment where the file was determined to be clean and now.'''
430 moment where the file was determined to be clean and now.'''
431 if parentfiledata:
431 if parentfiledata:
432 (mode, size, mtime) = parentfiledata
432 (mode, size, mtime) = parentfiledata
433 else:
433 else:
434 s = os.lstat(self._join(f))
434 s = os.lstat(self._join(f))
435 mode = s.st_mode
435 mode = s.st_mode
436 size = s.st_size
436 size = s.st_size
437 mtime = s[stat.ST_MTIME]
437 mtime = s[stat.ST_MTIME]
438 self._addpath(f, b'n', mode, size & _rangemask, mtime & _rangemask)
438 self._addpath(f, b'n', mode, size & _rangemask, mtime & _rangemask)
439 self._map.copymap.pop(f, None)
439 self._map.copymap.pop(f, None)
440 if f in self._map.nonnormalset:
440 if f in self._map.nonnormalset:
441 self._map.nonnormalset.remove(f)
441 self._map.nonnormalset.remove(f)
442 if mtime > self._lastnormaltime:
442 if mtime > self._lastnormaltime:
443 # Remember the most recent modification timeslot for status(),
443 # Remember the most recent modification timeslot for status(),
444 # to make sure we won't miss future size-preserving file content
444 # to make sure we won't miss future size-preserving file content
445 # modifications that happen within the same timeslot.
445 # modifications that happen within the same timeslot.
446 self._lastnormaltime = mtime
446 self._lastnormaltime = mtime
447
447
448 def normallookup(self, f):
448 def normallookup(self, f):
449 '''Mark a file normal, but possibly dirty.'''
449 '''Mark a file normal, but possibly dirty.'''
450 if self._pl[1] != nullid:
450 if self._pl[1] != nullid:
451 # if there is a merge going on and the file was either
451 # if there is a merge going on and the file was either
452 # in state 'm' (-1) or coming from other parent (-2) before
452 # in state 'm' (-1) or coming from other parent (-2) before
453 # being removed, restore that state.
453 # being removed, restore that state.
454 entry = self._map.get(f)
454 entry = self._map.get(f)
455 if entry is not None:
455 if entry is not None:
456 if entry[0] == b'r' and entry[2] in (-1, -2):
456 if entry[0] == b'r' and entry[2] in (-1, -2):
457 source = self._map.copymap.get(f)
457 source = self._map.copymap.get(f)
458 if entry[2] == -1:
458 if entry[2] == -1:
459 self.merge(f)
459 self.merge(f)
460 elif entry[2] == -2:
460 elif entry[2] == -2:
461 self.otherparent(f)
461 self.otherparent(f)
462 if source:
462 if source:
463 self.copy(source, f)
463 self.copy(source, f)
464 return
464 return
465 if entry[0] == b'm' or entry[0] == b'n' and entry[2] == -2:
465 if entry[0] == b'm' or entry[0] == b'n' and entry[2] == -2:
466 return
466 return
467 self._addpath(f, b'n', 0, -1, -1)
467 self._addpath(f, b'n', 0, -1, -1)
468 self._map.copymap.pop(f, None)
468 self._map.copymap.pop(f, None)
469
469
470 def otherparent(self, f):
470 def otherparent(self, f):
471 '''Mark as coming from the other parent, always dirty.'''
471 '''Mark as coming from the other parent, always dirty.'''
472 if self._pl[1] == nullid:
472 if self._pl[1] == nullid:
473 raise error.Abort(
473 raise error.Abort(
474 _(b"setting %r to other parent only allowed in merges") % f
474 _(b"setting %r to other parent only allowed in merges") % f
475 )
475 )
476 if f in self and self[f] == b'n':
476 if f in self and self[f] == b'n':
477 # merge-like
477 # merge-like
478 self._addpath(f, b'm', 0, -2, -1)
478 self._addpath(f, b'm', 0, -2, -1)
479 else:
479 else:
480 # add-like
480 # add-like
481 self._addpath(f, b'n', 0, -2, -1)
481 self._addpath(f, b'n', 0, -2, -1)
482 self._map.copymap.pop(f, None)
482 self._map.copymap.pop(f, None)
483
483
484 def add(self, f):
484 def add(self, f):
485 '''Mark a file added.'''
485 '''Mark a file added.'''
486 self._addpath(f, b'a', 0, -1, -1)
486 self._addpath(f, b'a', 0, -1, -1)
487 self._map.copymap.pop(f, None)
487 self._map.copymap.pop(f, None)
488
488
489 def remove(self, f):
489 def remove(self, f):
490 '''Mark a file removed.'''
490 '''Mark a file removed.'''
491 self._dirty = True
491 self._dirty = True
492 oldstate = self[f]
492 oldstate = self[f]
493 size = 0
493 size = 0
494 if self._pl[1] != nullid:
494 if self._pl[1] != nullid:
495 entry = self._map.get(f)
495 entry = self._map.get(f)
496 if entry is not None:
496 if entry is not None:
497 # backup the previous state
497 # backup the previous state
498 if entry[0] == b'm': # merge
498 if entry[0] == b'm': # merge
499 size = -1
499 size = -1
500 elif entry[0] == b'n' and entry[2] == -2: # other parent
500 elif entry[0] == b'n' and entry[2] == -2: # other parent
501 size = -2
501 size = -2
502 self._map.otherparentset.add(f)
502 self._map.otherparentset.add(f)
503 self._updatedfiles.add(f)
503 self._updatedfiles.add(f)
504 self._map.removefile(f, oldstate, size)
504 self._map.removefile(f, oldstate, size)
505 if size == 0:
505 if size == 0:
506 self._map.copymap.pop(f, None)
506 self._map.copymap.pop(f, None)
507
507
508 def merge(self, f):
508 def merge(self, f):
509 '''Mark a file merged.'''
509 '''Mark a file merged.'''
510 if self._pl[1] == nullid:
510 if self._pl[1] == nullid:
511 return self.normallookup(f)
511 return self.normallookup(f)
512 return self.otherparent(f)
512 return self.otherparent(f)
513
513
514 def drop(self, f):
514 def drop(self, f):
515 '''Drop a file from the dirstate'''
515 '''Drop a file from the dirstate'''
516 oldstate = self[f]
516 oldstate = self[f]
517 if self._map.dropfile(f, oldstate):
517 if self._map.dropfile(f, oldstate):
518 self._dirty = True
518 self._dirty = True
519 self._updatedfiles.add(f)
519 self._updatedfiles.add(f)
520 self._map.copymap.pop(f, None)
520 self._map.copymap.pop(f, None)
521
521
522 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
522 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
523 if exists is None:
523 if exists is None:
524 exists = os.path.lexists(os.path.join(self._root, path))
524 exists = os.path.lexists(os.path.join(self._root, path))
525 if not exists:
525 if not exists:
526 # Maybe a path component exists
526 # Maybe a path component exists
527 if not ignoremissing and b'/' in path:
527 if not ignoremissing and b'/' in path:
528 d, f = path.rsplit(b'/', 1)
528 d, f = path.rsplit(b'/', 1)
529 d = self._normalize(d, False, ignoremissing, None)
529 d = self._normalize(d, False, ignoremissing, None)
530 folded = d + b"/" + f
530 folded = d + b"/" + f
531 else:
531 else:
532 # No path components, preserve original case
532 # No path components, preserve original case
533 folded = path
533 folded = path
534 else:
534 else:
535 # recursively normalize leading directory components
535 # recursively normalize leading directory components
536 # against dirstate
536 # against dirstate
537 if b'/' in normed:
537 if b'/' in normed:
538 d, f = normed.rsplit(b'/', 1)
538 d, f = normed.rsplit(b'/', 1)
539 d = self._normalize(d, False, ignoremissing, True)
539 d = self._normalize(d, False, ignoremissing, True)
540 r = self._root + b"/" + d
540 r = self._root + b"/" + d
541 folded = d + b"/" + util.fspath(f, r)
541 folded = d + b"/" + util.fspath(f, r)
542 else:
542 else:
543 folded = util.fspath(normed, self._root)
543 folded = util.fspath(normed, self._root)
544 storemap[normed] = folded
544 storemap[normed] = folded
545
545
546 return folded
546 return folded
547
547
548 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
548 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
549 normed = util.normcase(path)
549 normed = util.normcase(path)
550 folded = self._map.filefoldmap.get(normed, None)
550 folded = self._map.filefoldmap.get(normed, None)
551 if folded is None:
551 if folded is None:
552 if isknown:
552 if isknown:
553 folded = path
553 folded = path
554 else:
554 else:
555 folded = self._discoverpath(
555 folded = self._discoverpath(
556 path, normed, ignoremissing, exists, self._map.filefoldmap
556 path, normed, ignoremissing, exists, self._map.filefoldmap
557 )
557 )
558 return folded
558 return folded
559
559
560 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
560 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
561 normed = util.normcase(path)
561 normed = util.normcase(path)
562 folded = self._map.filefoldmap.get(normed, None)
562 folded = self._map.filefoldmap.get(normed, None)
563 if folded is None:
563 if folded is None:
564 folded = self._map.dirfoldmap.get(normed, None)
564 folded = self._map.dirfoldmap.get(normed, None)
565 if folded is None:
565 if folded is None:
566 if isknown:
566 if isknown:
567 folded = path
567 folded = path
568 else:
568 else:
569 # store discovered result in dirfoldmap so that future
569 # store discovered result in dirfoldmap so that future
570 # normalizefile calls don't start matching directories
570 # normalizefile calls don't start matching directories
571 folded = self._discoverpath(
571 folded = self._discoverpath(
572 path, normed, ignoremissing, exists, self._map.dirfoldmap
572 path, normed, ignoremissing, exists, self._map.dirfoldmap
573 )
573 )
574 return folded
574 return folded
575
575
576 def normalize(self, path, isknown=False, ignoremissing=False):
576 def normalize(self, path, isknown=False, ignoremissing=False):
577 '''
577 '''
578 normalize the case of a pathname when on a casefolding filesystem
578 normalize the case of a pathname when on a casefolding filesystem
579
579
580 isknown specifies whether the filename came from walking the
580 isknown specifies whether the filename came from walking the
581 disk, to avoid extra filesystem access.
581 disk, to avoid extra filesystem access.
582
582
583 If ignoremissing is True, missing path are returned
583 If ignoremissing is True, missing path are returned
584 unchanged. Otherwise, we try harder to normalize possibly
584 unchanged. Otherwise, we try harder to normalize possibly
585 existing path components.
585 existing path components.
586
586
587 The normalized case is determined based on the following precedence:
587 The normalized case is determined based on the following precedence:
588
588
589 - version of name already stored in the dirstate
589 - version of name already stored in the dirstate
590 - version of name stored on disk
590 - version of name stored on disk
591 - version provided via command arguments
591 - version provided via command arguments
592 '''
592 '''
593
593
594 if self._checkcase:
594 if self._checkcase:
595 return self._normalize(path, isknown, ignoremissing)
595 return self._normalize(path, isknown, ignoremissing)
596 return path
596 return path
597
597
598 def clear(self):
598 def clear(self):
599 self._map.clear()
599 self._map.clear()
600 self._lastnormaltime = 0
600 self._lastnormaltime = 0
601 self._updatedfiles.clear()
601 self._updatedfiles.clear()
602 self._dirty = True
602 self._dirty = True
603
603
604 def rebuild(self, parent, allfiles, changedfiles=None):
604 def rebuild(self, parent, allfiles, changedfiles=None):
605 if changedfiles is None:
605 if changedfiles is None:
606 # Rebuild entire dirstate
606 # Rebuild entire dirstate
607 to_lookup = allfiles
607 to_lookup = allfiles
608 to_drop = []
608 to_drop = []
609 lastnormaltime = self._lastnormaltime
609 lastnormaltime = self._lastnormaltime
610 self.clear()
610 self.clear()
611 self._lastnormaltime = lastnormaltime
611 self._lastnormaltime = lastnormaltime
612 elif len(changedfiles) < 10:
612 elif len(changedfiles) < 10:
613 # Avoid turning allfiles into a set, which can be expensive if it's
613 # Avoid turning allfiles into a set, which can be expensive if it's
614 # large.
614 # large.
615 to_lookup = []
615 to_lookup = []
616 to_drop = []
616 to_drop = []
617 for f in changedfiles:
617 for f in changedfiles:
618 if f in allfiles:
618 if f in allfiles:
619 to_lookup.append(f)
619 to_lookup.append(f)
620 else:
620 else:
621 to_drop.append(f)
621 to_drop.append(f)
622 else:
622 else:
623 changedfilesset = set(changedfiles)
623 changedfilesset = set(changedfiles)
624 to_lookup = changedfilesset & set(allfiles)
624 to_lookup = changedfilesset & set(allfiles)
625 to_drop = changedfilesset - to_lookup
625 to_drop = changedfilesset - to_lookup
626
626
627 if self._origpl is None:
627 if self._origpl is None:
628 self._origpl = self._pl
628 self._origpl = self._pl
629 self._map.setparents(parent, nullid)
629 self._map.setparents(parent, nullid)
630
630
631 for f in to_lookup:
631 for f in to_lookup:
632 self.normallookup(f)
632 self.normallookup(f)
633 for f in to_drop:
633 for f in to_drop:
634 self.drop(f)
634 self.drop(f)
635
635
636 self._dirty = True
636 self._dirty = True
637
637
638 def identity(self):
638 def identity(self):
639 '''Return identity of dirstate itself to detect changing in storage
639 '''Return identity of dirstate itself to detect changing in storage
640
640
641 If identity of previous dirstate is equal to this, writing
641 If identity of previous dirstate is equal to this, writing
642 changes based on the former dirstate out can keep consistency.
642 changes based on the former dirstate out can keep consistency.
643 '''
643 '''
644 return self._map.identity
644 return self._map.identity
645
645
646 def write(self, tr):
646 def write(self, tr):
647 if not self._dirty:
647 if not self._dirty:
648 return
648 return
649
649
650 filename = self._filename
650 filename = self._filename
651 if tr:
651 if tr:
652 # 'dirstate.write()' is not only for writing in-memory
652 # 'dirstate.write()' is not only for writing in-memory
653 # changes out, but also for dropping ambiguous timestamp.
653 # changes out, but also for dropping ambiguous timestamp.
654 # delayed writing re-raise "ambiguous timestamp issue".
654 # delayed writing re-raise "ambiguous timestamp issue".
655 # See also the wiki page below for detail:
655 # See also the wiki page below for detail:
656 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
656 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
657
657
658 # emulate dropping timestamp in 'parsers.pack_dirstate'
658 # emulate dropping timestamp in 'parsers.pack_dirstate'
659 now = _getfsnow(self._opener)
659 now = _getfsnow(self._opener)
660 self._map.clearambiguoustimes(self._updatedfiles, now)
660 self._map.clearambiguoustimes(self._updatedfiles, now)
661
661
662 # emulate that all 'dirstate.normal' results are written out
662 # emulate that all 'dirstate.normal' results are written out
663 self._lastnormaltime = 0
663 self._lastnormaltime = 0
664 self._updatedfiles.clear()
664 self._updatedfiles.clear()
665
665
666 # delay writing in-memory changes out
666 # delay writing in-memory changes out
667 tr.addfilegenerator(
667 tr.addfilegenerator(
668 b'dirstate',
668 b'dirstate',
669 (self._filename,),
669 (self._filename,),
670 self._writedirstate,
670 self._writedirstate,
671 location=b'plain',
671 location=b'plain',
672 )
672 )
673 return
673 return
674
674
675 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
675 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
676 self._writedirstate(st)
676 self._writedirstate(st)
677
677
678 def addparentchangecallback(self, category, callback):
678 def addparentchangecallback(self, category, callback):
679 """add a callback to be called when the wd parents are changed
679 """add a callback to be called when the wd parents are changed
680
680
681 Callback will be called with the following arguments:
681 Callback will be called with the following arguments:
682 dirstate, (oldp1, oldp2), (newp1, newp2)
682 dirstate, (oldp1, oldp2), (newp1, newp2)
683
683
684 Category is a unique identifier to allow overwriting an old callback
684 Category is a unique identifier to allow overwriting an old callback
685 with a newer callback.
685 with a newer callback.
686 """
686 """
687 self._plchangecallbacks[category] = callback
687 self._plchangecallbacks[category] = callback
688
688
689 def _writedirstate(self, st):
689 def _writedirstate(self, st):
690 # notify callbacks about parents change
690 # notify callbacks about parents change
691 if self._origpl is not None and self._origpl != self._pl:
691 if self._origpl is not None and self._origpl != self._pl:
692 for c, callback in sorted(
692 for c, callback in sorted(
693 pycompat.iteritems(self._plchangecallbacks)
693 pycompat.iteritems(self._plchangecallbacks)
694 ):
694 ):
695 callback(self, self._origpl, self._pl)
695 callback(self, self._origpl, self._pl)
696 self._origpl = None
696 self._origpl = None
697 # use the modification time of the newly created temporary file as the
697 # use the modification time of the newly created temporary file as the
698 # filesystem's notion of 'now'
698 # filesystem's notion of 'now'
699 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
699 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
700
700
701 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
701 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
702 # timestamp of each entries in dirstate, because of 'now > mtime'
702 # timestamp of each entries in dirstate, because of 'now > mtime'
703 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
703 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
704 if delaywrite > 0:
704 if delaywrite > 0:
705 # do we have any files to delay for?
705 # do we have any files to delay for?
706 for f, e in pycompat.iteritems(self._map):
706 for f, e in pycompat.iteritems(self._map):
707 if e[0] == b'n' and e[3] == now:
707 if e[0] == b'n' and e[3] == now:
708 import time # to avoid useless import
708 import time # to avoid useless import
709
709
710 # rather than sleep n seconds, sleep until the next
710 # rather than sleep n seconds, sleep until the next
711 # multiple of n seconds
711 # multiple of n seconds
712 clock = time.time()
712 clock = time.time()
713 start = int(clock) - (int(clock) % delaywrite)
713 start = int(clock) - (int(clock) % delaywrite)
714 end = start + delaywrite
714 end = start + delaywrite
715 time.sleep(end - clock)
715 time.sleep(end - clock)
716 now = end # trust our estimate that the end is near now
716 now = end # trust our estimate that the end is near now
717 break
717 break
718
718
719 self._map.write(st, now)
719 self._map.write(st, now)
720 self._lastnormaltime = 0
720 self._lastnormaltime = 0
721 self._dirty = False
721 self._dirty = False
722
722
723 def _dirignore(self, f):
723 def _dirignore(self, f):
724 if self._ignore(f):
724 if self._ignore(f):
725 return True
725 return True
726 for p in pathutil.finddirs(f):
726 for p in pathutil.finddirs(f):
727 if self._ignore(p):
727 if self._ignore(p):
728 return True
728 return True
729 return False
729 return False
730
730
731 def _ignorefiles(self):
731 def _ignorefiles(self):
732 files = []
732 files = []
733 if os.path.exists(self._join(b'.hgignore')):
733 if os.path.exists(self._join(b'.hgignore')):
734 files.append(self._join(b'.hgignore'))
734 files.append(self._join(b'.hgignore'))
735 for name, path in self._ui.configitems(b"ui"):
735 for name, path in self._ui.configitems(b"ui"):
736 if name == b'ignore' or name.startswith(b'ignore.'):
736 if name == b'ignore' or name.startswith(b'ignore.'):
737 # we need to use os.path.join here rather than self._join
737 # we need to use os.path.join here rather than self._join
738 # because path is arbitrary and user-specified
738 # because path is arbitrary and user-specified
739 files.append(os.path.join(self._rootdir, util.expandpath(path)))
739 files.append(os.path.join(self._rootdir, util.expandpath(path)))
740 return files
740 return files
741
741
742 def _ignorefileandline(self, f):
742 def _ignorefileandline(self, f):
743 files = collections.deque(self._ignorefiles())
743 files = collections.deque(self._ignorefiles())
744 visited = set()
744 visited = set()
745 while files:
745 while files:
746 i = files.popleft()
746 i = files.popleft()
747 patterns = matchmod.readpatternfile(
747 patterns = matchmod.readpatternfile(
748 i, self._ui.warn, sourceinfo=True
748 i, self._ui.warn, sourceinfo=True
749 )
749 )
750 for pattern, lineno, line in patterns:
750 for pattern, lineno, line in patterns:
751 kind, p = matchmod._patsplit(pattern, b'glob')
751 kind, p = matchmod._patsplit(pattern, b'glob')
752 if kind == b"subinclude":
752 if kind == b"subinclude":
753 if p not in visited:
753 if p not in visited:
754 files.append(p)
754 files.append(p)
755 continue
755 continue
756 m = matchmod.match(
756 m = matchmod.match(
757 self._root, b'', [], [pattern], warn=self._ui.warn
757 self._root, b'', [], [pattern], warn=self._ui.warn
758 )
758 )
759 if m(f):
759 if m(f):
760 return (i, lineno, line)
760 return (i, lineno, line)
761 visited.add(i)
761 visited.add(i)
762 return (None, -1, b"")
762 return (None, -1, b"")
763
763
764 def _walkexplicit(self, match, subrepos):
764 def _walkexplicit(self, match, subrepos):
765 '''Get stat data about the files explicitly specified by match.
765 '''Get stat data about the files explicitly specified by match.
766
766
767 Return a triple (results, dirsfound, dirsnotfound).
767 Return a triple (results, dirsfound, dirsnotfound).
768 - results is a mapping from filename to stat result. It also contains
768 - results is a mapping from filename to stat result. It also contains
769 listings mapping subrepos and .hg to None.
769 listings mapping subrepos and .hg to None.
770 - dirsfound is a list of files found to be directories.
770 - dirsfound is a list of files found to be directories.
771 - dirsnotfound is a list of files that the dirstate thinks are
771 - dirsnotfound is a list of files that the dirstate thinks are
772 directories and that were not found.'''
772 directories and that were not found.'''
773
773
774 def badtype(mode):
774 def badtype(mode):
775 kind = _(b'unknown')
775 kind = _(b'unknown')
776 if stat.S_ISCHR(mode):
776 if stat.S_ISCHR(mode):
777 kind = _(b'character device')
777 kind = _(b'character device')
778 elif stat.S_ISBLK(mode):
778 elif stat.S_ISBLK(mode):
779 kind = _(b'block device')
779 kind = _(b'block device')
780 elif stat.S_ISFIFO(mode):
780 elif stat.S_ISFIFO(mode):
781 kind = _(b'fifo')
781 kind = _(b'fifo')
782 elif stat.S_ISSOCK(mode):
782 elif stat.S_ISSOCK(mode):
783 kind = _(b'socket')
783 kind = _(b'socket')
784 elif stat.S_ISDIR(mode):
784 elif stat.S_ISDIR(mode):
785 kind = _(b'directory')
785 kind = _(b'directory')
786 return _(b'unsupported file type (type is %s)') % kind
786 return _(b'unsupported file type (type is %s)') % kind
787
787
788 badfn = match.bad
788 badfn = match.bad
789 dmap = self._map
789 dmap = self._map
790 lstat = os.lstat
790 lstat = os.lstat
791 getkind = stat.S_IFMT
791 getkind = stat.S_IFMT
792 dirkind = stat.S_IFDIR
792 dirkind = stat.S_IFDIR
793 regkind = stat.S_IFREG
793 regkind = stat.S_IFREG
794 lnkkind = stat.S_IFLNK
794 lnkkind = stat.S_IFLNK
795 join = self._join
795 join = self._join
796 dirsfound = []
796 dirsfound = []
797 foundadd = dirsfound.append
797 foundadd = dirsfound.append
798 dirsnotfound = []
798 dirsnotfound = []
799 notfoundadd = dirsnotfound.append
799 notfoundadd = dirsnotfound.append
800
800
801 if not match.isexact() and self._checkcase:
801 if not match.isexact() and self._checkcase:
802 normalize = self._normalize
802 normalize = self._normalize
803 else:
803 else:
804 normalize = None
804 normalize = None
805
805
806 files = sorted(match.files())
806 files = sorted(match.files())
807 subrepos.sort()
807 subrepos.sort()
808 i, j = 0, 0
808 i, j = 0, 0
809 while i < len(files) and j < len(subrepos):
809 while i < len(files) and j < len(subrepos):
810 subpath = subrepos[j] + b"/"
810 subpath = subrepos[j] + b"/"
811 if files[i] < subpath:
811 if files[i] < subpath:
812 i += 1
812 i += 1
813 continue
813 continue
814 while i < len(files) and files[i].startswith(subpath):
814 while i < len(files) and files[i].startswith(subpath):
815 del files[i]
815 del files[i]
816 j += 1
816 j += 1
817
817
818 if not files or b'' in files:
818 if not files or b'' in files:
819 files = [b'']
819 files = [b'']
820 # constructing the foldmap is expensive, so don't do it for the
820 # constructing the foldmap is expensive, so don't do it for the
821 # common case where files is ['']
821 # common case where files is ['']
822 normalize = None
822 normalize = None
823 results = dict.fromkeys(subrepos)
823 results = dict.fromkeys(subrepos)
824 results[b'.hg'] = None
824 results[b'.hg'] = None
825
825
826 for ff in files:
826 for ff in files:
827 if normalize:
827 if normalize:
828 nf = normalize(ff, False, True)
828 nf = normalize(ff, False, True)
829 else:
829 else:
830 nf = ff
830 nf = ff
831 if nf in results:
831 if nf in results:
832 continue
832 continue
833
833
834 try:
834 try:
835 st = lstat(join(nf))
835 st = lstat(join(nf))
836 kind = getkind(st.st_mode)
836 kind = getkind(st.st_mode)
837 if kind == dirkind:
837 if kind == dirkind:
838 if nf in dmap:
838 if nf in dmap:
839 # file replaced by dir on disk but still in dirstate
839 # file replaced by dir on disk but still in dirstate
840 results[nf] = None
840 results[nf] = None
841 foundadd((nf, ff))
841 foundadd((nf, ff))
842 elif kind == regkind or kind == lnkkind:
842 elif kind == regkind or kind == lnkkind:
843 results[nf] = st
843 results[nf] = st
844 else:
844 else:
845 badfn(ff, badtype(kind))
845 badfn(ff, badtype(kind))
846 if nf in dmap:
846 if nf in dmap:
847 results[nf] = None
847 results[nf] = None
848 except OSError as inst: # nf not found on disk - it is dirstate only
848 except OSError as inst: # nf not found on disk - it is dirstate only
849 if nf in dmap: # does it exactly match a missing file?
849 if nf in dmap: # does it exactly match a missing file?
850 results[nf] = None
850 results[nf] = None
851 else: # does it match a missing directory?
851 else: # does it match a missing directory?
852 if self._map.hasdir(nf):
852 if self._map.hasdir(nf):
853 notfoundadd(nf)
853 notfoundadd(nf)
854 else:
854 else:
855 badfn(ff, encoding.strtolocal(inst.strerror))
855 badfn(ff, encoding.strtolocal(inst.strerror))
856
856
857 # match.files() may contain explicitly-specified paths that shouldn't
857 # match.files() may contain explicitly-specified paths that shouldn't
858 # be taken; drop them from the list of files found. dirsfound/notfound
858 # be taken; drop them from the list of files found. dirsfound/notfound
859 # aren't filtered here because they will be tested later.
859 # aren't filtered here because they will be tested later.
860 if match.anypats():
860 if match.anypats():
861 for f in list(results):
861 for f in list(results):
862 if f == b'.hg' or f in subrepos:
862 if f == b'.hg' or f in subrepos:
863 # keep sentinel to disable further out-of-repo walks
863 # keep sentinel to disable further out-of-repo walks
864 continue
864 continue
865 if not match(f):
865 if not match(f):
866 del results[f]
866 del results[f]
867
867
868 # Case insensitive filesystems cannot rely on lstat() failing to detect
868 # Case insensitive filesystems cannot rely on lstat() failing to detect
869 # a case-only rename. Prune the stat object for any file that does not
869 # a case-only rename. Prune the stat object for any file that does not
870 # match the case in the filesystem, if there are multiple files that
870 # match the case in the filesystem, if there are multiple files that
871 # normalize to the same path.
871 # normalize to the same path.
872 if match.isexact() and self._checkcase:
872 if match.isexact() and self._checkcase:
873 normed = {}
873 normed = {}
874
874
875 for f, st in pycompat.iteritems(results):
875 for f, st in pycompat.iteritems(results):
876 if st is None:
876 if st is None:
877 continue
877 continue
878
878
879 nc = util.normcase(f)
879 nc = util.normcase(f)
880 paths = normed.get(nc)
880 paths = normed.get(nc)
881
881
882 if paths is None:
882 if paths is None:
883 paths = set()
883 paths = set()
884 normed[nc] = paths
884 normed[nc] = paths
885
885
886 paths.add(f)
886 paths.add(f)
887
887
888 for norm, paths in pycompat.iteritems(normed):
888 for norm, paths in pycompat.iteritems(normed):
889 if len(paths) > 1:
889 if len(paths) > 1:
890 for path in paths:
890 for path in paths:
891 folded = self._discoverpath(
891 folded = self._discoverpath(
892 path, norm, True, None, self._map.dirfoldmap
892 path, norm, True, None, self._map.dirfoldmap
893 )
893 )
894 if path != folded:
894 if path != folded:
895 results[path] = None
895 results[path] = None
896
896
897 return results, dirsfound, dirsnotfound
897 return results, dirsfound, dirsnotfound
898
898
899 def walk(self, match, subrepos, unknown, ignored, full=True):
899 def walk(self, match, subrepos, unknown, ignored, full=True):
900 '''
900 '''
901 Walk recursively through the directory tree, finding all files
901 Walk recursively through the directory tree, finding all files
902 matched by match.
902 matched by match.
903
903
904 If full is False, maybe skip some known-clean files.
904 If full is False, maybe skip some known-clean files.
905
905
906 Return a dict mapping filename to stat-like object (either
906 Return a dict mapping filename to stat-like object (either
907 mercurial.osutil.stat instance or return value of os.stat()).
907 mercurial.osutil.stat instance or return value of os.stat()).
908
908
909 '''
909 '''
910 # full is a flag that extensions that hook into walk can use -- this
910 # full is a flag that extensions that hook into walk can use -- this
911 # implementation doesn't use it at all. This satisfies the contract
911 # implementation doesn't use it at all. This satisfies the contract
912 # because we only guarantee a "maybe".
912 # because we only guarantee a "maybe".
913
913
914 if ignored:
914 if ignored:
915 ignore = util.never
915 ignore = util.never
916 dirignore = util.never
916 dirignore = util.never
917 elif unknown:
917 elif unknown:
918 ignore = self._ignore
918 ignore = self._ignore
919 dirignore = self._dirignore
919 dirignore = self._dirignore
920 else:
920 else:
921 # if not unknown and not ignored, drop dir recursion and step 2
921 # if not unknown and not ignored, drop dir recursion and step 2
922 ignore = util.always
922 ignore = util.always
923 dirignore = util.always
923 dirignore = util.always
924
924
925 matchfn = match.matchfn
925 matchfn = match.matchfn
926 matchalways = match.always()
926 matchalways = match.always()
927 matchtdir = match.traversedir
927 matchtdir = match.traversedir
928 dmap = self._map
928 dmap = self._map
929 listdir = util.listdir
929 listdir = util.listdir
930 lstat = os.lstat
930 lstat = os.lstat
931 dirkind = stat.S_IFDIR
931 dirkind = stat.S_IFDIR
932 regkind = stat.S_IFREG
932 regkind = stat.S_IFREG
933 lnkkind = stat.S_IFLNK
933 lnkkind = stat.S_IFLNK
934 join = self._join
934 join = self._join
935
935
936 exact = skipstep3 = False
936 exact = skipstep3 = False
937 if match.isexact(): # match.exact
937 if match.isexact(): # match.exact
938 exact = True
938 exact = True
939 dirignore = util.always # skip step 2
939 dirignore = util.always # skip step 2
940 elif match.prefix(): # match.match, no patterns
940 elif match.prefix(): # match.match, no patterns
941 skipstep3 = True
941 skipstep3 = True
942
942
943 if not exact and self._checkcase:
943 if not exact and self._checkcase:
944 normalize = self._normalize
944 normalize = self._normalize
945 normalizefile = self._normalizefile
945 normalizefile = self._normalizefile
946 skipstep3 = False
946 skipstep3 = False
947 else:
947 else:
948 normalize = self._normalize
948 normalize = self._normalize
949 normalizefile = None
949 normalizefile = None
950
950
951 # step 1: find all explicit files
951 # step 1: find all explicit files
952 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
952 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
953 if matchtdir:
953 if matchtdir:
954 for d in work:
954 for d in work:
955 matchtdir(d[0])
955 matchtdir(d[0])
956 for d in dirsnotfound:
956 for d in dirsnotfound:
957 matchtdir(d)
957 matchtdir(d)
958
958
959 skipstep3 = skipstep3 and not (work or dirsnotfound)
959 skipstep3 = skipstep3 and not (work or dirsnotfound)
960 work = [d for d in work if not dirignore(d[0])]
960 work = [d for d in work if not dirignore(d[0])]
961
961
962 # step 2: visit subdirectories
962 # step 2: visit subdirectories
963 def traverse(work, alreadynormed):
963 def traverse(work, alreadynormed):
964 wadd = work.append
964 wadd = work.append
965 while work:
965 while work:
966 tracing.counter('dirstate.walk work', len(work))
966 tracing.counter('dirstate.walk work', len(work))
967 nd = work.pop()
967 nd = work.pop()
968 visitentries = match.visitchildrenset(nd)
968 visitentries = match.visitchildrenset(nd)
969 if not visitentries:
969 if not visitentries:
970 continue
970 continue
971 if visitentries == b'this' or visitentries == b'all':
971 if visitentries == b'this' or visitentries == b'all':
972 visitentries = None
972 visitentries = None
973 skip = None
973 skip = None
974 if nd != b'':
974 if nd != b'':
975 skip = b'.hg'
975 skip = b'.hg'
976 try:
976 try:
977 with tracing.log('dirstate.walk.traverse listdir %s', nd):
977 with tracing.log('dirstate.walk.traverse listdir %s', nd):
978 entries = listdir(join(nd), stat=True, skip=skip)
978 entries = listdir(join(nd), stat=True, skip=skip)
979 except OSError as inst:
979 except OSError as inst:
980 if inst.errno in (errno.EACCES, errno.ENOENT):
980 if inst.errno in (errno.EACCES, errno.ENOENT):
981 match.bad(
981 match.bad(
982 self.pathto(nd), encoding.strtolocal(inst.strerror)
982 self.pathto(nd), encoding.strtolocal(inst.strerror)
983 )
983 )
984 continue
984 continue
985 raise
985 raise
986 for f, kind, st in entries:
986 for f, kind, st in entries:
987 # Some matchers may return files in the visitentries set,
987 # Some matchers may return files in the visitentries set,
988 # instead of 'this', if the matcher explicitly mentions them
988 # instead of 'this', if the matcher explicitly mentions them
989 # and is not an exactmatcher. This is acceptable; we do not
989 # and is not an exactmatcher. This is acceptable; we do not
990 # make any hard assumptions about file-or-directory below
990 # make any hard assumptions about file-or-directory below
991 # based on the presence of `f` in visitentries. If
991 # based on the presence of `f` in visitentries. If
992 # visitchildrenset returned a set, we can always skip the
992 # visitchildrenset returned a set, we can always skip the
993 # entries *not* in the set it provided regardless of whether
993 # entries *not* in the set it provided regardless of whether
994 # they're actually a file or a directory.
994 # they're actually a file or a directory.
995 if visitentries and f not in visitentries:
995 if visitentries and f not in visitentries:
996 continue
996 continue
997 if normalizefile:
997 if normalizefile:
998 # even though f might be a directory, we're only
998 # even though f might be a directory, we're only
999 # interested in comparing it to files currently in the
999 # interested in comparing it to files currently in the
1000 # dmap -- therefore normalizefile is enough
1000 # dmap -- therefore normalizefile is enough
1001 nf = normalizefile(
1001 nf = normalizefile(
1002 nd and (nd + b"/" + f) or f, True, True
1002 nd and (nd + b"/" + f) or f, True, True
1003 )
1003 )
1004 else:
1004 else:
1005 nf = nd and (nd + b"/" + f) or f
1005 nf = nd and (nd + b"/" + f) or f
1006 if nf not in results:
1006 if nf not in results:
1007 if kind == dirkind:
1007 if kind == dirkind:
1008 if not ignore(nf):
1008 if not ignore(nf):
1009 if matchtdir:
1009 if matchtdir:
1010 matchtdir(nf)
1010 matchtdir(nf)
1011 wadd(nf)
1011 wadd(nf)
1012 if nf in dmap and (matchalways or matchfn(nf)):
1012 if nf in dmap and (matchalways or matchfn(nf)):
1013 results[nf] = None
1013 results[nf] = None
1014 elif kind == regkind or kind == lnkkind:
1014 elif kind == regkind or kind == lnkkind:
1015 if nf in dmap:
1015 if nf in dmap:
1016 if matchalways or matchfn(nf):
1016 if matchalways or matchfn(nf):
1017 results[nf] = st
1017 results[nf] = st
1018 elif (matchalways or matchfn(nf)) and not ignore(
1018 elif (matchalways or matchfn(nf)) and not ignore(
1019 nf
1019 nf
1020 ):
1020 ):
1021 # unknown file -- normalize if necessary
1021 # unknown file -- normalize if necessary
1022 if not alreadynormed:
1022 if not alreadynormed:
1023 nf = normalize(nf, False, True)
1023 nf = normalize(nf, False, True)
1024 results[nf] = st
1024 results[nf] = st
1025 elif nf in dmap and (matchalways or matchfn(nf)):
1025 elif nf in dmap and (matchalways or matchfn(nf)):
1026 results[nf] = None
1026 results[nf] = None
1027
1027
1028 for nd, d in work:
1028 for nd, d in work:
1029 # alreadynormed means that processwork doesn't have to do any
1029 # alreadynormed means that processwork doesn't have to do any
1030 # expensive directory normalization
1030 # expensive directory normalization
1031 alreadynormed = not normalize or nd == d
1031 alreadynormed = not normalize or nd == d
1032 traverse([d], alreadynormed)
1032 traverse([d], alreadynormed)
1033
1033
1034 for s in subrepos:
1034 for s in subrepos:
1035 del results[s]
1035 del results[s]
1036 del results[b'.hg']
1036 del results[b'.hg']
1037
1037
1038 # step 3: visit remaining files from dmap
1038 # step 3: visit remaining files from dmap
1039 if not skipstep3 and not exact:
1039 if not skipstep3 and not exact:
1040 # If a dmap file is not in results yet, it was either
1040 # If a dmap file is not in results yet, it was either
1041 # a) not matching matchfn b) ignored, c) missing, or d) under a
1041 # a) not matching matchfn b) ignored, c) missing, or d) under a
1042 # symlink directory.
1042 # symlink directory.
1043 if not results and matchalways:
1043 if not results and matchalways:
1044 visit = [f for f in dmap]
1044 visit = [f for f in dmap]
1045 else:
1045 else:
1046 visit = [f for f in dmap if f not in results and matchfn(f)]
1046 visit = [f for f in dmap if f not in results and matchfn(f)]
1047 visit.sort()
1047 visit.sort()
1048
1048
1049 if unknown:
1049 if unknown:
1050 # unknown == True means we walked all dirs under the roots
1050 # unknown == True means we walked all dirs under the roots
1051 # that wasn't ignored, and everything that matched was stat'ed
1051 # that wasn't ignored, and everything that matched was stat'ed
1052 # and is already in results.
1052 # and is already in results.
1053 # The rest must thus be ignored or under a symlink.
1053 # The rest must thus be ignored or under a symlink.
1054 audit_path = pathutil.pathauditor(self._root, cached=True)
1054 audit_path = pathutil.pathauditor(self._root, cached=True)
1055
1055
1056 for nf in iter(visit):
1056 for nf in iter(visit):
1057 # If a stat for the same file was already added with a
1057 # If a stat for the same file was already added with a
1058 # different case, don't add one for this, since that would
1058 # different case, don't add one for this, since that would
1059 # make it appear as if the file exists under both names
1059 # make it appear as if the file exists under both names
1060 # on disk.
1060 # on disk.
1061 if (
1061 if (
1062 normalizefile
1062 normalizefile
1063 and normalizefile(nf, True, True) in results
1063 and normalizefile(nf, True, True) in results
1064 ):
1064 ):
1065 results[nf] = None
1065 results[nf] = None
1066 # Report ignored items in the dmap as long as they are not
1066 # Report ignored items in the dmap as long as they are not
1067 # under a symlink directory.
1067 # under a symlink directory.
1068 elif audit_path.check(nf):
1068 elif audit_path.check(nf):
1069 try:
1069 try:
1070 results[nf] = lstat(join(nf))
1070 results[nf] = lstat(join(nf))
1071 # file was just ignored, no links, and exists
1071 # file was just ignored, no links, and exists
1072 except OSError:
1072 except OSError:
1073 # file doesn't exist
1073 # file doesn't exist
1074 results[nf] = None
1074 results[nf] = None
1075 else:
1075 else:
1076 # It's either missing or under a symlink directory
1076 # It's either missing or under a symlink directory
1077 # which we in this case report as missing
1077 # which we in this case report as missing
1078 results[nf] = None
1078 results[nf] = None
1079 else:
1079 else:
1080 # We may not have walked the full directory tree above,
1080 # We may not have walked the full directory tree above,
1081 # so stat and check everything we missed.
1081 # so stat and check everything we missed.
1082 iv = iter(visit)
1082 iv = iter(visit)
1083 for st in util.statfiles([join(i) for i in visit]):
1083 for st in util.statfiles([join(i) for i in visit]):
1084 results[next(iv)] = st
1084 results[next(iv)] = st
1085 return results
1085 return results
1086
1086
1087 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1087 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1088 # Force Rayon (Rust parallelism library) to respect the number of
1088 # Force Rayon (Rust parallelism library) to respect the number of
1089 # workers. This is a temporary workaround until Rust code knows
1089 # workers. This is a temporary workaround until Rust code knows
1090 # how to read the config file.
1090 # how to read the config file.
1091 numcpus = self._ui.configint(b"worker", b"numcpus")
1091 numcpus = self._ui.configint(b"worker", b"numcpus")
1092 if numcpus is not None:
1092 if numcpus is not None:
1093 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1093 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1094
1094
1095 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1095 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1096 if not workers_enabled:
1096 if not workers_enabled:
1097 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1097 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1098
1098
1099 (
1099 (
1100 lookup,
1100 lookup,
1101 modified,
1101 modified,
1102 added,
1102 added,
1103 removed,
1103 removed,
1104 deleted,
1104 deleted,
1105 clean,
1105 clean,
1106 ignored,
1106 ignored,
1107 unknown,
1107 unknown,
1108 warnings,
1108 warnings,
1109 bad,
1109 bad,
1110 ) = rustmod.status(
1110 ) = rustmod.status(
1111 self._map._rustmap,
1111 self._map._rustmap,
1112 matcher,
1112 matcher,
1113 self._rootdir,
1113 self._rootdir,
1114 self._ignorefiles(),
1114 self._ignorefiles(),
1115 self._checkexec,
1115 self._checkexec,
1116 self._lastnormaltime,
1116 self._lastnormaltime,
1117 bool(list_clean),
1117 bool(list_clean),
1118 bool(list_ignored),
1118 bool(list_ignored),
1119 bool(list_unknown),
1119 bool(list_unknown),
1120 )
1120 )
1121 if self._ui.warn:
1121 if self._ui.warn:
1122 for item in warnings:
1122 for item in warnings:
1123 if isinstance(item, tuple):
1123 if isinstance(item, tuple):
1124 file_path, syntax = item
1124 file_path, syntax = item
1125 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1125 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1126 file_path,
1126 file_path,
1127 syntax,
1127 syntax,
1128 )
1128 )
1129 self._ui.warn(msg)
1129 self._ui.warn(msg)
1130 else:
1130 else:
1131 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1131 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1132 self._ui.warn(
1132 self._ui.warn(
1133 msg
1133 msg
1134 % (
1134 % (
1135 pathutil.canonpath(
1135 pathutil.canonpath(
1136 self._rootdir, self._rootdir, item
1136 self._rootdir, self._rootdir, item
1137 ),
1137 ),
1138 b"No such file or directory",
1138 b"No such file or directory",
1139 )
1139 )
1140 )
1140 )
1141
1141
1142 for (fn, message) in bad:
1142 for (fn, message) in bad:
1143 matcher.bad(fn, encoding.strtolocal(message))
1143 matcher.bad(fn, encoding.strtolocal(message))
1144
1144
1145 status = scmutil.status(
1145 status = scmutil.status(
1146 modified=modified,
1146 modified=modified,
1147 added=added,
1147 added=added,
1148 removed=removed,
1148 removed=removed,
1149 deleted=deleted,
1149 deleted=deleted,
1150 unknown=unknown,
1150 unknown=unknown,
1151 ignored=ignored,
1151 ignored=ignored,
1152 clean=clean,
1152 clean=clean,
1153 )
1153 )
1154 return (lookup, status)
1154 return (lookup, status)
1155
1155
1156 def status(self, match, subrepos, ignored, clean, unknown):
1156 def status(self, match, subrepos, ignored, clean, unknown):
1157 '''Determine the status of the working copy relative to the
1157 '''Determine the status of the working copy relative to the
1158 dirstate and return a pair of (unsure, status), where status is of type
1158 dirstate and return a pair of (unsure, status), where status is of type
1159 scmutil.status and:
1159 scmutil.status and:
1160
1160
1161 unsure:
1161 unsure:
1162 files that might have been modified since the dirstate was
1162 files that might have been modified since the dirstate was
1163 written, but need to be read to be sure (size is the same
1163 written, but need to be read to be sure (size is the same
1164 but mtime differs)
1164 but mtime differs)
1165 status.modified:
1165 status.modified:
1166 files that have definitely been modified since the dirstate
1166 files that have definitely been modified since the dirstate
1167 was written (different size or mode)
1167 was written (different size or mode)
1168 status.clean:
1168 status.clean:
1169 files that have definitely not been modified since the
1169 files that have definitely not been modified since the
1170 dirstate was written
1170 dirstate was written
1171 '''
1171 '''
1172 listignored, listclean, listunknown = ignored, clean, unknown
1172 listignored, listclean, listunknown = ignored, clean, unknown
1173 lookup, modified, added, unknown, ignored = [], [], [], [], []
1173 lookup, modified, added, unknown, ignored = [], [], [], [], []
1174 removed, deleted, clean = [], [], []
1174 removed, deleted, clean = [], [], []
1175
1175
1176 dmap = self._map
1176 dmap = self._map
1177 dmap.preload()
1177 dmap.preload()
1178
1178
1179 use_rust = True
1179 use_rust = True
1180
1180
1181 allowed_matchers = (
1181 allowed_matchers = (
1182 matchmod.alwaysmatcher,
1182 matchmod.alwaysmatcher,
1183 matchmod.exactmatcher,
1183 matchmod.exactmatcher,
1184 matchmod.includematcher,
1184 matchmod.includematcher,
1185 )
1185 )
1186
1186
1187 if rustmod is None:
1187 if rustmod is None:
1188 use_rust = False
1188 use_rust = False
1189 elif self._checkcase:
1189 elif self._checkcase:
1190 # Case-insensitive filesystems are not handled yet
1190 # Case-insensitive filesystems are not handled yet
1191 use_rust = False
1191 use_rust = False
1192 elif subrepos:
1192 elif subrepos:
1193 use_rust = False
1193 use_rust = False
1194 elif sparse.enabled:
1194 elif sparse.enabled:
1195 use_rust = False
1195 use_rust = False
1196 elif match.traversedir is not None:
1196 elif match.traversedir is not None:
1197 use_rust = False
1197 use_rust = False
1198 elif not isinstance(match, allowed_matchers):
1198 elif not isinstance(match, allowed_matchers):
1199 # Matchers have yet to be implemented
1199 # Matchers have yet to be implemented
1200 use_rust = False
1200 use_rust = False
1201
1201
1202 if use_rust:
1202 if use_rust:
1203 try:
1203 try:
1204 return self._rust_status(
1204 return self._rust_status(
1205 match, listclean, listignored, listunknown
1205 match, listclean, listignored, listunknown
1206 )
1206 )
1207 except rustmod.FallbackError:
1207 except rustmod.FallbackError:
1208 pass
1208 pass
1209
1209
1210 def noop(f):
1210 def noop(f):
1211 pass
1211 pass
1212
1212
1213 dcontains = dmap.__contains__
1213 dcontains = dmap.__contains__
1214 dget = dmap.__getitem__
1214 dget = dmap.__getitem__
1215 ladd = lookup.append # aka "unsure"
1215 ladd = lookup.append # aka "unsure"
1216 madd = modified.append
1216 madd = modified.append
1217 aadd = added.append
1217 aadd = added.append
1218 uadd = unknown.append if listunknown else noop
1218 uadd = unknown.append if listunknown else noop
1219 iadd = ignored.append if listignored else noop
1219 iadd = ignored.append if listignored else noop
1220 radd = removed.append
1220 radd = removed.append
1221 dadd = deleted.append
1221 dadd = deleted.append
1222 cadd = clean.append if listclean else noop
1222 cadd = clean.append if listclean else noop
1223 mexact = match.exact
1223 mexact = match.exact
1224 dirignore = self._dirignore
1224 dirignore = self._dirignore
1225 checkexec = self._checkexec
1225 checkexec = self._checkexec
1226 copymap = self._map.copymap
1226 copymap = self._map.copymap
1227 lastnormaltime = self._lastnormaltime
1227 lastnormaltime = self._lastnormaltime
1228
1228
1229 # We need to do full walks when either
1229 # We need to do full walks when either
1230 # - we're listing all clean files, or
1230 # - we're listing all clean files, or
1231 # - match.traversedir does something, because match.traversedir should
1231 # - match.traversedir does something, because match.traversedir should
1232 # be called for every dir in the working dir
1232 # be called for every dir in the working dir
1233 full = listclean or match.traversedir is not None
1233 full = listclean or match.traversedir is not None
1234 for fn, st in pycompat.iteritems(
1234 for fn, st in pycompat.iteritems(
1235 self.walk(match, subrepos, listunknown, listignored, full=full)
1235 self.walk(match, subrepos, listunknown, listignored, full=full)
1236 ):
1236 ):
1237 if not dcontains(fn):
1237 if not dcontains(fn):
1238 if (listignored or mexact(fn)) and dirignore(fn):
1238 if (listignored or mexact(fn)) and dirignore(fn):
1239 if listignored:
1239 if listignored:
1240 iadd(fn)
1240 iadd(fn)
1241 else:
1241 else:
1242 uadd(fn)
1242 uadd(fn)
1243 continue
1243 continue
1244
1244
1245 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1245 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1246 # written like that for performance reasons. dmap[fn] is not a
1246 # written like that for performance reasons. dmap[fn] is not a
1247 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1247 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1248 # opcode has fast paths when the value to be unpacked is a tuple or
1248 # opcode has fast paths when the value to be unpacked is a tuple or
1249 # a list, but falls back to creating a full-fledged iterator in
1249 # a list, but falls back to creating a full-fledged iterator in
1250 # general. That is much slower than simply accessing and storing the
1250 # general. That is much slower than simply accessing and storing the
1251 # tuple members one by one.
1251 # tuple members one by one.
1252 t = dget(fn)
1252 t = dget(fn)
1253 state = t[0]
1253 state = t[0]
1254 mode = t[1]
1254 mode = t[1]
1255 size = t[2]
1255 size = t[2]
1256 time = t[3]
1256 time = t[3]
1257
1257
1258 if not st and state in b"nma":
1258 if not st and state in b"nma":
1259 dadd(fn)
1259 dadd(fn)
1260 elif state == b'n':
1260 elif state == b'n':
1261 if (
1261 if (
1262 size >= 0
1262 size >= 0
1263 and (
1263 and (
1264 (size != st.st_size and size != st.st_size & _rangemask)
1264 (size != st.st_size and size != st.st_size & _rangemask)
1265 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1265 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1266 )
1266 )
1267 or size == -2 # other parent
1267 or size == -2 # other parent
1268 or fn in copymap
1268 or fn in copymap
1269 ):
1269 ):
1270 madd(fn)
1270 madd(fn)
1271 elif (
1271 elif (
1272 time != st[stat.ST_MTIME]
1272 time != st[stat.ST_MTIME]
1273 and time != st[stat.ST_MTIME] & _rangemask
1273 and time != st[stat.ST_MTIME] & _rangemask
1274 ):
1274 ):
1275 ladd(fn)
1275 ladd(fn)
1276 elif st[stat.ST_MTIME] == lastnormaltime:
1276 elif st[stat.ST_MTIME] == lastnormaltime:
1277 # fn may have just been marked as normal and it may have
1277 # fn may have just been marked as normal and it may have
1278 # changed in the same second without changing its size.
1278 # changed in the same second without changing its size.
1279 # This can happen if we quickly do multiple commits.
1279 # This can happen if we quickly do multiple commits.
1280 # Force lookup, so we don't miss such a racy file change.
1280 # Force lookup, so we don't miss such a racy file change.
1281 ladd(fn)
1281 ladd(fn)
1282 elif listclean:
1282 elif listclean:
1283 cadd(fn)
1283 cadd(fn)
1284 elif state == b'm':
1284 elif state == b'm':
1285 madd(fn)
1285 madd(fn)
1286 elif state == b'a':
1286 elif state == b'a':
1287 aadd(fn)
1287 aadd(fn)
1288 elif state == b'r':
1288 elif state == b'r':
1289 radd(fn)
1289 radd(fn)
1290 status = scmutil.status(
1290 status = scmutil.status(
1291 modified, added, removed, deleted, unknown, ignored, clean
1291 modified, added, removed, deleted, unknown, ignored, clean
1292 )
1292 )
1293 return (lookup, status)
1293 return (lookup, status)
1294
1294
1295 def matches(self, match):
1295 def matches(self, match):
1296 '''
1296 '''
1297 return files in the dirstate (in whatever state) filtered by match
1297 return files in the dirstate (in whatever state) filtered by match
1298 '''
1298 '''
1299 dmap = self._map
1299 dmap = self._map
1300 if rustmod is not None:
1300 if rustmod is not None:
1301 dmap = self._map._rustmap
1301 dmap = self._map._rustmap
1302
1302
1303 if match.always():
1303 if match.always():
1304 return dmap.keys()
1304 return dmap.keys()
1305 files = match.files()
1305 files = match.files()
1306 if match.isexact():
1306 if match.isexact():
1307 # fast path -- filter the other way around, since typically files is
1307 # fast path -- filter the other way around, since typically files is
1308 # much smaller than dmap
1308 # much smaller than dmap
1309 return [f for f in files if f in dmap]
1309 return [f for f in files if f in dmap]
1310 if match.prefix() and all(fn in dmap for fn in files):
1310 if match.prefix() and all(fn in dmap for fn in files):
1311 # fast path -- all the values are known to be files, so just return
1311 # fast path -- all the values are known to be files, so just return
1312 # that
1312 # that
1313 return list(files)
1313 return list(files)
1314 return [f for f in dmap if match(f)]
1314 return [f for f in dmap if match(f)]
1315
1315
1316 def _actualfilename(self, tr):
1316 def _actualfilename(self, tr):
1317 if tr:
1317 if tr:
1318 return self._pendingfilename
1318 return self._pendingfilename
1319 else:
1319 else:
1320 return self._filename
1320 return self._filename
1321
1321
1322 def savebackup(self, tr, backupname):
1322 def savebackup(self, tr, backupname):
1323 '''Save current dirstate into backup file'''
1323 '''Save current dirstate into backup file'''
1324 filename = self._actualfilename(tr)
1324 filename = self._actualfilename(tr)
1325 assert backupname != filename
1325 assert backupname != filename
1326
1326
1327 # use '_writedirstate' instead of 'write' to write changes certainly,
1327 # use '_writedirstate' instead of 'write' to write changes certainly,
1328 # because the latter omits writing out if transaction is running.
1328 # because the latter omits writing out if transaction is running.
1329 # output file will be used to create backup of dirstate at this point.
1329 # output file will be used to create backup of dirstate at this point.
1330 if self._dirty or not self._opener.exists(filename):
1330 if self._dirty or not self._opener.exists(filename):
1331 self._writedirstate(
1331 self._writedirstate(
1332 self._opener(filename, b"w", atomictemp=True, checkambig=True)
1332 self._opener(filename, b"w", atomictemp=True, checkambig=True)
1333 )
1333 )
1334
1334
1335 if tr:
1335 if tr:
1336 # ensure that subsequent tr.writepending returns True for
1336 # ensure that subsequent tr.writepending returns True for
1337 # changes written out above, even if dirstate is never
1337 # changes written out above, even if dirstate is never
1338 # changed after this
1338 # changed after this
1339 tr.addfilegenerator(
1339 tr.addfilegenerator(
1340 b'dirstate',
1340 b'dirstate',
1341 (self._filename,),
1341 (self._filename,),
1342 self._writedirstate,
1342 self._writedirstate,
1343 location=b'plain',
1343 location=b'plain',
1344 )
1344 )
1345
1345
1346 # ensure that pending file written above is unlinked at
1346 # ensure that pending file written above is unlinked at
1347 # failure, even if tr.writepending isn't invoked until the
1347 # failure, even if tr.writepending isn't invoked until the
1348 # end of this transaction
1348 # end of this transaction
1349 tr.registertmp(filename, location=b'plain')
1349 tr.registertmp(filename, location=b'plain')
1350
1350
1351 self._opener.tryunlink(backupname)
1351 self._opener.tryunlink(backupname)
1352 # hardlink backup is okay because _writedirstate is always called
1352 # hardlink backup is okay because _writedirstate is always called
1353 # with an "atomictemp=True" file.
1353 # with an "atomictemp=True" file.
1354 util.copyfile(
1354 util.copyfile(
1355 self._opener.join(filename),
1355 self._opener.join(filename),
1356 self._opener.join(backupname),
1356 self._opener.join(backupname),
1357 hardlink=True,
1357 hardlink=True,
1358 )
1358 )
1359
1359
1360 def restorebackup(self, tr, backupname):
1360 def restorebackup(self, tr, backupname):
1361 '''Restore dirstate by backup file'''
1361 '''Restore dirstate by backup file'''
1362 # this "invalidate()" prevents "wlock.release()" from writing
1362 # this "invalidate()" prevents "wlock.release()" from writing
1363 # changes of dirstate out after restoring from backup file
1363 # changes of dirstate out after restoring from backup file
1364 self.invalidate()
1364 self.invalidate()
1365 filename = self._actualfilename(tr)
1365 filename = self._actualfilename(tr)
1366 o = self._opener
1366 o = self._opener
1367 if util.samefile(o.join(backupname), o.join(filename)):
1367 if util.samefile(o.join(backupname), o.join(filename)):
1368 o.unlink(backupname)
1368 o.unlink(backupname)
1369 else:
1369 else:
1370 o.rename(backupname, filename, checkambig=True)
1370 o.rename(backupname, filename, checkambig=True)
1371
1371
1372 def clearbackup(self, tr, backupname):
1372 def clearbackup(self, tr, backupname):
1373 '''Clear backup file'''
1373 '''Clear backup file'''
1374 self._opener.unlink(backupname)
1374 self._opener.unlink(backupname)
1375
1375
1376
1376
1377 class dirstatemap(object):
1377 class dirstatemap(object):
1378 """Map encapsulating the dirstate's contents.
1378 """Map encapsulating the dirstate's contents.
1379
1379
1380 The dirstate contains the following state:
1380 The dirstate contains the following state:
1381
1381
1382 - `identity` is the identity of the dirstate file, which can be used to
1382 - `identity` is the identity of the dirstate file, which can be used to
1383 detect when changes have occurred to the dirstate file.
1383 detect when changes have occurred to the dirstate file.
1384
1384
1385 - `parents` is a pair containing the parents of the working copy. The
1385 - `parents` is a pair containing the parents of the working copy. The
1386 parents are updated by calling `setparents`.
1386 parents are updated by calling `setparents`.
1387
1387
1388 - the state map maps filenames to tuples of (state, mode, size, mtime),
1388 - the state map maps filenames to tuples of (state, mode, size, mtime),
1389 where state is a single character representing 'normal', 'added',
1389 where state is a single character representing 'normal', 'added',
1390 'removed', or 'merged'. It is read by treating the dirstate as a
1390 'removed', or 'merged'. It is read by treating the dirstate as a
1391 dict. File state is updated by calling the `addfile`, `removefile` and
1391 dict. File state is updated by calling the `addfile`, `removefile` and
1392 `dropfile` methods.
1392 `dropfile` methods.
1393
1393
1394 - `copymap` maps destination filenames to their source filename.
1394 - `copymap` maps destination filenames to their source filename.
1395
1395
1396 The dirstate also provides the following views onto the state:
1396 The dirstate also provides the following views onto the state:
1397
1397
1398 - `nonnormalset` is a set of the filenames that have state other
1398 - `nonnormalset` is a set of the filenames that have state other
1399 than 'normal', or are normal but have an mtime of -1 ('normallookup').
1399 than 'normal', or are normal but have an mtime of -1 ('normallookup').
1400
1400
1401 - `otherparentset` is a set of the filenames that are marked as coming
1401 - `otherparentset` is a set of the filenames that are marked as coming
1402 from the second parent when the dirstate is currently being merged.
1402 from the second parent when the dirstate is currently being merged.
1403
1403
1404 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
1404 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
1405 form that they appear as in the dirstate.
1405 form that they appear as in the dirstate.
1406
1406
1407 - `dirfoldmap` is a dict mapping normalized directory names to the
1407 - `dirfoldmap` is a dict mapping normalized directory names to the
1408 denormalized form that they appear as in the dirstate.
1408 denormalized form that they appear as in the dirstate.
1409 """
1409 """
1410
1410
1411 def __init__(self, ui, opener, root):
1411 def __init__(self, ui, opener, root):
1412 self._ui = ui
1412 self._ui = ui
1413 self._opener = opener
1413 self._opener = opener
1414 self._root = root
1414 self._root = root
1415 self._filename = b'dirstate'
1415 self._filename = b'dirstate'
1416
1416
1417 self._parents = None
1417 self._parents = None
1418 self._dirtyparents = False
1418 self._dirtyparents = False
1419
1419
1420 # for consistent view between _pl() and _read() invocations
1420 # for consistent view between _pl() and _read() invocations
1421 self._pendingmode = None
1421 self._pendingmode = None
1422
1422
1423 @propertycache
1423 @propertycache
1424 def _map(self):
1424 def _map(self):
1425 self._map = {}
1425 self._map = {}
1426 self.read()
1426 self.read()
1427 return self._map
1427 return self._map
1428
1428
1429 @propertycache
1429 @propertycache
1430 def copymap(self):
1430 def copymap(self):
1431 self.copymap = {}
1431 self.copymap = {}
1432 self._map
1432 self._map
1433 return self.copymap
1433 return self.copymap
1434
1434
1435 def clear(self):
1435 def clear(self):
1436 self._map.clear()
1436 self._map.clear()
1437 self.copymap.clear()
1437 self.copymap.clear()
1438 self.setparents(nullid, nullid)
1438 self.setparents(nullid, nullid)
1439 util.clearcachedproperty(self, b"_dirs")
1439 util.clearcachedproperty(self, b"_dirs")
1440 util.clearcachedproperty(self, b"_alldirs")
1440 util.clearcachedproperty(self, b"_alldirs")
1441 util.clearcachedproperty(self, b"filefoldmap")
1441 util.clearcachedproperty(self, b"filefoldmap")
1442 util.clearcachedproperty(self, b"dirfoldmap")
1442 util.clearcachedproperty(self, b"dirfoldmap")
1443 util.clearcachedproperty(self, b"nonnormalset")
1443 util.clearcachedproperty(self, b"nonnormalset")
1444 util.clearcachedproperty(self, b"otherparentset")
1444 util.clearcachedproperty(self, b"otherparentset")
1445
1445
1446 def items(self):
1446 def items(self):
1447 return pycompat.iteritems(self._map)
1447 return pycompat.iteritems(self._map)
1448
1448
1449 # forward for python2,3 compat
1449 # forward for python2,3 compat
1450 iteritems = items
1450 iteritems = items
1451
1451
1452 def __len__(self):
1452 def __len__(self):
1453 return len(self._map)
1453 return len(self._map)
1454
1454
1455 def __iter__(self):
1455 def __iter__(self):
1456 return iter(self._map)
1456 return iter(self._map)
1457
1457
1458 def get(self, key, default=None):
1458 def get(self, key, default=None):
1459 return self._map.get(key, default)
1459 return self._map.get(key, default)
1460
1460
1461 def __contains__(self, key):
1461 def __contains__(self, key):
1462 return key in self._map
1462 return key in self._map
1463
1463
1464 def __getitem__(self, key):
1464 def __getitem__(self, key):
1465 return self._map[key]
1465 return self._map[key]
1466
1466
1467 def keys(self):
1467 def keys(self):
1468 return self._map.keys()
1468 return self._map.keys()
1469
1469
1470 def preload(self):
1470 def preload(self):
1471 """Loads the underlying data, if it's not already loaded"""
1471 """Loads the underlying data, if it's not already loaded"""
1472 self._map
1472 self._map
1473
1473
1474 def addfile(self, f, oldstate, state, mode, size, mtime):
1474 def addfile(self, f, oldstate, state, mode, size, mtime):
1475 """Add a tracked file to the dirstate."""
1475 """Add a tracked file to the dirstate."""
1476 if oldstate in b"?r" and "_dirs" in self.__dict__:
1476 if oldstate in b"?r" and "_dirs" in self.__dict__:
1477 self._dirs.addpath(f)
1477 self._dirs.addpath(f)
1478 if oldstate == b"?" and "_alldirs" in self.__dict__:
1478 if oldstate == b"?" and "_alldirs" in self.__dict__:
1479 self._alldirs.addpath(f)
1479 self._alldirs.addpath(f)
1480 self._map[f] = dirstatetuple(state, mode, size, mtime)
1480 self._map[f] = dirstatetuple(state, mode, size, mtime)
1481 if state != b'n' or mtime == -1:
1481 if state != b'n' or mtime == -1:
1482 self.nonnormalset.add(f)
1482 self.nonnormalset.add(f)
1483 if size == -2:
1483 if size == -2:
1484 self.otherparentset.add(f)
1484 self.otherparentset.add(f)
1485
1485
1486 def removefile(self, f, oldstate, size):
1486 def removefile(self, f, oldstate, size):
1487 """
1487 """
1488 Mark a file as removed in the dirstate.
1488 Mark a file as removed in the dirstate.
1489
1489
1490 The `size` parameter is used to store sentinel values that indicate
1490 The `size` parameter is used to store sentinel values that indicate
1491 the file's previous state. In the future, we should refactor this
1491 the file's previous state. In the future, we should refactor this
1492 to be more explicit about what that state is.
1492 to be more explicit about what that state is.
1493 """
1493 """
1494 if oldstate not in b"?r" and "_dirs" in self.__dict__:
1494 if oldstate not in b"?r" and "_dirs" in self.__dict__:
1495 self._dirs.delpath(f)
1495 self._dirs.delpath(f)
1496 if oldstate == b"?" and "_alldirs" in self.__dict__:
1496 if oldstate == b"?" and "_alldirs" in self.__dict__:
1497 self._alldirs.addpath(f)
1497 self._alldirs.addpath(f)
1498 if "filefoldmap" in self.__dict__:
1498 if "filefoldmap" in self.__dict__:
1499 normed = util.normcase(f)
1499 normed = util.normcase(f)
1500 self.filefoldmap.pop(normed, None)
1500 self.filefoldmap.pop(normed, None)
1501 self._map[f] = dirstatetuple(b'r', 0, size, 0)
1501 self._map[f] = dirstatetuple(b'r', 0, size, 0)
1502 self.nonnormalset.add(f)
1502 self.nonnormalset.add(f)
1503
1503
1504 def dropfile(self, f, oldstate):
1504 def dropfile(self, f, oldstate):
1505 """
1505 """
1506 Remove a file from the dirstate. Returns True if the file was
1506 Remove a file from the dirstate. Returns True if the file was
1507 previously recorded.
1507 previously recorded.
1508 """
1508 """
1509 exists = self._map.pop(f, None) is not None
1509 exists = self._map.pop(f, None) is not None
1510 if exists:
1510 if exists:
1511 if oldstate != b"r" and "_dirs" in self.__dict__:
1511 if oldstate != b"r" and "_dirs" in self.__dict__:
1512 self._dirs.delpath(f)
1512 self._dirs.delpath(f)
1513 if "_alldirs" in self.__dict__:
1513 if "_alldirs" in self.__dict__:
1514 self._alldirs.delpath(f)
1514 self._alldirs.delpath(f)
1515 if "filefoldmap" in self.__dict__:
1515 if "filefoldmap" in self.__dict__:
1516 normed = util.normcase(f)
1516 normed = util.normcase(f)
1517 self.filefoldmap.pop(normed, None)
1517 self.filefoldmap.pop(normed, None)
1518 self.nonnormalset.discard(f)
1518 self.nonnormalset.discard(f)
1519 return exists
1519 return exists
1520
1520
1521 def clearambiguoustimes(self, files, now):
1521 def clearambiguoustimes(self, files, now):
1522 for f in files:
1522 for f in files:
1523 e = self.get(f)
1523 e = self.get(f)
1524 if e is not None and e[0] == b'n' and e[3] == now:
1524 if e is not None and e[0] == b'n' and e[3] == now:
1525 self._map[f] = dirstatetuple(e[0], e[1], e[2], -1)
1525 self._map[f] = dirstatetuple(e[0], e[1], e[2], -1)
1526 self.nonnormalset.add(f)
1526 self.nonnormalset.add(f)
1527
1527
1528 def nonnormalentries(self):
1528 def nonnormalentries(self):
1529 '''Compute the nonnormal dirstate entries from the dmap'''
1529 '''Compute the nonnormal dirstate entries from the dmap'''
1530 try:
1530 try:
1531 return parsers.nonnormalotherparententries(self._map)
1531 return parsers.nonnormalotherparententries(self._map)
1532 except AttributeError:
1532 except AttributeError:
1533 nonnorm = set()
1533 nonnorm = set()
1534 otherparent = set()
1534 otherparent = set()
1535 for fname, e in pycompat.iteritems(self._map):
1535 for fname, e in pycompat.iteritems(self._map):
1536 if e[0] != b'n' or e[3] == -1:
1536 if e[0] != b'n' or e[3] == -1:
1537 nonnorm.add(fname)
1537 nonnorm.add(fname)
1538 if e[0] == b'n' and e[2] == -2:
1538 if e[0] == b'n' and e[2] == -2:
1539 otherparent.add(fname)
1539 otherparent.add(fname)
1540 return nonnorm, otherparent
1540 return nonnorm, otherparent
1541
1541
1542 @propertycache
1542 @propertycache
1543 def filefoldmap(self):
1543 def filefoldmap(self):
1544 """Returns a dictionary mapping normalized case paths to their
1544 """Returns a dictionary mapping normalized case paths to their
1545 non-normalized versions.
1545 non-normalized versions.
1546 """
1546 """
1547 try:
1547 try:
1548 makefilefoldmap = parsers.make_file_foldmap
1548 makefilefoldmap = parsers.make_file_foldmap
1549 except AttributeError:
1549 except AttributeError:
1550 pass
1550 pass
1551 else:
1551 else:
1552 return makefilefoldmap(
1552 return makefilefoldmap(
1553 self._map, util.normcasespec, util.normcasefallback
1553 self._map, util.normcasespec, util.normcasefallback
1554 )
1554 )
1555
1555
1556 f = {}
1556 f = {}
1557 normcase = util.normcase
1557 normcase = util.normcase
1558 for name, s in pycompat.iteritems(self._map):
1558 for name, s in pycompat.iteritems(self._map):
1559 if s[0] != b'r':
1559 if s[0] != b'r':
1560 f[normcase(name)] = name
1560 f[normcase(name)] = name
1561 f[b'.'] = b'.' # prevents useless util.fspath() invocation
1561 f[b'.'] = b'.' # prevents useless util.fspath() invocation
1562 return f
1562 return f
1563
1563
1564 def hastrackeddir(self, d):
1564 def hastrackeddir(self, d):
1565 """
1565 """
1566 Returns True if the dirstate contains a tracked (not removed) file
1566 Returns True if the dirstate contains a tracked (not removed) file
1567 in this directory.
1567 in this directory.
1568 """
1568 """
1569 return d in self._dirs
1569 return d in self._dirs
1570
1570
1571 def hasdir(self, d):
1571 def hasdir(self, d):
1572 """
1572 """
1573 Returns True if the dirstate contains a file (tracked or removed)
1573 Returns True if the dirstate contains a file (tracked or removed)
1574 in this directory.
1574 in this directory.
1575 """
1575 """
1576 return d in self._alldirs
1576 return d in self._alldirs
1577
1577
1578 @propertycache
1578 @propertycache
1579 def _dirs(self):
1579 def _dirs(self):
1580 return pathutil.dirs(self._map, b'r')
1580 return pathutil.dirs(self._map, b'r')
1581
1581
1582 @propertycache
1582 @propertycache
1583 def _alldirs(self):
1583 def _alldirs(self):
1584 return pathutil.dirs(self._map)
1584 return pathutil.dirs(self._map)
1585
1585
1586 def _opendirstatefile(self):
1586 def _opendirstatefile(self):
1587 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
1587 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
1588 if self._pendingmode is not None and self._pendingmode != mode:
1588 if self._pendingmode is not None and self._pendingmode != mode:
1589 fp.close()
1589 fp.close()
1590 raise error.Abort(
1590 raise error.Abort(
1591 _(b'working directory state may be changed parallelly')
1591 _(b'working directory state may be changed parallelly')
1592 )
1592 )
1593 self._pendingmode = mode
1593 self._pendingmode = mode
1594 return fp
1594 return fp
1595
1595
1596 def parents(self):
1596 def parents(self):
1597 if not self._parents:
1597 if not self._parents:
1598 try:
1598 try:
1599 fp = self._opendirstatefile()
1599 fp = self._opendirstatefile()
1600 st = fp.read(40)
1600 st = fp.read(40)
1601 fp.close()
1601 fp.close()
1602 except IOError as err:
1602 except IOError as err:
1603 if err.errno != errno.ENOENT:
1603 if err.errno != errno.ENOENT:
1604 raise
1604 raise
1605 # File doesn't exist, so the current state is empty
1605 # File doesn't exist, so the current state is empty
1606 st = b''
1606 st = b''
1607
1607
1608 l = len(st)
1608 l = len(st)
1609 if l == 40:
1609 if l == 40:
1610 self._parents = (st[:20], st[20:40])
1610 self._parents = (st[:20], st[20:40])
1611 elif l == 0:
1611 elif l == 0:
1612 self._parents = (nullid, nullid)
1612 self._parents = (nullid, nullid)
1613 else:
1613 else:
1614 raise error.Abort(
1614 raise error.Abort(
1615 _(b'working directory state appears damaged!')
1615 _(b'working directory state appears damaged!')
1616 )
1616 )
1617
1617
1618 return self._parents
1618 return self._parents
1619
1619
1620 def setparents(self, p1, p2):
1620 def setparents(self, p1, p2):
1621 self._parents = (p1, p2)
1621 self._parents = (p1, p2)
1622 self._dirtyparents = True
1622 self._dirtyparents = True
1623
1623
1624 def read(self):
1624 def read(self):
1625 # ignore HG_PENDING because identity is used only for writing
1625 # ignore HG_PENDING because identity is used only for writing
1626 self.identity = util.filestat.frompath(
1626 self.identity = util.filestat.frompath(
1627 self._opener.join(self._filename)
1627 self._opener.join(self._filename)
1628 )
1628 )
1629
1629
1630 try:
1630 try:
1631 fp = self._opendirstatefile()
1631 fp = self._opendirstatefile()
1632 try:
1632 try:
1633 st = fp.read()
1633 st = fp.read()
1634 finally:
1634 finally:
1635 fp.close()
1635 fp.close()
1636 except IOError as err:
1636 except IOError as err:
1637 if err.errno != errno.ENOENT:
1637 if err.errno != errno.ENOENT:
1638 raise
1638 raise
1639 return
1639 return
1640 if not st:
1640 if not st:
1641 return
1641 return
1642
1642
1643 if util.safehasattr(parsers, b'dict_new_presized'):
1643 if util.safehasattr(parsers, b'dict_new_presized'):
1644 # Make an estimate of the number of files in the dirstate based on
1644 # Make an estimate of the number of files in the dirstate based on
1645 # its size. From a linear regression on a set of real-world repos,
1645 # its size. From a linear regression on a set of real-world repos,
1646 # all over 10,000 files, the size of a dirstate entry is 85
1646 # all over 10,000 files, the size of a dirstate entry is 85
1647 # bytes. The cost of resizing is significantly higher than the cost
1647 # bytes. The cost of resizing is significantly higher than the cost
1648 # of filling in a larger presized dict, so subtract 20% from the
1648 # of filling in a larger presized dict, so subtract 20% from the
1649 # size.
1649 # size.
1650 #
1650 #
1651 # This heuristic is imperfect in many ways, so in a future dirstate
1651 # This heuristic is imperfect in many ways, so in a future dirstate
1652 # format update it makes sense to just record the number of entries
1652 # format update it makes sense to just record the number of entries
1653 # on write.
1653 # on write.
1654 self._map = parsers.dict_new_presized(len(st) // 71)
1654 self._map = parsers.dict_new_presized(len(st) // 71)
1655
1655
1656 # Python's garbage collector triggers a GC each time a certain number
1656 # Python's garbage collector triggers a GC each time a certain number
1657 # of container objects (the number being defined by
1657 # of container objects (the number being defined by
1658 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
1658 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
1659 # for each file in the dirstate. The C version then immediately marks
1659 # for each file in the dirstate. The C version then immediately marks
1660 # them as not to be tracked by the collector. However, this has no
1660 # them as not to be tracked by the collector. However, this has no
1661 # effect on when GCs are triggered, only on what objects the GC looks
1661 # effect on when GCs are triggered, only on what objects the GC looks
1662 # into. This means that O(number of files) GCs are unavoidable.
1662 # into. This means that O(number of files) GCs are unavoidable.
1663 # Depending on when in the process's lifetime the dirstate is parsed,
1663 # Depending on when in the process's lifetime the dirstate is parsed,
1664 # this can get very expensive. As a workaround, disable GC while
1664 # this can get very expensive. As a workaround, disable GC while
1665 # parsing the dirstate.
1665 # parsing the dirstate.
1666 #
1666 #
1667 # (we cannot decorate the function directly since it is in a C module)
1667 # (we cannot decorate the function directly since it is in a C module)
1668 parse_dirstate = util.nogc(parsers.parse_dirstate)
1668 parse_dirstate = util.nogc(parsers.parse_dirstate)
1669 p = parse_dirstate(self._map, self.copymap, st)
1669 p = parse_dirstate(self._map, self.copymap, st)
1670 if not self._dirtyparents:
1670 if not self._dirtyparents:
1671 self.setparents(*p)
1671 self.setparents(*p)
1672
1672
1673 # Avoid excess attribute lookups by fast pathing certain checks
1673 # Avoid excess attribute lookups by fast pathing certain checks
1674 self.__contains__ = self._map.__contains__
1674 self.__contains__ = self._map.__contains__
1675 self.__getitem__ = self._map.__getitem__
1675 self.__getitem__ = self._map.__getitem__
1676 self.get = self._map.get
1676 self.get = self._map.get
1677
1677
1678 def write(self, st, now):
1678 def write(self, st, now):
1679 st.write(
1679 st.write(
1680 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
1680 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
1681 )
1681 )
1682 st.close()
1682 st.close()
1683 self._dirtyparents = False
1683 self._dirtyparents = False
1684 self.nonnormalset, self.otherparentset = self.nonnormalentries()
1684 self.nonnormalset, self.otherparentset = self.nonnormalentries()
1685
1685
1686 @propertycache
1686 @propertycache
1687 def nonnormalset(self):
1687 def nonnormalset(self):
1688 nonnorm, otherparents = self.nonnormalentries()
1688 nonnorm, otherparents = self.nonnormalentries()
1689 self.otherparentset = otherparents
1689 self.otherparentset = otherparents
1690 return nonnorm
1690 return nonnorm
1691
1691
1692 @propertycache
1692 @propertycache
1693 def otherparentset(self):
1693 def otherparentset(self):
1694 nonnorm, otherparents = self.nonnormalentries()
1694 nonnorm, otherparents = self.nonnormalentries()
1695 self.nonnormalset = nonnorm
1695 self.nonnormalset = nonnorm
1696 return otherparents
1696 return otherparents
1697
1697
1698 @propertycache
1698 @propertycache
1699 def identity(self):
1699 def identity(self):
1700 self._map
1700 self._map
1701 return self.identity
1701 return self.identity
1702
1702
1703 @propertycache
1703 @propertycache
1704 def dirfoldmap(self):
1704 def dirfoldmap(self):
1705 f = {}
1705 f = {}
1706 normcase = util.normcase
1706 normcase = util.normcase
1707 for name in self._dirs:
1707 for name in self._dirs:
1708 f[normcase(name)] = name
1708 f[normcase(name)] = name
1709 return f
1709 return f
1710
1710
1711
1711
1712 if rustmod is not None:
1712 if rustmod is not None:
1713
1713
1714 class dirstatemap(object):
1714 class dirstatemap(object):
1715 def __init__(self, ui, opener, root):
1715 def __init__(self, ui, opener, root):
1716 self._ui = ui
1716 self._ui = ui
1717 self._opener = opener
1717 self._opener = opener
1718 self._root = root
1718 self._root = root
1719 self._filename = b'dirstate'
1719 self._filename = b'dirstate'
1720 self._parents = None
1720 self._parents = None
1721 self._dirtyparents = False
1721 self._dirtyparents = False
1722
1722
1723 # for consistent view between _pl() and _read() invocations
1723 # for consistent view between _pl() and _read() invocations
1724 self._pendingmode = None
1724 self._pendingmode = None
1725
1725
1726 def addfile(self, *args, **kwargs):
1726 def addfile(self, *args, **kwargs):
1727 return self._rustmap.addfile(*args, **kwargs)
1727 return self._rustmap.addfile(*args, **kwargs)
1728
1728
1729 def removefile(self, *args, **kwargs):
1729 def removefile(self, *args, **kwargs):
1730 return self._rustmap.removefile(*args, **kwargs)
1730 return self._rustmap.removefile(*args, **kwargs)
1731
1731
1732 def dropfile(self, *args, **kwargs):
1732 def dropfile(self, *args, **kwargs):
1733 return self._rustmap.dropfile(*args, **kwargs)
1733 return self._rustmap.dropfile(*args, **kwargs)
1734
1734
1735 def clearambiguoustimes(self, *args, **kwargs):
1735 def clearambiguoustimes(self, *args, **kwargs):
1736 return self._rustmap.clearambiguoustimes(*args, **kwargs)
1736 return self._rustmap.clearambiguoustimes(*args, **kwargs)
1737
1737
1738 def nonnormalentries(self):
1738 def nonnormalentries(self):
1739 return self._rustmap.nonnormalentries()
1739 return self._rustmap.nonnormalentries()
1740
1740
1741 def get(self, *args, **kwargs):
1741 def get(self, *args, **kwargs):
1742 return self._rustmap.get(*args, **kwargs)
1742 return self._rustmap.get(*args, **kwargs)
1743
1743
1744 @propertycache
1744 @propertycache
1745 def _rustmap(self):
1745 def _rustmap(self):
1746 self._rustmap = rustmod.DirstateMap(self._root)
1746 """
1747 Fills the Dirstatemap when called.
1748 Use `self._inner_rustmap` if reading the dirstate is not necessary.
1749 """
1750 self._rustmap = self._inner_rustmap
1747 self.read()
1751 self.read()
1748 return self._rustmap
1752 return self._rustmap
1749
1753
1754 @propertycache
1755 def _inner_rustmap(self):
1756 """
1757 Does not fill the Dirstatemap when called. This allows for
1758 optimizations where only setting/getting the parents is needed.
1759 """
1760 self._inner_rustmap = rustmod.DirstateMap(self._root)
1761 return self._inner_rustmap
1762
1750 @property
1763 @property
1751 def copymap(self):
1764 def copymap(self):
1752 return self._rustmap.copymap()
1765 return self._rustmap.copymap()
1753
1766
1754 def preload(self):
1767 def preload(self):
1755 self._rustmap
1768 self._rustmap
1756
1769
1757 def clear(self):
1770 def clear(self):
1758 self._rustmap.clear()
1771 self._rustmap.clear()
1772 self._inner_rustmap.clear()
1759 self.setparents(nullid, nullid)
1773 self.setparents(nullid, nullid)
1760 util.clearcachedproperty(self, b"_dirs")
1774 util.clearcachedproperty(self, b"_dirs")
1761 util.clearcachedproperty(self, b"_alldirs")
1775 util.clearcachedproperty(self, b"_alldirs")
1762 util.clearcachedproperty(self, b"dirfoldmap")
1776 util.clearcachedproperty(self, b"dirfoldmap")
1763
1777
1764 def items(self):
1778 def items(self):
1765 return self._rustmap.items()
1779 return self._rustmap.items()
1766
1780
1767 def keys(self):
1781 def keys(self):
1768 return iter(self._rustmap)
1782 return iter(self._rustmap)
1769
1783
1770 def __contains__(self, key):
1784 def __contains__(self, key):
1771 return key in self._rustmap
1785 return key in self._rustmap
1772
1786
1773 def __getitem__(self, item):
1787 def __getitem__(self, item):
1774 return self._rustmap[item]
1788 return self._rustmap[item]
1775
1789
1776 def __len__(self):
1790 def __len__(self):
1777 return len(self._rustmap)
1791 return len(self._rustmap)
1778
1792
1779 def __iter__(self):
1793 def __iter__(self):
1780 return iter(self._rustmap)
1794 return iter(self._rustmap)
1781
1795
1782 # forward for python2,3 compat
1796 # forward for python2,3 compat
1783 iteritems = items
1797 iteritems = items
1784
1798
1785 def _opendirstatefile(self):
1799 def _opendirstatefile(self):
1786 fp, mode = txnutil.trypending(
1800 fp, mode = txnutil.trypending(
1787 self._root, self._opener, self._filename
1801 self._root, self._opener, self._filename
1788 )
1802 )
1789 if self._pendingmode is not None and self._pendingmode != mode:
1803 if self._pendingmode is not None and self._pendingmode != mode:
1790 fp.close()
1804 fp.close()
1791 raise error.Abort(
1805 raise error.Abort(
1792 _(b'working directory state may be changed parallelly')
1806 _(b'working directory state may be changed parallelly')
1793 )
1807 )
1794 self._pendingmode = mode
1808 self._pendingmode = mode
1795 return fp
1809 return fp
1796
1810
1797 def setparents(self, p1, p2):
1811 def setparents(self, p1, p2):
1798 self._rustmap.setparents(p1, p2)
1812 self._rustmap.setparents(p1, p2)
1799 self._parents = (p1, p2)
1813 self._parents = (p1, p2)
1800 self._dirtyparents = True
1814 self._dirtyparents = True
1801
1815
1802 def parents(self):
1816 def parents(self):
1803 if not self._parents:
1817 if not self._parents:
1804 try:
1818 try:
1805 fp = self._opendirstatefile()
1819 fp = self._opendirstatefile()
1806 st = fp.read(40)
1820 st = fp.read(40)
1807 fp.close()
1821 fp.close()
1808 except IOError as err:
1822 except IOError as err:
1809 if err.errno != errno.ENOENT:
1823 if err.errno != errno.ENOENT:
1810 raise
1824 raise
1811 # File doesn't exist, so the current state is empty
1825 # File doesn't exist, so the current state is empty
1812 st = b''
1826 st = b''
1813
1827
1814 try:
1828 try:
1815 self._parents = self._rustmap.parents(st)
1829 self._parents = self._inner_rustmap.parents(st)
1816 except ValueError:
1830 except ValueError:
1817 raise error.Abort(
1831 raise error.Abort(
1818 _(b'working directory state appears damaged!')
1832 _(b'working directory state appears damaged!')
1819 )
1833 )
1820
1834
1821 return self._parents
1835 return self._parents
1822
1836
1823 def read(self):
1837 def read(self):
1824 # ignore HG_PENDING because identity is used only for writing
1838 # ignore HG_PENDING because identity is used only for writing
1825 self.identity = util.filestat.frompath(
1839 self.identity = util.filestat.frompath(
1826 self._opener.join(self._filename)
1840 self._opener.join(self._filename)
1827 )
1841 )
1828
1842
1829 try:
1843 try:
1830 fp = self._opendirstatefile()
1844 fp = self._opendirstatefile()
1831 try:
1845 try:
1832 st = fp.read()
1846 st = fp.read()
1833 finally:
1847 finally:
1834 fp.close()
1848 fp.close()
1835 except IOError as err:
1849 except IOError as err:
1836 if err.errno != errno.ENOENT:
1850 if err.errno != errno.ENOENT:
1837 raise
1851 raise
1838 return
1852 return
1839 if not st:
1853 if not st:
1840 return
1854 return
1841
1855
1842 parse_dirstate = util.nogc(self._rustmap.read)
1856 parse_dirstate = util.nogc(self._rustmap.read)
1843 parents = parse_dirstate(st)
1857 parents = parse_dirstate(st)
1844 if parents and not self._dirtyparents:
1858 if parents and not self._dirtyparents:
1845 self.setparents(*parents)
1859 self.setparents(*parents)
1846
1860
1847 self.__contains__ = self._rustmap.__contains__
1861 self.__contains__ = self._rustmap.__contains__
1848 self.__getitem__ = self._rustmap.__getitem__
1862 self.__getitem__ = self._rustmap.__getitem__
1849 self.get = self._rustmap.get
1863 self.get = self._rustmap.get
1850
1864
1851 def write(self, st, now):
1865 def write(self, st, now):
1852 parents = self.parents()
1866 parents = self.parents()
1853 st.write(self._rustmap.write(parents[0], parents[1], now))
1867 st.write(self._rustmap.write(parents[0], parents[1], now))
1854 st.close()
1868 st.close()
1855 self._dirtyparents = False
1869 self._dirtyparents = False
1856
1870
1857 @propertycache
1871 @propertycache
1858 def filefoldmap(self):
1872 def filefoldmap(self):
1859 """Returns a dictionary mapping normalized case paths to their
1873 """Returns a dictionary mapping normalized case paths to their
1860 non-normalized versions.
1874 non-normalized versions.
1861 """
1875 """
1862 return self._rustmap.filefoldmapasdict()
1876 return self._rustmap.filefoldmapasdict()
1863
1877
1864 def hastrackeddir(self, d):
1878 def hastrackeddir(self, d):
1865 self._dirs # Trigger Python's propertycache
1879 self._dirs # Trigger Python's propertycache
1866 return self._rustmap.hastrackeddir(d)
1880 return self._rustmap.hastrackeddir(d)
1867
1881
1868 def hasdir(self, d):
1882 def hasdir(self, d):
1869 self._dirs # Trigger Python's propertycache
1883 self._dirs # Trigger Python's propertycache
1870 return self._rustmap.hasdir(d)
1884 return self._rustmap.hasdir(d)
1871
1885
1872 @propertycache
1886 @propertycache
1873 def _dirs(self):
1887 def _dirs(self):
1874 return self._rustmap.getdirs()
1888 return self._rustmap.getdirs()
1875
1889
1876 @propertycache
1890 @propertycache
1877 def _alldirs(self):
1891 def _alldirs(self):
1878 return self._rustmap.getalldirs()
1892 return self._rustmap.getalldirs()
1879
1893
1880 @propertycache
1894 @propertycache
1881 def identity(self):
1895 def identity(self):
1882 self._rustmap
1896 self._rustmap
1883 return self.identity
1897 return self.identity
1884
1898
1885 @property
1899 @property
1886 def nonnormalset(self):
1900 def nonnormalset(self):
1887 nonnorm = self._rustmap.non_normal_entries()
1901 nonnorm = self._rustmap.non_normal_entries()
1888 return nonnorm
1902 return nonnorm
1889
1903
1890 @propertycache
1904 @propertycache
1891 def otherparentset(self):
1905 def otherparentset(self):
1892 otherparents = self._rustmap.other_parent_entries()
1906 otherparents = self._rustmap.other_parent_entries()
1893 return otherparents
1907 return otherparents
1894
1908
1895 @propertycache
1909 @propertycache
1896 def dirfoldmap(self):
1910 def dirfoldmap(self):
1897 f = {}
1911 f = {}
1898 normcase = util.normcase
1912 normcase = util.normcase
1899 for name in self._dirs:
1913 for name in self._dirs:
1900 f[normcase(name)] = name
1914 f[normcase(name)] = name
1901 return f
1915 return f
General Comments 0
You need to be logged in to leave comments. Login now