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