##// END OF EJS Templates
rust-dirstate: add `unionmatcher` to the allowed matchers...
Raphaël Gomès -
r50244:0043c7aa default
parent child Browse files
Show More
@@ -1,1467 +1,1468 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
9 9 import collections
10 10 import contextlib
11 11 import os
12 12 import stat
13 13 import uuid
14 14
15 15 from .i18n import _
16 16 from .pycompat import delattr
17 17
18 18 from hgdemandimport import tracing
19 19
20 20 from . import (
21 21 dirstatemap,
22 22 encoding,
23 23 error,
24 24 match as matchmod,
25 25 node,
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 .dirstateutils import (
35 35 timestamp,
36 36 )
37 37
38 38 from .interfaces import (
39 39 dirstate as intdirstate,
40 40 util as interfaceutil,
41 41 )
42 42
43 43 parsers = policy.importmod('parsers')
44 44 rustmod = policy.importrust('dirstate')
45 45
46 46 HAS_FAST_DIRSTATE_V2 = rustmod is not None
47 47
48 48 propertycache = util.propertycache
49 49 filecache = scmutil.filecache
50 50 _rangemask = dirstatemap.rangemask
51 51
52 52 DirstateItem = dirstatemap.DirstateItem
53 53
54 54
55 55 class repocache(filecache):
56 56 """filecache for files in .hg/"""
57 57
58 58 def join(self, obj, fname):
59 59 return obj._opener.join(fname)
60 60
61 61
62 62 class rootcache(filecache):
63 63 """filecache for files in the repository root"""
64 64
65 65 def join(self, obj, fname):
66 66 return obj._join(fname)
67 67
68 68
69 69 def requires_parents_change(func):
70 70 def wrap(self, *args, **kwargs):
71 71 if not self.pendingparentchange():
72 72 msg = 'calling `%s` outside of a parentchange context'
73 73 msg %= func.__name__
74 74 raise error.ProgrammingError(msg)
75 75 return func(self, *args, **kwargs)
76 76
77 77 return wrap
78 78
79 79
80 80 def requires_no_parents_change(func):
81 81 def wrap(self, *args, **kwargs):
82 82 if self.pendingparentchange():
83 83 msg = 'calling `%s` inside of a parentchange context'
84 84 msg %= func.__name__
85 85 raise error.ProgrammingError(msg)
86 86 return func(self, *args, **kwargs)
87 87
88 88 return wrap
89 89
90 90
91 91 @interfaceutil.implementer(intdirstate.idirstate)
92 92 class dirstate:
93 93 def __init__(
94 94 self,
95 95 opener,
96 96 ui,
97 97 root,
98 98 validate,
99 99 sparsematchfn,
100 100 nodeconstants,
101 101 use_dirstate_v2,
102 102 use_tracked_hint=False,
103 103 ):
104 104 """Create a new dirstate object.
105 105
106 106 opener is an open()-like callable that can be used to open the
107 107 dirstate file; root is the root of the directory tracked by
108 108 the dirstate.
109 109 """
110 110 self._use_dirstate_v2 = use_dirstate_v2
111 111 self._use_tracked_hint = use_tracked_hint
112 112 self._nodeconstants = nodeconstants
113 113 self._opener = opener
114 114 self._validate = validate
115 115 self._root = root
116 116 self._sparsematchfn = sparsematchfn
117 117 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
118 118 # UNC path pointing to root share (issue4557)
119 119 self._rootdir = pathutil.normasprefix(root)
120 120 # True is any internal state may be different
121 121 self._dirty = False
122 122 # True if the set of tracked file may be different
123 123 self._dirty_tracked_set = False
124 124 self._ui = ui
125 125 self._filecache = {}
126 126 self._parentwriters = 0
127 127 self._filename = b'dirstate'
128 128 self._filename_th = b'dirstate-tracked-hint'
129 129 self._pendingfilename = b'%s.pending' % self._filename
130 130 self._plchangecallbacks = {}
131 131 self._origpl = None
132 132 self._mapcls = dirstatemap.dirstatemap
133 133 # Access and cache cwd early, so we don't access it for the first time
134 134 # after a working-copy update caused it to not exist (accessing it then
135 135 # raises an exception).
136 136 self._cwd
137 137
138 138 def prefetch_parents(self):
139 139 """make sure the parents are loaded
140 140
141 141 Used to avoid a race condition.
142 142 """
143 143 self._pl
144 144
145 145 @contextlib.contextmanager
146 146 def parentchange(self):
147 147 """Context manager for handling dirstate parents.
148 148
149 149 If an exception occurs in the scope of the context manager,
150 150 the incoherent dirstate won't be written when wlock is
151 151 released.
152 152 """
153 153 self._parentwriters += 1
154 154 yield
155 155 # Typically we want the "undo" step of a context manager in a
156 156 # finally block so it happens even when an exception
157 157 # occurs. In this case, however, we only want to decrement
158 158 # parentwriters if the code in the with statement exits
159 159 # normally, so we don't have a try/finally here on purpose.
160 160 self._parentwriters -= 1
161 161
162 162 def pendingparentchange(self):
163 163 """Returns true if the dirstate is in the middle of a set of changes
164 164 that modify the dirstate parent.
165 165 """
166 166 return self._parentwriters > 0
167 167
168 168 @propertycache
169 169 def _map(self):
170 170 """Return the dirstate contents (see documentation for dirstatemap)."""
171 171 self._map = self._mapcls(
172 172 self._ui,
173 173 self._opener,
174 174 self._root,
175 175 self._nodeconstants,
176 176 self._use_dirstate_v2,
177 177 )
178 178 return self._map
179 179
180 180 @property
181 181 def _sparsematcher(self):
182 182 """The matcher for the sparse checkout.
183 183
184 184 The working directory may not include every file from a manifest. The
185 185 matcher obtained by this property will match a path if it is to be
186 186 included in the working directory.
187 187 """
188 188 # TODO there is potential to cache this property. For now, the matcher
189 189 # is resolved on every access. (But the called function does use a
190 190 # cache to keep the lookup fast.)
191 191 return self._sparsematchfn()
192 192
193 193 @repocache(b'branch')
194 194 def _branch(self):
195 195 try:
196 196 return self._opener.read(b"branch").strip() or b"default"
197 197 except FileNotFoundError:
198 198 return b"default"
199 199
200 200 @property
201 201 def _pl(self):
202 202 return self._map.parents()
203 203
204 204 def hasdir(self, d):
205 205 return self._map.hastrackeddir(d)
206 206
207 207 @rootcache(b'.hgignore')
208 208 def _ignore(self):
209 209 files = self._ignorefiles()
210 210 if not files:
211 211 return matchmod.never()
212 212
213 213 pats = [b'include:%s' % f for f in files]
214 214 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
215 215
216 216 @propertycache
217 217 def _slash(self):
218 218 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
219 219
220 220 @propertycache
221 221 def _checklink(self):
222 222 return util.checklink(self._root)
223 223
224 224 @propertycache
225 225 def _checkexec(self):
226 226 return bool(util.checkexec(self._root))
227 227
228 228 @propertycache
229 229 def _checkcase(self):
230 230 return not util.fscasesensitive(self._join(b'.hg'))
231 231
232 232 def _join(self, f):
233 233 # much faster than os.path.join()
234 234 # it's safe because f is always a relative path
235 235 return self._rootdir + f
236 236
237 237 def flagfunc(self, buildfallback):
238 238 """build a callable that returns flags associated with a filename
239 239
240 240 The information is extracted from three possible layers:
241 241 1. the file system if it supports the information
242 242 2. the "fallback" information stored in the dirstate if any
243 243 3. a more expensive mechanism inferring the flags from the parents.
244 244 """
245 245
246 246 # small hack to cache the result of buildfallback()
247 247 fallback_func = []
248 248
249 249 def get_flags(x):
250 250 entry = None
251 251 fallback_value = None
252 252 try:
253 253 st = os.lstat(self._join(x))
254 254 except OSError:
255 255 return b''
256 256
257 257 if self._checklink:
258 258 if util.statislink(st):
259 259 return b'l'
260 260 else:
261 261 entry = self.get_entry(x)
262 262 if entry.has_fallback_symlink:
263 263 if entry.fallback_symlink:
264 264 return b'l'
265 265 else:
266 266 if not fallback_func:
267 267 fallback_func.append(buildfallback())
268 268 fallback_value = fallback_func[0](x)
269 269 if b'l' in fallback_value:
270 270 return b'l'
271 271
272 272 if self._checkexec:
273 273 if util.statisexec(st):
274 274 return b'x'
275 275 else:
276 276 if entry is None:
277 277 entry = self.get_entry(x)
278 278 if entry.has_fallback_exec:
279 279 if entry.fallback_exec:
280 280 return b'x'
281 281 else:
282 282 if fallback_value is None:
283 283 if not fallback_func:
284 284 fallback_func.append(buildfallback())
285 285 fallback_value = fallback_func[0](x)
286 286 if b'x' in fallback_value:
287 287 return b'x'
288 288 return b''
289 289
290 290 return get_flags
291 291
292 292 @propertycache
293 293 def _cwd(self):
294 294 # internal config: ui.forcecwd
295 295 forcecwd = self._ui.config(b'ui', b'forcecwd')
296 296 if forcecwd:
297 297 return forcecwd
298 298 return encoding.getcwd()
299 299
300 300 def getcwd(self):
301 301 """Return the path from which a canonical path is calculated.
302 302
303 303 This path should be used to resolve file patterns or to convert
304 304 canonical paths back to file paths for display. It shouldn't be
305 305 used to get real file paths. Use vfs functions instead.
306 306 """
307 307 cwd = self._cwd
308 308 if cwd == self._root:
309 309 return b''
310 310 # self._root ends with a path separator if self._root is '/' or 'C:\'
311 311 rootsep = self._root
312 312 if not util.endswithsep(rootsep):
313 313 rootsep += pycompat.ossep
314 314 if cwd.startswith(rootsep):
315 315 return cwd[len(rootsep) :]
316 316 else:
317 317 # we're outside the repo. return an absolute path.
318 318 return cwd
319 319
320 320 def pathto(self, f, cwd=None):
321 321 if cwd is None:
322 322 cwd = self.getcwd()
323 323 path = util.pathto(self._root, cwd, f)
324 324 if self._slash:
325 325 return util.pconvert(path)
326 326 return path
327 327
328 328 def get_entry(self, path):
329 329 """return a DirstateItem for the associated path"""
330 330 entry = self._map.get(path)
331 331 if entry is None:
332 332 return DirstateItem()
333 333 return entry
334 334
335 335 def __contains__(self, key):
336 336 return key in self._map
337 337
338 338 def __iter__(self):
339 339 return iter(sorted(self._map))
340 340
341 341 def items(self):
342 342 return self._map.items()
343 343
344 344 iteritems = items
345 345
346 346 def parents(self):
347 347 return [self._validate(p) for p in self._pl]
348 348
349 349 def p1(self):
350 350 return self._validate(self._pl[0])
351 351
352 352 def p2(self):
353 353 return self._validate(self._pl[1])
354 354
355 355 @property
356 356 def in_merge(self):
357 357 """True if a merge is in progress"""
358 358 return self._pl[1] != self._nodeconstants.nullid
359 359
360 360 def branch(self):
361 361 return encoding.tolocal(self._branch)
362 362
363 363 def setparents(self, p1, p2=None):
364 364 """Set dirstate parents to p1 and p2.
365 365
366 366 When moving from two parents to one, "merged" entries a
367 367 adjusted to normal and previous copy records discarded and
368 368 returned by the call.
369 369
370 370 See localrepo.setparents()
371 371 """
372 372 if p2 is None:
373 373 p2 = self._nodeconstants.nullid
374 374 if self._parentwriters == 0:
375 375 raise ValueError(
376 376 b"cannot set dirstate parent outside of "
377 377 b"dirstate.parentchange context manager"
378 378 )
379 379
380 380 self._dirty = True
381 381 oldp2 = self._pl[1]
382 382 if self._origpl is None:
383 383 self._origpl = self._pl
384 384 nullid = self._nodeconstants.nullid
385 385 # True if we need to fold p2 related state back to a linear case
386 386 fold_p2 = oldp2 != nullid and p2 == nullid
387 387 return self._map.setparents(p1, p2, fold_p2=fold_p2)
388 388
389 389 def setbranch(self, branch):
390 390 self.__class__._branch.set(self, encoding.fromlocal(branch))
391 391 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
392 392 try:
393 393 f.write(self._branch + b'\n')
394 394 f.close()
395 395
396 396 # make sure filecache has the correct stat info for _branch after
397 397 # replacing the underlying file
398 398 ce = self._filecache[b'_branch']
399 399 if ce:
400 400 ce.refresh()
401 401 except: # re-raises
402 402 f.discard()
403 403 raise
404 404
405 405 def invalidate(self):
406 406 """Causes the next access to reread the dirstate.
407 407
408 408 This is different from localrepo.invalidatedirstate() because it always
409 409 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
410 410 check whether the dirstate has changed before rereading it."""
411 411
412 412 for a in ("_map", "_branch", "_ignore"):
413 413 if a in self.__dict__:
414 414 delattr(self, a)
415 415 self._dirty = False
416 416 self._dirty_tracked_set = False
417 417 self._parentwriters = 0
418 418 self._origpl = None
419 419
420 420 def copy(self, source, dest):
421 421 """Mark dest as a copy of source. Unmark dest if source is None."""
422 422 if source == dest:
423 423 return
424 424 self._dirty = True
425 425 if source is not None:
426 426 self._map.copymap[dest] = source
427 427 else:
428 428 self._map.copymap.pop(dest, None)
429 429
430 430 def copied(self, file):
431 431 return self._map.copymap.get(file, None)
432 432
433 433 def copies(self):
434 434 return self._map.copymap
435 435
436 436 @requires_no_parents_change
437 437 def set_tracked(self, filename, reset_copy=False):
438 438 """a "public" method for generic code to mark a file as tracked
439 439
440 440 This function is to be called outside of "update/merge" case. For
441 441 example by a command like `hg add X`.
442 442
443 443 if reset_copy is set, any existing copy information will be dropped.
444 444
445 445 return True the file was previously untracked, False otherwise.
446 446 """
447 447 self._dirty = True
448 448 entry = self._map.get(filename)
449 449 if entry is None or not entry.tracked:
450 450 self._check_new_tracked_filename(filename)
451 451 pre_tracked = self._map.set_tracked(filename)
452 452 if reset_copy:
453 453 self._map.copymap.pop(filename, None)
454 454 if pre_tracked:
455 455 self._dirty_tracked_set = True
456 456 return pre_tracked
457 457
458 458 @requires_no_parents_change
459 459 def set_untracked(self, filename):
460 460 """a "public" method for generic code to mark a file as untracked
461 461
462 462 This function is to be called outside of "update/merge" case. For
463 463 example by a command like `hg remove X`.
464 464
465 465 return True the file was previously tracked, False otherwise.
466 466 """
467 467 ret = self._map.set_untracked(filename)
468 468 if ret:
469 469 self._dirty = True
470 470 self._dirty_tracked_set = True
471 471 return ret
472 472
473 473 @requires_no_parents_change
474 474 def set_clean(self, filename, parentfiledata):
475 475 """record that the current state of the file on disk is known to be clean"""
476 476 self._dirty = True
477 477 if not self._map[filename].tracked:
478 478 self._check_new_tracked_filename(filename)
479 479 (mode, size, mtime) = parentfiledata
480 480 self._map.set_clean(filename, mode, size, mtime)
481 481
482 482 @requires_no_parents_change
483 483 def set_possibly_dirty(self, filename):
484 484 """record that the current state of the file on disk is unknown"""
485 485 self._dirty = True
486 486 self._map.set_possibly_dirty(filename)
487 487
488 488 @requires_parents_change
489 489 def update_file_p1(
490 490 self,
491 491 filename,
492 492 p1_tracked,
493 493 ):
494 494 """Set a file as tracked in the parent (or not)
495 495
496 496 This is to be called when adjust the dirstate to a new parent after an history
497 497 rewriting operation.
498 498
499 499 It should not be called during a merge (p2 != nullid) and only within
500 500 a `with dirstate.parentchange():` context.
501 501 """
502 502 if self.in_merge:
503 503 msg = b'update_file_reference should not be called when merging'
504 504 raise error.ProgrammingError(msg)
505 505 entry = self._map.get(filename)
506 506 if entry is None:
507 507 wc_tracked = False
508 508 else:
509 509 wc_tracked = entry.tracked
510 510 if not (p1_tracked or wc_tracked):
511 511 # the file is no longer relevant to anyone
512 512 if self._map.get(filename) is not None:
513 513 self._map.reset_state(filename)
514 514 self._dirty = True
515 515 elif (not p1_tracked) and wc_tracked:
516 516 if entry is not None and entry.added:
517 517 return # avoid dropping copy information (maybe?)
518 518
519 519 self._map.reset_state(
520 520 filename,
521 521 wc_tracked,
522 522 p1_tracked,
523 523 # the underlying reference might have changed, we will have to
524 524 # check it.
525 525 has_meaningful_mtime=False,
526 526 )
527 527
528 528 @requires_parents_change
529 529 def update_file(
530 530 self,
531 531 filename,
532 532 wc_tracked,
533 533 p1_tracked,
534 534 p2_info=False,
535 535 possibly_dirty=False,
536 536 parentfiledata=None,
537 537 ):
538 538 """update the information about a file in the dirstate
539 539
540 540 This is to be called when the direstates parent changes to keep track
541 541 of what is the file situation in regards to the working copy and its parent.
542 542
543 543 This function must be called within a `dirstate.parentchange` context.
544 544
545 545 note: the API is at an early stage and we might need to adjust it
546 546 depending of what information ends up being relevant and useful to
547 547 other processing.
548 548 """
549 549
550 550 # note: I do not think we need to double check name clash here since we
551 551 # are in a update/merge case that should already have taken care of
552 552 # this. The test agrees
553 553
554 554 self._dirty = True
555 555 old_entry = self._map.get(filename)
556 556 if old_entry is None:
557 557 prev_tracked = False
558 558 else:
559 559 prev_tracked = old_entry.tracked
560 560 if prev_tracked != wc_tracked:
561 561 self._dirty_tracked_set = True
562 562
563 563 self._map.reset_state(
564 564 filename,
565 565 wc_tracked,
566 566 p1_tracked,
567 567 p2_info=p2_info,
568 568 has_meaningful_mtime=not possibly_dirty,
569 569 parentfiledata=parentfiledata,
570 570 )
571 571
572 572 def _check_new_tracked_filename(self, filename):
573 573 scmutil.checkfilename(filename)
574 574 if self._map.hastrackeddir(filename):
575 575 msg = _(b'directory %r already in dirstate')
576 576 msg %= pycompat.bytestr(filename)
577 577 raise error.Abort(msg)
578 578 # shadows
579 579 for d in pathutil.finddirs(filename):
580 580 if self._map.hastrackeddir(d):
581 581 break
582 582 entry = self._map.get(d)
583 583 if entry is not None and not entry.removed:
584 584 msg = _(b'file %r in dirstate clashes with %r')
585 585 msg %= (pycompat.bytestr(d), pycompat.bytestr(filename))
586 586 raise error.Abort(msg)
587 587
588 588 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
589 589 if exists is None:
590 590 exists = os.path.lexists(os.path.join(self._root, path))
591 591 if not exists:
592 592 # Maybe a path component exists
593 593 if not ignoremissing and b'/' in path:
594 594 d, f = path.rsplit(b'/', 1)
595 595 d = self._normalize(d, False, ignoremissing, None)
596 596 folded = d + b"/" + f
597 597 else:
598 598 # No path components, preserve original case
599 599 folded = path
600 600 else:
601 601 # recursively normalize leading directory components
602 602 # against dirstate
603 603 if b'/' in normed:
604 604 d, f = normed.rsplit(b'/', 1)
605 605 d = self._normalize(d, False, ignoremissing, True)
606 606 r = self._root + b"/" + d
607 607 folded = d + b"/" + util.fspath(f, r)
608 608 else:
609 609 folded = util.fspath(normed, self._root)
610 610 storemap[normed] = folded
611 611
612 612 return folded
613 613
614 614 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
615 615 normed = util.normcase(path)
616 616 folded = self._map.filefoldmap.get(normed, None)
617 617 if folded is None:
618 618 if isknown:
619 619 folded = path
620 620 else:
621 621 folded = self._discoverpath(
622 622 path, normed, ignoremissing, exists, self._map.filefoldmap
623 623 )
624 624 return folded
625 625
626 626 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
627 627 normed = util.normcase(path)
628 628 folded = self._map.filefoldmap.get(normed, None)
629 629 if folded is None:
630 630 folded = self._map.dirfoldmap.get(normed, None)
631 631 if folded is None:
632 632 if isknown:
633 633 folded = path
634 634 else:
635 635 # store discovered result in dirfoldmap so that future
636 636 # normalizefile calls don't start matching directories
637 637 folded = self._discoverpath(
638 638 path, normed, ignoremissing, exists, self._map.dirfoldmap
639 639 )
640 640 return folded
641 641
642 642 def normalize(self, path, isknown=False, ignoremissing=False):
643 643 """
644 644 normalize the case of a pathname when on a casefolding filesystem
645 645
646 646 isknown specifies whether the filename came from walking the
647 647 disk, to avoid extra filesystem access.
648 648
649 649 If ignoremissing is True, missing path are returned
650 650 unchanged. Otherwise, we try harder to normalize possibly
651 651 existing path components.
652 652
653 653 The normalized case is determined based on the following precedence:
654 654
655 655 - version of name already stored in the dirstate
656 656 - version of name stored on disk
657 657 - version provided via command arguments
658 658 """
659 659
660 660 if self._checkcase:
661 661 return self._normalize(path, isknown, ignoremissing)
662 662 return path
663 663
664 664 def clear(self):
665 665 self._map.clear()
666 666 self._dirty = True
667 667
668 668 def rebuild(self, parent, allfiles, changedfiles=None):
669 669 if changedfiles is None:
670 670 # Rebuild entire dirstate
671 671 to_lookup = allfiles
672 672 to_drop = []
673 673 self.clear()
674 674 elif len(changedfiles) < 10:
675 675 # Avoid turning allfiles into a set, which can be expensive if it's
676 676 # large.
677 677 to_lookup = []
678 678 to_drop = []
679 679 for f in changedfiles:
680 680 if f in allfiles:
681 681 to_lookup.append(f)
682 682 else:
683 683 to_drop.append(f)
684 684 else:
685 685 changedfilesset = set(changedfiles)
686 686 to_lookup = changedfilesset & set(allfiles)
687 687 to_drop = changedfilesset - to_lookup
688 688
689 689 if self._origpl is None:
690 690 self._origpl = self._pl
691 691 self._map.setparents(parent, self._nodeconstants.nullid)
692 692
693 693 for f in to_lookup:
694 694
695 695 if self.in_merge:
696 696 self.set_tracked(f)
697 697 else:
698 698 self._map.reset_state(
699 699 f,
700 700 wc_tracked=True,
701 701 p1_tracked=True,
702 702 )
703 703 for f in to_drop:
704 704 self._map.reset_state(f)
705 705
706 706 self._dirty = True
707 707
708 708 def identity(self):
709 709 """Return identity of dirstate itself to detect changing in storage
710 710
711 711 If identity of previous dirstate is equal to this, writing
712 712 changes based on the former dirstate out can keep consistency.
713 713 """
714 714 return self._map.identity
715 715
716 716 def write(self, tr):
717 717 if not self._dirty:
718 718 return
719 719
720 720 write_key = self._use_tracked_hint and self._dirty_tracked_set
721 721 if tr:
722 722 # delay writing in-memory changes out
723 723 tr.addfilegenerator(
724 724 b'dirstate-1-main',
725 725 (self._filename,),
726 726 lambda f: self._writedirstate(tr, f),
727 727 location=b'plain',
728 728 post_finalize=True,
729 729 )
730 730 if write_key:
731 731 tr.addfilegenerator(
732 732 b'dirstate-2-key-post',
733 733 (self._filename_th,),
734 734 lambda f: self._write_tracked_hint(tr, f),
735 735 location=b'plain',
736 736 post_finalize=True,
737 737 )
738 738 return
739 739
740 740 file = lambda f: self._opener(f, b"w", atomictemp=True, checkambig=True)
741 741 with file(self._filename) as f:
742 742 self._writedirstate(tr, f)
743 743 if write_key:
744 744 # we update the key-file after writing to make sure reader have a
745 745 # key that match the newly written content
746 746 with file(self._filename_th) as f:
747 747 self._write_tracked_hint(tr, f)
748 748
749 749 def delete_tracked_hint(self):
750 750 """remove the tracked_hint file
751 751
752 752 To be used by format downgrades operation"""
753 753 self._opener.unlink(self._filename_th)
754 754 self._use_tracked_hint = False
755 755
756 756 def addparentchangecallback(self, category, callback):
757 757 """add a callback to be called when the wd parents are changed
758 758
759 759 Callback will be called with the following arguments:
760 760 dirstate, (oldp1, oldp2), (newp1, newp2)
761 761
762 762 Category is a unique identifier to allow overwriting an old callback
763 763 with a newer callback.
764 764 """
765 765 self._plchangecallbacks[category] = callback
766 766
767 767 def _writedirstate(self, tr, st):
768 768 # notify callbacks about parents change
769 769 if self._origpl is not None and self._origpl != self._pl:
770 770 for c, callback in sorted(self._plchangecallbacks.items()):
771 771 callback(self, self._origpl, self._pl)
772 772 self._origpl = None
773 773 self._map.write(tr, st)
774 774 self._dirty = False
775 775 self._dirty_tracked_set = False
776 776
777 777 def _write_tracked_hint(self, tr, f):
778 778 key = node.hex(uuid.uuid4().bytes)
779 779 f.write(b"1\n%s\n" % key) # 1 is the format version
780 780
781 781 def _dirignore(self, f):
782 782 if self._ignore(f):
783 783 return True
784 784 for p in pathutil.finddirs(f):
785 785 if self._ignore(p):
786 786 return True
787 787 return False
788 788
789 789 def _ignorefiles(self):
790 790 files = []
791 791 if os.path.exists(self._join(b'.hgignore')):
792 792 files.append(self._join(b'.hgignore'))
793 793 for name, path in self._ui.configitems(b"ui"):
794 794 if name == b'ignore' or name.startswith(b'ignore.'):
795 795 # we need to use os.path.join here rather than self._join
796 796 # because path is arbitrary and user-specified
797 797 files.append(os.path.join(self._rootdir, util.expandpath(path)))
798 798 return files
799 799
800 800 def _ignorefileandline(self, f):
801 801 files = collections.deque(self._ignorefiles())
802 802 visited = set()
803 803 while files:
804 804 i = files.popleft()
805 805 patterns = matchmod.readpatternfile(
806 806 i, self._ui.warn, sourceinfo=True
807 807 )
808 808 for pattern, lineno, line in patterns:
809 809 kind, p = matchmod._patsplit(pattern, b'glob')
810 810 if kind == b"subinclude":
811 811 if p not in visited:
812 812 files.append(p)
813 813 continue
814 814 m = matchmod.match(
815 815 self._root, b'', [], [pattern], warn=self._ui.warn
816 816 )
817 817 if m(f):
818 818 return (i, lineno, line)
819 819 visited.add(i)
820 820 return (None, -1, b"")
821 821
822 822 def _walkexplicit(self, match, subrepos):
823 823 """Get stat data about the files explicitly specified by match.
824 824
825 825 Return a triple (results, dirsfound, dirsnotfound).
826 826 - results is a mapping from filename to stat result. It also contains
827 827 listings mapping subrepos and .hg to None.
828 828 - dirsfound is a list of files found to be directories.
829 829 - dirsnotfound is a list of files that the dirstate thinks are
830 830 directories and that were not found."""
831 831
832 832 def badtype(mode):
833 833 kind = _(b'unknown')
834 834 if stat.S_ISCHR(mode):
835 835 kind = _(b'character device')
836 836 elif stat.S_ISBLK(mode):
837 837 kind = _(b'block device')
838 838 elif stat.S_ISFIFO(mode):
839 839 kind = _(b'fifo')
840 840 elif stat.S_ISSOCK(mode):
841 841 kind = _(b'socket')
842 842 elif stat.S_ISDIR(mode):
843 843 kind = _(b'directory')
844 844 return _(b'unsupported file type (type is %s)') % kind
845 845
846 846 badfn = match.bad
847 847 dmap = self._map
848 848 lstat = os.lstat
849 849 getkind = stat.S_IFMT
850 850 dirkind = stat.S_IFDIR
851 851 regkind = stat.S_IFREG
852 852 lnkkind = stat.S_IFLNK
853 853 join = self._join
854 854 dirsfound = []
855 855 foundadd = dirsfound.append
856 856 dirsnotfound = []
857 857 notfoundadd = dirsnotfound.append
858 858
859 859 if not match.isexact() and self._checkcase:
860 860 normalize = self._normalize
861 861 else:
862 862 normalize = None
863 863
864 864 files = sorted(match.files())
865 865 subrepos.sort()
866 866 i, j = 0, 0
867 867 while i < len(files) and j < len(subrepos):
868 868 subpath = subrepos[j] + b"/"
869 869 if files[i] < subpath:
870 870 i += 1
871 871 continue
872 872 while i < len(files) and files[i].startswith(subpath):
873 873 del files[i]
874 874 j += 1
875 875
876 876 if not files or b'' in files:
877 877 files = [b'']
878 878 # constructing the foldmap is expensive, so don't do it for the
879 879 # common case where files is ['']
880 880 normalize = None
881 881 results = dict.fromkeys(subrepos)
882 882 results[b'.hg'] = None
883 883
884 884 for ff in files:
885 885 if normalize:
886 886 nf = normalize(ff, False, True)
887 887 else:
888 888 nf = ff
889 889 if nf in results:
890 890 continue
891 891
892 892 try:
893 893 st = lstat(join(nf))
894 894 kind = getkind(st.st_mode)
895 895 if kind == dirkind:
896 896 if nf in dmap:
897 897 # file replaced by dir on disk but still in dirstate
898 898 results[nf] = None
899 899 foundadd((nf, ff))
900 900 elif kind == regkind or kind == lnkkind:
901 901 results[nf] = st
902 902 else:
903 903 badfn(ff, badtype(kind))
904 904 if nf in dmap:
905 905 results[nf] = None
906 906 except OSError as inst: # nf not found on disk - it is dirstate only
907 907 if nf in dmap: # does it exactly match a missing file?
908 908 results[nf] = None
909 909 else: # does it match a missing directory?
910 910 if self._map.hasdir(nf):
911 911 notfoundadd(nf)
912 912 else:
913 913 badfn(ff, encoding.strtolocal(inst.strerror))
914 914
915 915 # match.files() may contain explicitly-specified paths that shouldn't
916 916 # be taken; drop them from the list of files found. dirsfound/notfound
917 917 # aren't filtered here because they will be tested later.
918 918 if match.anypats():
919 919 for f in list(results):
920 920 if f == b'.hg' or f in subrepos:
921 921 # keep sentinel to disable further out-of-repo walks
922 922 continue
923 923 if not match(f):
924 924 del results[f]
925 925
926 926 # Case insensitive filesystems cannot rely on lstat() failing to detect
927 927 # a case-only rename. Prune the stat object for any file that does not
928 928 # match the case in the filesystem, if there are multiple files that
929 929 # normalize to the same path.
930 930 if match.isexact() and self._checkcase:
931 931 normed = {}
932 932
933 933 for f, st in results.items():
934 934 if st is None:
935 935 continue
936 936
937 937 nc = util.normcase(f)
938 938 paths = normed.get(nc)
939 939
940 940 if paths is None:
941 941 paths = set()
942 942 normed[nc] = paths
943 943
944 944 paths.add(f)
945 945
946 946 for norm, paths in normed.items():
947 947 if len(paths) > 1:
948 948 for path in paths:
949 949 folded = self._discoverpath(
950 950 path, norm, True, None, self._map.dirfoldmap
951 951 )
952 952 if path != folded:
953 953 results[path] = None
954 954
955 955 return results, dirsfound, dirsnotfound
956 956
957 957 def walk(self, match, subrepos, unknown, ignored, full=True):
958 958 """
959 959 Walk recursively through the directory tree, finding all files
960 960 matched by match.
961 961
962 962 If full is False, maybe skip some known-clean files.
963 963
964 964 Return a dict mapping filename to stat-like object (either
965 965 mercurial.osutil.stat instance or return value of os.stat()).
966 966
967 967 """
968 968 # full is a flag that extensions that hook into walk can use -- this
969 969 # implementation doesn't use it at all. This satisfies the contract
970 970 # because we only guarantee a "maybe".
971 971
972 972 if ignored:
973 973 ignore = util.never
974 974 dirignore = util.never
975 975 elif unknown:
976 976 ignore = self._ignore
977 977 dirignore = self._dirignore
978 978 else:
979 979 # if not unknown and not ignored, drop dir recursion and step 2
980 980 ignore = util.always
981 981 dirignore = util.always
982 982
983 983 matchfn = match.matchfn
984 984 matchalways = match.always()
985 985 matchtdir = match.traversedir
986 986 dmap = self._map
987 987 listdir = util.listdir
988 988 lstat = os.lstat
989 989 dirkind = stat.S_IFDIR
990 990 regkind = stat.S_IFREG
991 991 lnkkind = stat.S_IFLNK
992 992 join = self._join
993 993
994 994 exact = skipstep3 = False
995 995 if match.isexact(): # match.exact
996 996 exact = True
997 997 dirignore = util.always # skip step 2
998 998 elif match.prefix(): # match.match, no patterns
999 999 skipstep3 = True
1000 1000
1001 1001 if not exact and self._checkcase:
1002 1002 normalize = self._normalize
1003 1003 normalizefile = self._normalizefile
1004 1004 skipstep3 = False
1005 1005 else:
1006 1006 normalize = self._normalize
1007 1007 normalizefile = None
1008 1008
1009 1009 # step 1: find all explicit files
1010 1010 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1011 1011 if matchtdir:
1012 1012 for d in work:
1013 1013 matchtdir(d[0])
1014 1014 for d in dirsnotfound:
1015 1015 matchtdir(d)
1016 1016
1017 1017 skipstep3 = skipstep3 and not (work or dirsnotfound)
1018 1018 work = [d for d in work if not dirignore(d[0])]
1019 1019
1020 1020 # step 2: visit subdirectories
1021 1021 def traverse(work, alreadynormed):
1022 1022 wadd = work.append
1023 1023 while work:
1024 1024 tracing.counter('dirstate.walk work', len(work))
1025 1025 nd = work.pop()
1026 1026 visitentries = match.visitchildrenset(nd)
1027 1027 if not visitentries:
1028 1028 continue
1029 1029 if visitentries == b'this' or visitentries == b'all':
1030 1030 visitentries = None
1031 1031 skip = None
1032 1032 if nd != b'':
1033 1033 skip = b'.hg'
1034 1034 try:
1035 1035 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1036 1036 entries = listdir(join(nd), stat=True, skip=skip)
1037 1037 except (PermissionError, FileNotFoundError) as inst:
1038 1038 match.bad(
1039 1039 self.pathto(nd), encoding.strtolocal(inst.strerror)
1040 1040 )
1041 1041 continue
1042 1042 for f, kind, st in entries:
1043 1043 # Some matchers may return files in the visitentries set,
1044 1044 # instead of 'this', if the matcher explicitly mentions them
1045 1045 # and is not an exactmatcher. This is acceptable; we do not
1046 1046 # make any hard assumptions about file-or-directory below
1047 1047 # based on the presence of `f` in visitentries. If
1048 1048 # visitchildrenset returned a set, we can always skip the
1049 1049 # entries *not* in the set it provided regardless of whether
1050 1050 # they're actually a file or a directory.
1051 1051 if visitentries and f not in visitentries:
1052 1052 continue
1053 1053 if normalizefile:
1054 1054 # even though f might be a directory, we're only
1055 1055 # interested in comparing it to files currently in the
1056 1056 # dmap -- therefore normalizefile is enough
1057 1057 nf = normalizefile(
1058 1058 nd and (nd + b"/" + f) or f, True, True
1059 1059 )
1060 1060 else:
1061 1061 nf = nd and (nd + b"/" + f) or f
1062 1062 if nf not in results:
1063 1063 if kind == dirkind:
1064 1064 if not ignore(nf):
1065 1065 if matchtdir:
1066 1066 matchtdir(nf)
1067 1067 wadd(nf)
1068 1068 if nf in dmap and (matchalways or matchfn(nf)):
1069 1069 results[nf] = None
1070 1070 elif kind == regkind or kind == lnkkind:
1071 1071 if nf in dmap:
1072 1072 if matchalways or matchfn(nf):
1073 1073 results[nf] = st
1074 1074 elif (matchalways or matchfn(nf)) and not ignore(
1075 1075 nf
1076 1076 ):
1077 1077 # unknown file -- normalize if necessary
1078 1078 if not alreadynormed:
1079 1079 nf = normalize(nf, False, True)
1080 1080 results[nf] = st
1081 1081 elif nf in dmap and (matchalways or matchfn(nf)):
1082 1082 results[nf] = None
1083 1083
1084 1084 for nd, d in work:
1085 1085 # alreadynormed means that processwork doesn't have to do any
1086 1086 # expensive directory normalization
1087 1087 alreadynormed = not normalize or nd == d
1088 1088 traverse([d], alreadynormed)
1089 1089
1090 1090 for s in subrepos:
1091 1091 del results[s]
1092 1092 del results[b'.hg']
1093 1093
1094 1094 # step 3: visit remaining files from dmap
1095 1095 if not skipstep3 and not exact:
1096 1096 # If a dmap file is not in results yet, it was either
1097 1097 # a) not matching matchfn b) ignored, c) missing, or d) under a
1098 1098 # symlink directory.
1099 1099 if not results and matchalways:
1100 1100 visit = [f for f in dmap]
1101 1101 else:
1102 1102 visit = [f for f in dmap if f not in results and matchfn(f)]
1103 1103 visit.sort()
1104 1104
1105 1105 if unknown:
1106 1106 # unknown == True means we walked all dirs under the roots
1107 1107 # that wasn't ignored, and everything that matched was stat'ed
1108 1108 # and is already in results.
1109 1109 # The rest must thus be ignored or under a symlink.
1110 1110 audit_path = pathutil.pathauditor(self._root, cached=True)
1111 1111
1112 1112 for nf in iter(visit):
1113 1113 # If a stat for the same file was already added with a
1114 1114 # different case, don't add one for this, since that would
1115 1115 # make it appear as if the file exists under both names
1116 1116 # on disk.
1117 1117 if (
1118 1118 normalizefile
1119 1119 and normalizefile(nf, True, True) in results
1120 1120 ):
1121 1121 results[nf] = None
1122 1122 # Report ignored items in the dmap as long as they are not
1123 1123 # under a symlink directory.
1124 1124 elif audit_path.check(nf):
1125 1125 try:
1126 1126 results[nf] = lstat(join(nf))
1127 1127 # file was just ignored, no links, and exists
1128 1128 except OSError:
1129 1129 # file doesn't exist
1130 1130 results[nf] = None
1131 1131 else:
1132 1132 # It's either missing or under a symlink directory
1133 1133 # which we in this case report as missing
1134 1134 results[nf] = None
1135 1135 else:
1136 1136 # We may not have walked the full directory tree above,
1137 1137 # so stat and check everything we missed.
1138 1138 iv = iter(visit)
1139 1139 for st in util.statfiles([join(i) for i in visit]):
1140 1140 results[next(iv)] = st
1141 1141 return results
1142 1142
1143 1143 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1144 1144 # Force Rayon (Rust parallelism library) to respect the number of
1145 1145 # workers. This is a temporary workaround until Rust code knows
1146 1146 # how to read the config file.
1147 1147 numcpus = self._ui.configint(b"worker", b"numcpus")
1148 1148 if numcpus is not None:
1149 1149 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1150 1150
1151 1151 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1152 1152 if not workers_enabled:
1153 1153 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1154 1154
1155 1155 (
1156 1156 lookup,
1157 1157 modified,
1158 1158 added,
1159 1159 removed,
1160 1160 deleted,
1161 1161 clean,
1162 1162 ignored,
1163 1163 unknown,
1164 1164 warnings,
1165 1165 bad,
1166 1166 traversed,
1167 1167 dirty,
1168 1168 ) = rustmod.status(
1169 1169 self._map._map,
1170 1170 matcher,
1171 1171 self._rootdir,
1172 1172 self._ignorefiles(),
1173 1173 self._checkexec,
1174 1174 bool(list_clean),
1175 1175 bool(list_ignored),
1176 1176 bool(list_unknown),
1177 1177 bool(matcher.traversedir),
1178 1178 )
1179 1179
1180 1180 self._dirty |= dirty
1181 1181
1182 1182 if matcher.traversedir:
1183 1183 for dir in traversed:
1184 1184 matcher.traversedir(dir)
1185 1185
1186 1186 if self._ui.warn:
1187 1187 for item in warnings:
1188 1188 if isinstance(item, tuple):
1189 1189 file_path, syntax = item
1190 1190 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1191 1191 file_path,
1192 1192 syntax,
1193 1193 )
1194 1194 self._ui.warn(msg)
1195 1195 else:
1196 1196 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1197 1197 self._ui.warn(
1198 1198 msg
1199 1199 % (
1200 1200 pathutil.canonpath(
1201 1201 self._rootdir, self._rootdir, item
1202 1202 ),
1203 1203 b"No such file or directory",
1204 1204 )
1205 1205 )
1206 1206
1207 1207 for (fn, message) in bad:
1208 1208 matcher.bad(fn, encoding.strtolocal(message))
1209 1209
1210 1210 status = scmutil.status(
1211 1211 modified=modified,
1212 1212 added=added,
1213 1213 removed=removed,
1214 1214 deleted=deleted,
1215 1215 unknown=unknown,
1216 1216 ignored=ignored,
1217 1217 clean=clean,
1218 1218 )
1219 1219 return (lookup, status)
1220 1220
1221 1221 def status(self, match, subrepos, ignored, clean, unknown):
1222 1222 """Determine the status of the working copy relative to the
1223 1223 dirstate and return a pair of (unsure, status), where status is of type
1224 1224 scmutil.status and:
1225 1225
1226 1226 unsure:
1227 1227 files that might have been modified since the dirstate was
1228 1228 written, but need to be read to be sure (size is the same
1229 1229 but mtime differs)
1230 1230 status.modified:
1231 1231 files that have definitely been modified since the dirstate
1232 1232 was written (different size or mode)
1233 1233 status.clean:
1234 1234 files that have definitely not been modified since the
1235 1235 dirstate was written
1236 1236 """
1237 1237 listignored, listclean, listunknown = ignored, clean, unknown
1238 1238 lookup, modified, added, unknown, ignored = [], [], [], [], []
1239 1239 removed, deleted, clean = [], [], []
1240 1240
1241 1241 dmap = self._map
1242 1242 dmap.preload()
1243 1243
1244 1244 use_rust = True
1245 1245
1246 1246 allowed_matchers = (
1247 1247 matchmod.alwaysmatcher,
1248 1248 matchmod.exactmatcher,
1249 1249 matchmod.includematcher,
1250 matchmod.unionmatcher,
1250 1251 )
1251 1252
1252 1253 if rustmod is None:
1253 1254 use_rust = False
1254 1255 elif self._checkcase:
1255 1256 # Case-insensitive filesystems are not handled yet
1256 1257 use_rust = False
1257 1258 elif subrepos:
1258 1259 use_rust = False
1259 1260 elif sparse.enabled:
1260 1261 use_rust = False
1261 1262 elif not isinstance(match, allowed_matchers):
1262 1263 # Some matchers have yet to be implemented
1263 1264 use_rust = False
1264 1265
1265 1266 # Get the time from the filesystem so we can disambiguate files that
1266 1267 # appear modified in the present or future.
1267 1268 try:
1268 1269 mtime_boundary = timestamp.get_fs_now(self._opener)
1269 1270 except OSError:
1270 1271 # In largefiles or readonly context
1271 1272 mtime_boundary = None
1272 1273
1273 1274 if use_rust:
1274 1275 try:
1275 1276 res = self._rust_status(
1276 1277 match, listclean, listignored, listunknown
1277 1278 )
1278 1279 return res + (mtime_boundary,)
1279 1280 except rustmod.FallbackError:
1280 1281 pass
1281 1282
1282 1283 def noop(f):
1283 1284 pass
1284 1285
1285 1286 dcontains = dmap.__contains__
1286 1287 dget = dmap.__getitem__
1287 1288 ladd = lookup.append # aka "unsure"
1288 1289 madd = modified.append
1289 1290 aadd = added.append
1290 1291 uadd = unknown.append if listunknown else noop
1291 1292 iadd = ignored.append if listignored else noop
1292 1293 radd = removed.append
1293 1294 dadd = deleted.append
1294 1295 cadd = clean.append if listclean else noop
1295 1296 mexact = match.exact
1296 1297 dirignore = self._dirignore
1297 1298 checkexec = self._checkexec
1298 1299 checklink = self._checklink
1299 1300 copymap = self._map.copymap
1300 1301
1301 1302 # We need to do full walks when either
1302 1303 # - we're listing all clean files, or
1303 1304 # - match.traversedir does something, because match.traversedir should
1304 1305 # be called for every dir in the working dir
1305 1306 full = listclean or match.traversedir is not None
1306 1307 for fn, st in self.walk(
1307 1308 match, subrepos, listunknown, listignored, full=full
1308 1309 ).items():
1309 1310 if not dcontains(fn):
1310 1311 if (listignored or mexact(fn)) and dirignore(fn):
1311 1312 if listignored:
1312 1313 iadd(fn)
1313 1314 else:
1314 1315 uadd(fn)
1315 1316 continue
1316 1317
1317 1318 t = dget(fn)
1318 1319 mode = t.mode
1319 1320 size = t.size
1320 1321
1321 1322 if not st and t.tracked:
1322 1323 dadd(fn)
1323 1324 elif t.p2_info:
1324 1325 madd(fn)
1325 1326 elif t.added:
1326 1327 aadd(fn)
1327 1328 elif t.removed:
1328 1329 radd(fn)
1329 1330 elif t.tracked:
1330 1331 if not checklink and t.has_fallback_symlink:
1331 1332 # If the file system does not support symlink, the mode
1332 1333 # might not be correctly stored in the dirstate, so do not
1333 1334 # trust it.
1334 1335 ladd(fn)
1335 1336 elif not checkexec and t.has_fallback_exec:
1336 1337 # If the file system does not support exec bits, the mode
1337 1338 # might not be correctly stored in the dirstate, so do not
1338 1339 # trust it.
1339 1340 ladd(fn)
1340 1341 elif (
1341 1342 size >= 0
1342 1343 and (
1343 1344 (size != st.st_size and size != st.st_size & _rangemask)
1344 1345 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1345 1346 )
1346 1347 or fn in copymap
1347 1348 ):
1348 1349 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1349 1350 # issue6456: Size returned may be longer due to
1350 1351 # encryption on EXT-4 fscrypt, undecided.
1351 1352 ladd(fn)
1352 1353 else:
1353 1354 madd(fn)
1354 1355 elif not t.mtime_likely_equal_to(timestamp.mtime_of(st)):
1355 1356 # There might be a change in the future if for example the
1356 1357 # internal clock is off, but this is a case where the issues
1357 1358 # the user would face would be a lot worse and there is
1358 1359 # nothing we can really do.
1359 1360 ladd(fn)
1360 1361 elif listclean:
1361 1362 cadd(fn)
1362 1363 status = scmutil.status(
1363 1364 modified, added, removed, deleted, unknown, ignored, clean
1364 1365 )
1365 1366 return (lookup, status, mtime_boundary)
1366 1367
1367 1368 def matches(self, match):
1368 1369 """
1369 1370 return files in the dirstate (in whatever state) filtered by match
1370 1371 """
1371 1372 dmap = self._map
1372 1373 if rustmod is not None:
1373 1374 dmap = self._map._map
1374 1375
1375 1376 if match.always():
1376 1377 return dmap.keys()
1377 1378 files = match.files()
1378 1379 if match.isexact():
1379 1380 # fast path -- filter the other way around, since typically files is
1380 1381 # much smaller than dmap
1381 1382 return [f for f in files if f in dmap]
1382 1383 if match.prefix() and all(fn in dmap for fn in files):
1383 1384 # fast path -- all the values are known to be files, so just return
1384 1385 # that
1385 1386 return list(files)
1386 1387 return [f for f in dmap if match(f)]
1387 1388
1388 1389 def _actualfilename(self, tr):
1389 1390 if tr:
1390 1391 return self._pendingfilename
1391 1392 else:
1392 1393 return self._filename
1393 1394
1394 1395 def savebackup(self, tr, backupname):
1395 1396 '''Save current dirstate into backup file'''
1396 1397 filename = self._actualfilename(tr)
1397 1398 assert backupname != filename
1398 1399
1399 1400 # use '_writedirstate' instead of 'write' to write changes certainly,
1400 1401 # because the latter omits writing out if transaction is running.
1401 1402 # output file will be used to create backup of dirstate at this point.
1402 1403 if self._dirty or not self._opener.exists(filename):
1403 1404 self._writedirstate(
1404 1405 tr,
1405 1406 self._opener(filename, b"w", atomictemp=True, checkambig=True),
1406 1407 )
1407 1408
1408 1409 if tr:
1409 1410 # ensure that subsequent tr.writepending returns True for
1410 1411 # changes written out above, even if dirstate is never
1411 1412 # changed after this
1412 1413 tr.addfilegenerator(
1413 1414 b'dirstate-1-main',
1414 1415 (self._filename,),
1415 1416 lambda f: self._writedirstate(tr, f),
1416 1417 location=b'plain',
1417 1418 post_finalize=True,
1418 1419 )
1419 1420
1420 1421 # ensure that pending file written above is unlinked at
1421 1422 # failure, even if tr.writepending isn't invoked until the
1422 1423 # end of this transaction
1423 1424 tr.registertmp(filename, location=b'plain')
1424 1425
1425 1426 self._opener.tryunlink(backupname)
1426 1427 # hardlink backup is okay because _writedirstate is always called
1427 1428 # with an "atomictemp=True" file.
1428 1429 util.copyfile(
1429 1430 self._opener.join(filename),
1430 1431 self._opener.join(backupname),
1431 1432 hardlink=True,
1432 1433 )
1433 1434
1434 1435 def restorebackup(self, tr, backupname):
1435 1436 '''Restore dirstate by backup file'''
1436 1437 # this "invalidate()" prevents "wlock.release()" from writing
1437 1438 # changes of dirstate out after restoring from backup file
1438 1439 self.invalidate()
1439 1440 filename = self._actualfilename(tr)
1440 1441 o = self._opener
1441 1442 if util.samefile(o.join(backupname), o.join(filename)):
1442 1443 o.unlink(backupname)
1443 1444 else:
1444 1445 o.rename(backupname, filename, checkambig=True)
1445 1446
1446 1447 def clearbackup(self, tr, backupname):
1447 1448 '''Clear backup file'''
1448 1449 self._opener.unlink(backupname)
1449 1450
1450 1451 def verify(self, m1, m2):
1451 1452 """check the dirstate content again the parent manifest and yield errors"""
1452 1453 missing_from_p1 = b"%s in state %s, but not in manifest1\n"
1453 1454 unexpected_in_p1 = b"%s in state %s, but also in manifest1\n"
1454 1455 missing_from_ps = b"%s in state %s, but not in either manifest\n"
1455 1456 missing_from_ds = b"%s in manifest1, but listed as state %s\n"
1456 1457 for f, entry in self.items():
1457 1458 state = entry.state
1458 1459 if state in b"nr" and f not in m1:
1459 1460 yield (missing_from_p1, f, state)
1460 1461 if state in b"a" and f in m1:
1461 1462 yield (unexpected_in_p1, f, state)
1462 1463 if state in b"m" and f not in m1 and f not in m2:
1463 1464 yield (missing_from_ps, f, state)
1464 1465 for f in m1:
1465 1466 state = self.get_entry(f).state
1466 1467 if state not in b"nrm":
1467 1468 yield (missing_from_ds, f, state)
@@ -1,283 +1,292 b''
1 1 // status.rs
2 2 //
3 3 // Copyright 2019, Raphaël Gomès <rgomes@octobus.net>
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 //! Bindings for the `hg::status` module provided by the
9 9 //! `hg-core` crate. From Python, this will be seen as
10 10 //! `rustext.dirstate.status`.
11 11
12 12 use crate::{dirstate::DirstateMap, exceptions::FallbackError};
13 13 use cpython::{
14 14 exc::ValueError, ObjectProtocol, PyBytes, PyErr, PyList, PyObject,
15 15 PyResult, PyTuple, Python, PythonObject, ToPyObject,
16 16 };
17 17 use hg::dirstate::status::StatusPath;
18 use hg::matchers::Matcher;
18 use hg::matchers::{Matcher, UnionMatcher};
19 19 use hg::{
20 20 matchers::{AlwaysMatcher, FileMatcher, IncludeMatcher},
21 21 parse_pattern_syntax,
22 22 utils::{
23 23 files::{get_bytes_from_path, get_path_from_bytes},
24 24 hg_path::{HgPath, HgPathBuf},
25 25 },
26 26 BadMatch, DirstateStatus, IgnorePattern, PatternFileWarning, StatusError,
27 27 StatusOptions,
28 28 };
29 29 use std::borrow::Borrow;
30 30
31 31 fn collect_status_path_list(py: Python, paths: &[StatusPath<'_>]) -> PyList {
32 32 collect_pybytes_list(py, paths.iter().map(|item| &*item.path))
33 33 }
34 34
35 35 /// This will be useless once trait impls for collection are added to `PyBytes`
36 36 /// upstream.
37 37 fn collect_pybytes_list(
38 38 py: Python,
39 39 iter: impl Iterator<Item = impl AsRef<HgPath>>,
40 40 ) -> PyList {
41 41 let list = PyList::new(py, &[]);
42 42
43 43 for path in iter {
44 44 list.append(
45 45 py,
46 46 PyBytes::new(py, path.as_ref().as_bytes()).into_object(),
47 47 )
48 48 }
49 49
50 50 list
51 51 }
52 52
53 53 fn collect_bad_matches(
54 54 py: Python,
55 55 collection: &[(impl AsRef<HgPath>, BadMatch)],
56 56 ) -> PyResult<PyList> {
57 57 let list = PyList::new(py, &[]);
58 58
59 59 let os = py.import("os")?;
60 60 let get_error_message = |code: i32| -> PyResult<_> {
61 61 os.call(
62 62 py,
63 63 "strerror",
64 64 PyTuple::new(py, &[code.to_py_object(py).into_object()]),
65 65 None,
66 66 )
67 67 };
68 68
69 69 for (path, bad_match) in collection.iter() {
70 70 let message = match bad_match {
71 71 BadMatch::OsError(code) => get_error_message(*code)?,
72 72 BadMatch::BadType(bad_type) => format!(
73 73 "unsupported file type (type is {})",
74 74 bad_type.to_string()
75 75 )
76 76 .to_py_object(py)
77 77 .into_object(),
78 78 };
79 79 list.append(
80 80 py,
81 81 (PyBytes::new(py, path.as_ref().as_bytes()), message)
82 82 .to_py_object(py)
83 83 .into_object(),
84 84 )
85 85 }
86 86
87 87 Ok(list)
88 88 }
89 89
90 90 fn handle_fallback(py: Python, err: StatusError) -> PyErr {
91 91 match err {
92 92 StatusError::Pattern(e) => {
93 93 let as_string = e.to_string();
94 94 log::trace!("Rust status fallback: `{}`", &as_string);
95 95
96 96 PyErr::new::<FallbackError, _>(py, &as_string)
97 97 }
98 98 e => PyErr::new::<ValueError, _>(py, e.to_string()),
99 99 }
100 100 }
101 101
102 102 pub fn status_wrapper(
103 103 py: Python,
104 104 dmap: DirstateMap,
105 105 matcher: PyObject,
106 106 root_dir: PyObject,
107 107 ignore_files: PyList,
108 108 check_exec: bool,
109 109 list_clean: bool,
110 110 list_ignored: bool,
111 111 list_unknown: bool,
112 112 collect_traversed_dirs: bool,
113 113 ) -> PyResult<PyTuple> {
114 114 let bytes = root_dir.extract::<PyBytes>(py)?;
115 115 let root_dir = get_path_from_bytes(bytes.data(py));
116 116
117 117 let dmap: DirstateMap = dmap.to_py_object(py);
118 118 let mut dmap = dmap.get_inner_mut(py);
119 119
120 120 let ignore_files: PyResult<Vec<_>> = ignore_files
121 121 .iter(py)
122 122 .map(|b| {
123 123 let file = b.extract::<PyBytes>(py)?;
124 124 Ok(get_path_from_bytes(file.data(py)).to_owned())
125 125 })
126 126 .collect();
127 127 let ignore_files = ignore_files?;
128 128 // The caller may call `copymap.items()` separately
129 129 let list_copies = false;
130 130
131 131 let after_status = |res: Result<(DirstateStatus<'_>, _), StatusError>| {
132 132 let (status_res, warnings) =
133 133 res.map_err(|e| handle_fallback(py, e))?;
134 134 build_response(py, status_res, warnings)
135 135 };
136 136
137 137 let matcher = extract_matcher(py, matcher)?;
138 138 dmap.with_status(
139 139 &*matcher,
140 140 root_dir.to_path_buf(),
141 141 ignore_files,
142 142 StatusOptions {
143 143 check_exec,
144 144 list_clean,
145 145 list_ignored,
146 146 list_unknown,
147 147 list_copies,
148 148 collect_traversed_dirs,
149 149 },
150 150 after_status,
151 151 )
152 152 }
153 153
154 154 /// Transform a Python matcher into a Rust matcher.
155 155 fn extract_matcher(
156 156 py: Python,
157 157 matcher: PyObject,
158 158 ) -> PyResult<Box<dyn Matcher + Sync>> {
159 159 match matcher.get_type(py).name(py).borrow() {
160 160 "alwaysmatcher" => Ok(Box::new(AlwaysMatcher)),
161 161 "exactmatcher" => {
162 162 let files = matcher.call_method(
163 163 py,
164 164 "files",
165 165 PyTuple::new(py, &[]),
166 166 None,
167 167 )?;
168 168 let files: PyList = files.cast_into(py)?;
169 169 let files: PyResult<Vec<HgPathBuf>> = files
170 170 .iter(py)
171 171 .map(|f| {
172 172 Ok(HgPathBuf::from_bytes(
173 173 f.extract::<PyBytes>(py)?.data(py),
174 174 ))
175 175 })
176 176 .collect();
177 177
178 178 let files = files?;
179 179 let file_matcher = FileMatcher::new(files)
180 180 .map_err(|e| PyErr::new::<ValueError, _>(py, e.to_string()))?;
181 181 Ok(Box::new(file_matcher))
182 182 }
183 183 "includematcher" => {
184 184 // Get the patterns from Python even though most of them are
185 185 // redundant with those we will parse later on, as they include
186 186 // those passed from the command line.
187 187 let ignore_patterns: PyResult<Vec<_>> = matcher
188 188 .getattr(py, "_kindpats")?
189 189 .iter(py)?
190 190 .map(|k| {
191 191 let k = k?;
192 192 let syntax = parse_pattern_syntax(
193 193 &[
194 194 k.get_item(py, 0)?
195 195 .extract::<PyBytes>(py)?
196 196 .data(py),
197 197 &b":"[..],
198 198 ]
199 199 .concat(),
200 200 )
201 201 .map_err(|e| {
202 202 handle_fallback(py, StatusError::Pattern(e))
203 203 })?;
204 204 let pattern = k.get_item(py, 1)?.extract::<PyBytes>(py)?;
205 205 let pattern = pattern.data(py);
206 206 let source = k.get_item(py, 2)?.extract::<PyBytes>(py)?;
207 207 let source = get_path_from_bytes(source.data(py));
208 208 let new = IgnorePattern::new(syntax, pattern, source);
209 209 Ok(new)
210 210 })
211 211 .collect();
212 212
213 213 let ignore_patterns = ignore_patterns?;
214 214
215 215 let matcher = IncludeMatcher::new(ignore_patterns)
216 216 .map_err(|e| handle_fallback(py, e.into()))?;
217 217
218 218 Ok(Box::new(matcher))
219 219 }
220 "unionmatcher" => {
221 let matchers: PyResult<Vec<_>> = matcher
222 .getattr(py, "_matchers")?
223 .iter(py)?
224 .map(|py_matcher| extract_matcher(py, py_matcher?))
225 .collect();
226
227 Ok(Box::new(UnionMatcher::new(matchers?)))
228 }
220 229 e => Err(PyErr::new::<FallbackError, _>(
221 230 py,
222 231 format!("Unsupported matcher {}", e),
223 232 )),
224 233 }
225 234 }
226 235
227 236 fn build_response(
228 237 py: Python,
229 238 status_res: DirstateStatus,
230 239 warnings: Vec<PatternFileWarning>,
231 240 ) -> PyResult<PyTuple> {
232 241 let modified = collect_status_path_list(py, &status_res.modified);
233 242 let added = collect_status_path_list(py, &status_res.added);
234 243 let removed = collect_status_path_list(py, &status_res.removed);
235 244 let deleted = collect_status_path_list(py, &status_res.deleted);
236 245 let clean = collect_status_path_list(py, &status_res.clean);
237 246 let ignored = collect_status_path_list(py, &status_res.ignored);
238 247 let unknown = collect_status_path_list(py, &status_res.unknown);
239 248 let unsure = collect_status_path_list(py, &status_res.unsure);
240 249 let bad = collect_bad_matches(py, &status_res.bad)?;
241 250 let traversed = collect_pybytes_list(py, status_res.traversed.iter());
242 251 let dirty = status_res.dirty.to_py_object(py);
243 252 let py_warnings = PyList::new(py, &[]);
244 253 for warning in warnings.iter() {
245 254 // We use duck-typing on the Python side for dispatch, good enough for
246 255 // now.
247 256 match warning {
248 257 PatternFileWarning::InvalidSyntax(file, syn) => {
249 258 py_warnings.append(
250 259 py,
251 260 (
252 261 PyBytes::new(py, &get_bytes_from_path(&file)),
253 262 PyBytes::new(py, syn),
254 263 )
255 264 .to_py_object(py)
256 265 .into_object(),
257 266 );
258 267 }
259 268 PatternFileWarning::NoSuchFile(file) => py_warnings.append(
260 269 py,
261 270 PyBytes::new(py, &get_bytes_from_path(&file)).into_object(),
262 271 ),
263 272 }
264 273 }
265 274
266 275 Ok(PyTuple::new(
267 276 py,
268 277 &[
269 278 unsure.into_object(),
270 279 modified.into_object(),
271 280 added.into_object(),
272 281 removed.into_object(),
273 282 deleted.into_object(),
274 283 clean.into_object(),
275 284 ignored.into_object(),
276 285 unknown.into_object(),
277 286 py_warnings.into_object(),
278 287 bad.into_object(),
279 288 traversed.into_object(),
280 289 dirty.into_object(),
281 290 ][..],
282 291 ))
283 292 }
General Comments 0
You need to be logged in to leave comments. Login now