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