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