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