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