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