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