##// END OF EJS Templates
context: use `dirstate.set_tracked` in `context.add`...
marmoute -
r48394:0cef28b1 default
parent child Browse files
Show More
@@ -1,3124 +1,3120 b''
1 1 # context.py - changeset and file context objects for mercurial
2 2 #
3 3 # Copyright 2006, 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 errno
11 11 import filecmp
12 12 import os
13 13 import stat
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 hex,
18 18 nullrev,
19 19 short,
20 20 )
21 21 from .pycompat import (
22 22 getattr,
23 23 open,
24 24 )
25 25 from . import (
26 26 dagop,
27 27 encoding,
28 28 error,
29 29 fileset,
30 30 match as matchmod,
31 31 mergestate as mergestatemod,
32 32 metadata,
33 33 obsolete as obsmod,
34 34 patch,
35 35 pathutil,
36 36 phases,
37 37 pycompat,
38 38 repoview,
39 39 scmutil,
40 40 sparse,
41 41 subrepo,
42 42 subrepoutil,
43 43 util,
44 44 )
45 45 from .utils import (
46 46 dateutil,
47 47 stringutil,
48 48 )
49 49
50 50 propertycache = util.propertycache
51 51
52 52
53 53 class basectx(object):
54 54 """A basectx object represents the common logic for its children:
55 55 changectx: read-only context that is already present in the repo,
56 56 workingctx: a context that represents the working directory and can
57 57 be committed,
58 58 memctx: a context that represents changes in-memory and can also
59 59 be committed."""
60 60
61 61 def __init__(self, repo):
62 62 self._repo = repo
63 63
64 64 def __bytes__(self):
65 65 return short(self.node())
66 66
67 67 __str__ = encoding.strmethod(__bytes__)
68 68
69 69 def __repr__(self):
70 70 return "<%s %s>" % (type(self).__name__, str(self))
71 71
72 72 def __eq__(self, other):
73 73 try:
74 74 return type(self) == type(other) and self._rev == other._rev
75 75 except AttributeError:
76 76 return False
77 77
78 78 def __ne__(self, other):
79 79 return not (self == other)
80 80
81 81 def __contains__(self, key):
82 82 return key in self._manifest
83 83
84 84 def __getitem__(self, key):
85 85 return self.filectx(key)
86 86
87 87 def __iter__(self):
88 88 return iter(self._manifest)
89 89
90 90 def _buildstatusmanifest(self, status):
91 91 """Builds a manifest that includes the given status results, if this is
92 92 a working copy context. For non-working copy contexts, it just returns
93 93 the normal manifest."""
94 94 return self.manifest()
95 95
96 96 def _matchstatus(self, other, match):
97 97 """This internal method provides a way for child objects to override the
98 98 match operator.
99 99 """
100 100 return match
101 101
102 102 def _buildstatus(
103 103 self, other, s, match, listignored, listclean, listunknown
104 104 ):
105 105 """build a status with respect to another context"""
106 106 # Load earliest manifest first for caching reasons. More specifically,
107 107 # if you have revisions 1000 and 1001, 1001 is probably stored as a
108 108 # delta against 1000. Thus, if you read 1000 first, we'll reconstruct
109 109 # 1000 and cache it so that when you read 1001, we just need to apply a
110 110 # delta to what's in the cache. So that's one full reconstruction + one
111 111 # delta application.
112 112 mf2 = None
113 113 if self.rev() is not None and self.rev() < other.rev():
114 114 mf2 = self._buildstatusmanifest(s)
115 115 mf1 = other._buildstatusmanifest(s)
116 116 if mf2 is None:
117 117 mf2 = self._buildstatusmanifest(s)
118 118
119 119 modified, added = [], []
120 120 removed = []
121 121 clean = []
122 122 deleted, unknown, ignored = s.deleted, s.unknown, s.ignored
123 123 deletedset = set(deleted)
124 124 d = mf1.diff(mf2, match=match, clean=listclean)
125 125 for fn, value in pycompat.iteritems(d):
126 126 if fn in deletedset:
127 127 continue
128 128 if value is None:
129 129 clean.append(fn)
130 130 continue
131 131 (node1, flag1), (node2, flag2) = value
132 132 if node1 is None:
133 133 added.append(fn)
134 134 elif node2 is None:
135 135 removed.append(fn)
136 136 elif flag1 != flag2:
137 137 modified.append(fn)
138 138 elif node2 not in self._repo.nodeconstants.wdirfilenodeids:
139 139 # When comparing files between two commits, we save time by
140 140 # not comparing the file contents when the nodeids differ.
141 141 # Note that this means we incorrectly report a reverted change
142 142 # to a file as a modification.
143 143 modified.append(fn)
144 144 elif self[fn].cmp(other[fn]):
145 145 modified.append(fn)
146 146 else:
147 147 clean.append(fn)
148 148
149 149 if removed:
150 150 # need to filter files if they are already reported as removed
151 151 unknown = [
152 152 fn
153 153 for fn in unknown
154 154 if fn not in mf1 and (not match or match(fn))
155 155 ]
156 156 ignored = [
157 157 fn
158 158 for fn in ignored
159 159 if fn not in mf1 and (not match or match(fn))
160 160 ]
161 161 # if they're deleted, don't report them as removed
162 162 removed = [fn for fn in removed if fn not in deletedset]
163 163
164 164 return scmutil.status(
165 165 modified, added, removed, deleted, unknown, ignored, clean
166 166 )
167 167
168 168 @propertycache
169 169 def substate(self):
170 170 return subrepoutil.state(self, self._repo.ui)
171 171
172 172 def subrev(self, subpath):
173 173 return self.substate[subpath][1]
174 174
175 175 def rev(self):
176 176 return self._rev
177 177
178 178 def node(self):
179 179 return self._node
180 180
181 181 def hex(self):
182 182 return hex(self.node())
183 183
184 184 def manifest(self):
185 185 return self._manifest
186 186
187 187 def manifestctx(self):
188 188 return self._manifestctx
189 189
190 190 def repo(self):
191 191 return self._repo
192 192
193 193 def phasestr(self):
194 194 return phases.phasenames[self.phase()]
195 195
196 196 def mutable(self):
197 197 return self.phase() > phases.public
198 198
199 199 def matchfileset(self, cwd, expr, badfn=None):
200 200 return fileset.match(self, cwd, expr, badfn=badfn)
201 201
202 202 def obsolete(self):
203 203 """True if the changeset is obsolete"""
204 204 return self.rev() in obsmod.getrevs(self._repo, b'obsolete')
205 205
206 206 def extinct(self):
207 207 """True if the changeset is extinct"""
208 208 return self.rev() in obsmod.getrevs(self._repo, b'extinct')
209 209
210 210 def orphan(self):
211 211 """True if the changeset is not obsolete, but its ancestor is"""
212 212 return self.rev() in obsmod.getrevs(self._repo, b'orphan')
213 213
214 214 def phasedivergent(self):
215 215 """True if the changeset tries to be a successor of a public changeset
216 216
217 217 Only non-public and non-obsolete changesets may be phase-divergent.
218 218 """
219 219 return self.rev() in obsmod.getrevs(self._repo, b'phasedivergent')
220 220
221 221 def contentdivergent(self):
222 222 """Is a successor of a changeset with multiple possible successor sets
223 223
224 224 Only non-public and non-obsolete changesets may be content-divergent.
225 225 """
226 226 return self.rev() in obsmod.getrevs(self._repo, b'contentdivergent')
227 227
228 228 def isunstable(self):
229 229 """True if the changeset is either orphan, phase-divergent or
230 230 content-divergent"""
231 231 return self.orphan() or self.phasedivergent() or self.contentdivergent()
232 232
233 233 def instabilities(self):
234 234 """return the list of instabilities affecting this changeset.
235 235
236 236 Instabilities are returned as strings. possible values are:
237 237 - orphan,
238 238 - phase-divergent,
239 239 - content-divergent.
240 240 """
241 241 instabilities = []
242 242 if self.orphan():
243 243 instabilities.append(b'orphan')
244 244 if self.phasedivergent():
245 245 instabilities.append(b'phase-divergent')
246 246 if self.contentdivergent():
247 247 instabilities.append(b'content-divergent')
248 248 return instabilities
249 249
250 250 def parents(self):
251 251 """return contexts for each parent changeset"""
252 252 return self._parents
253 253
254 254 def p1(self):
255 255 return self._parents[0]
256 256
257 257 def p2(self):
258 258 parents = self._parents
259 259 if len(parents) == 2:
260 260 return parents[1]
261 261 return self._repo[nullrev]
262 262
263 263 def _fileinfo(self, path):
264 264 if '_manifest' in self.__dict__:
265 265 try:
266 266 return self._manifest.find(path)
267 267 except KeyError:
268 268 raise error.ManifestLookupError(
269 269 self._node or b'None', path, _(b'not found in manifest')
270 270 )
271 271 if '_manifestdelta' in self.__dict__ or path in self.files():
272 272 if path in self._manifestdelta:
273 273 return (
274 274 self._manifestdelta[path],
275 275 self._manifestdelta.flags(path),
276 276 )
277 277 mfl = self._repo.manifestlog
278 278 try:
279 279 node, flag = mfl[self._changeset.manifest].find(path)
280 280 except KeyError:
281 281 raise error.ManifestLookupError(
282 282 self._node or b'None', path, _(b'not found in manifest')
283 283 )
284 284
285 285 return node, flag
286 286
287 287 def filenode(self, path):
288 288 return self._fileinfo(path)[0]
289 289
290 290 def flags(self, path):
291 291 try:
292 292 return self._fileinfo(path)[1]
293 293 except error.LookupError:
294 294 return b''
295 295
296 296 @propertycache
297 297 def _copies(self):
298 298 return metadata.computechangesetcopies(self)
299 299
300 300 def p1copies(self):
301 301 return self._copies[0]
302 302
303 303 def p2copies(self):
304 304 return self._copies[1]
305 305
306 306 def sub(self, path, allowcreate=True):
307 307 '''return a subrepo for the stored revision of path, never wdir()'''
308 308 return subrepo.subrepo(self, path, allowcreate=allowcreate)
309 309
310 310 def nullsub(self, path, pctx):
311 311 return subrepo.nullsubrepo(self, path, pctx)
312 312
313 313 def workingsub(self, path):
314 314 """return a subrepo for the stored revision, or wdir if this is a wdir
315 315 context.
316 316 """
317 317 return subrepo.subrepo(self, path, allowwdir=True)
318 318
319 319 def match(
320 320 self,
321 321 pats=None,
322 322 include=None,
323 323 exclude=None,
324 324 default=b'glob',
325 325 listsubrepos=False,
326 326 badfn=None,
327 327 cwd=None,
328 328 ):
329 329 r = self._repo
330 330 if not cwd:
331 331 cwd = r.getcwd()
332 332 return matchmod.match(
333 333 r.root,
334 334 cwd,
335 335 pats,
336 336 include,
337 337 exclude,
338 338 default,
339 339 auditor=r.nofsauditor,
340 340 ctx=self,
341 341 listsubrepos=listsubrepos,
342 342 badfn=badfn,
343 343 )
344 344
345 345 def diff(
346 346 self,
347 347 ctx2=None,
348 348 match=None,
349 349 changes=None,
350 350 opts=None,
351 351 losedatafn=None,
352 352 pathfn=None,
353 353 copy=None,
354 354 copysourcematch=None,
355 355 hunksfilterfn=None,
356 356 ):
357 357 """Returns a diff generator for the given contexts and matcher"""
358 358 if ctx2 is None:
359 359 ctx2 = self.p1()
360 360 if ctx2 is not None:
361 361 ctx2 = self._repo[ctx2]
362 362 return patch.diff(
363 363 self._repo,
364 364 ctx2,
365 365 self,
366 366 match=match,
367 367 changes=changes,
368 368 opts=opts,
369 369 losedatafn=losedatafn,
370 370 pathfn=pathfn,
371 371 copy=copy,
372 372 copysourcematch=copysourcematch,
373 373 hunksfilterfn=hunksfilterfn,
374 374 )
375 375
376 376 def dirs(self):
377 377 return self._manifest.dirs()
378 378
379 379 def hasdir(self, dir):
380 380 return self._manifest.hasdir(dir)
381 381
382 382 def status(
383 383 self,
384 384 other=None,
385 385 match=None,
386 386 listignored=False,
387 387 listclean=False,
388 388 listunknown=False,
389 389 listsubrepos=False,
390 390 ):
391 391 """return status of files between two nodes or node and working
392 392 directory.
393 393
394 394 If other is None, compare this node with working directory.
395 395
396 396 ctx1.status(ctx2) returns the status of change from ctx1 to ctx2
397 397
398 398 Returns a mercurial.scmutils.status object.
399 399
400 400 Data can be accessed using either tuple notation:
401 401
402 402 (modified, added, removed, deleted, unknown, ignored, clean)
403 403
404 404 or direct attribute access:
405 405
406 406 s.modified, s.added, ...
407 407 """
408 408
409 409 ctx1 = self
410 410 ctx2 = self._repo[other]
411 411
412 412 # This next code block is, admittedly, fragile logic that tests for
413 413 # reversing the contexts and wouldn't need to exist if it weren't for
414 414 # the fast (and common) code path of comparing the working directory
415 415 # with its first parent.
416 416 #
417 417 # What we're aiming for here is the ability to call:
418 418 #
419 419 # workingctx.status(parentctx)
420 420 #
421 421 # If we always built the manifest for each context and compared those,
422 422 # then we'd be done. But the special case of the above call means we
423 423 # just copy the manifest of the parent.
424 424 reversed = False
425 425 if not isinstance(ctx1, changectx) and isinstance(ctx2, changectx):
426 426 reversed = True
427 427 ctx1, ctx2 = ctx2, ctx1
428 428
429 429 match = self._repo.narrowmatch(match)
430 430 match = ctx2._matchstatus(ctx1, match)
431 431 r = scmutil.status([], [], [], [], [], [], [])
432 432 r = ctx2._buildstatus(
433 433 ctx1, r, match, listignored, listclean, listunknown
434 434 )
435 435
436 436 if reversed:
437 437 # Reverse added and removed. Clear deleted, unknown and ignored as
438 438 # these make no sense to reverse.
439 439 r = scmutil.status(
440 440 r.modified, r.removed, r.added, [], [], [], r.clean
441 441 )
442 442
443 443 if listsubrepos:
444 444 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
445 445 try:
446 446 rev2 = ctx2.subrev(subpath)
447 447 except KeyError:
448 448 # A subrepo that existed in node1 was deleted between
449 449 # node1 and node2 (inclusive). Thus, ctx2's substate
450 450 # won't contain that subpath. The best we can do ignore it.
451 451 rev2 = None
452 452 submatch = matchmod.subdirmatcher(subpath, match)
453 453 s = sub.status(
454 454 rev2,
455 455 match=submatch,
456 456 ignored=listignored,
457 457 clean=listclean,
458 458 unknown=listunknown,
459 459 listsubrepos=True,
460 460 )
461 461 for k in (
462 462 'modified',
463 463 'added',
464 464 'removed',
465 465 'deleted',
466 466 'unknown',
467 467 'ignored',
468 468 'clean',
469 469 ):
470 470 rfiles, sfiles = getattr(r, k), getattr(s, k)
471 471 rfiles.extend(b"%s/%s" % (subpath, f) for f in sfiles)
472 472
473 473 r.modified.sort()
474 474 r.added.sort()
475 475 r.removed.sort()
476 476 r.deleted.sort()
477 477 r.unknown.sort()
478 478 r.ignored.sort()
479 479 r.clean.sort()
480 480
481 481 return r
482 482
483 483 def mergestate(self, clean=False):
484 484 """Get a mergestate object for this context."""
485 485 raise NotImplementedError(
486 486 '%s does not implement mergestate()' % self.__class__
487 487 )
488 488
489 489 def isempty(self):
490 490 return not (
491 491 len(self.parents()) > 1
492 492 or self.branch() != self.p1().branch()
493 493 or self.closesbranch()
494 494 or self.files()
495 495 )
496 496
497 497
498 498 class changectx(basectx):
499 499 """A changecontext object makes access to data related to a particular
500 500 changeset convenient. It represents a read-only context already present in
501 501 the repo."""
502 502
503 503 def __init__(self, repo, rev, node, maybe_filtered=True):
504 504 super(changectx, self).__init__(repo)
505 505 self._rev = rev
506 506 self._node = node
507 507 # When maybe_filtered is True, the revision might be affected by
508 508 # changelog filtering and operation through the filtered changelog must be used.
509 509 #
510 510 # When maybe_filtered is False, the revision has already been checked
511 511 # against filtering and is not filtered. Operation through the
512 512 # unfiltered changelog might be used in some case.
513 513 self._maybe_filtered = maybe_filtered
514 514
515 515 def __hash__(self):
516 516 try:
517 517 return hash(self._rev)
518 518 except AttributeError:
519 519 return id(self)
520 520
521 521 def __nonzero__(self):
522 522 return self._rev != nullrev
523 523
524 524 __bool__ = __nonzero__
525 525
526 526 @propertycache
527 527 def _changeset(self):
528 528 if self._maybe_filtered:
529 529 repo = self._repo
530 530 else:
531 531 repo = self._repo.unfiltered()
532 532 return repo.changelog.changelogrevision(self.rev())
533 533
534 534 @propertycache
535 535 def _manifest(self):
536 536 return self._manifestctx.read()
537 537
538 538 @property
539 539 def _manifestctx(self):
540 540 return self._repo.manifestlog[self._changeset.manifest]
541 541
542 542 @propertycache
543 543 def _manifestdelta(self):
544 544 return self._manifestctx.readdelta()
545 545
546 546 @propertycache
547 547 def _parents(self):
548 548 repo = self._repo
549 549 if self._maybe_filtered:
550 550 cl = repo.changelog
551 551 else:
552 552 cl = repo.unfiltered().changelog
553 553
554 554 p1, p2 = cl.parentrevs(self._rev)
555 555 if p2 == nullrev:
556 556 return [changectx(repo, p1, cl.node(p1), maybe_filtered=False)]
557 557 return [
558 558 changectx(repo, p1, cl.node(p1), maybe_filtered=False),
559 559 changectx(repo, p2, cl.node(p2), maybe_filtered=False),
560 560 ]
561 561
562 562 def changeset(self):
563 563 c = self._changeset
564 564 return (
565 565 c.manifest,
566 566 c.user,
567 567 c.date,
568 568 c.files,
569 569 c.description,
570 570 c.extra,
571 571 )
572 572
573 573 def manifestnode(self):
574 574 return self._changeset.manifest
575 575
576 576 def user(self):
577 577 return self._changeset.user
578 578
579 579 def date(self):
580 580 return self._changeset.date
581 581
582 582 def files(self):
583 583 return self._changeset.files
584 584
585 585 def filesmodified(self):
586 586 modified = set(self.files())
587 587 modified.difference_update(self.filesadded())
588 588 modified.difference_update(self.filesremoved())
589 589 return sorted(modified)
590 590
591 591 def filesadded(self):
592 592 filesadded = self._changeset.filesadded
593 593 compute_on_none = True
594 594 if self._repo.filecopiesmode == b'changeset-sidedata':
595 595 compute_on_none = False
596 596 else:
597 597 source = self._repo.ui.config(b'experimental', b'copies.read-from')
598 598 if source == b'changeset-only':
599 599 compute_on_none = False
600 600 elif source != b'compatibility':
601 601 # filelog mode, ignore any changelog content
602 602 filesadded = None
603 603 if filesadded is None:
604 604 if compute_on_none:
605 605 filesadded = metadata.computechangesetfilesadded(self)
606 606 else:
607 607 filesadded = []
608 608 return filesadded
609 609
610 610 def filesremoved(self):
611 611 filesremoved = self._changeset.filesremoved
612 612 compute_on_none = True
613 613 if self._repo.filecopiesmode == b'changeset-sidedata':
614 614 compute_on_none = False
615 615 else:
616 616 source = self._repo.ui.config(b'experimental', b'copies.read-from')
617 617 if source == b'changeset-only':
618 618 compute_on_none = False
619 619 elif source != b'compatibility':
620 620 # filelog mode, ignore any changelog content
621 621 filesremoved = None
622 622 if filesremoved is None:
623 623 if compute_on_none:
624 624 filesremoved = metadata.computechangesetfilesremoved(self)
625 625 else:
626 626 filesremoved = []
627 627 return filesremoved
628 628
629 629 @propertycache
630 630 def _copies(self):
631 631 p1copies = self._changeset.p1copies
632 632 p2copies = self._changeset.p2copies
633 633 compute_on_none = True
634 634 if self._repo.filecopiesmode == b'changeset-sidedata':
635 635 compute_on_none = False
636 636 else:
637 637 source = self._repo.ui.config(b'experimental', b'copies.read-from')
638 638 # If config says to get copy metadata only from changeset, then
639 639 # return that, defaulting to {} if there was no copy metadata. In
640 640 # compatibility mode, we return copy data from the changeset if it
641 641 # was recorded there, and otherwise we fall back to getting it from
642 642 # the filelogs (below).
643 643 #
644 644 # If we are in compatiblity mode and there is not data in the
645 645 # changeset), we get the copy metadata from the filelogs.
646 646 #
647 647 # otherwise, when config said to read only from filelog, we get the
648 648 # copy metadata from the filelogs.
649 649 if source == b'changeset-only':
650 650 compute_on_none = False
651 651 elif source != b'compatibility':
652 652 # filelog mode, ignore any changelog content
653 653 p1copies = p2copies = None
654 654 if p1copies is None:
655 655 if compute_on_none:
656 656 p1copies, p2copies = super(changectx, self)._copies
657 657 else:
658 658 if p1copies is None:
659 659 p1copies = {}
660 660 if p2copies is None:
661 661 p2copies = {}
662 662 return p1copies, p2copies
663 663
664 664 def description(self):
665 665 return self._changeset.description
666 666
667 667 def branch(self):
668 668 return encoding.tolocal(self._changeset.extra.get(b"branch"))
669 669
670 670 def closesbranch(self):
671 671 return b'close' in self._changeset.extra
672 672
673 673 def extra(self):
674 674 """Return a dict of extra information."""
675 675 return self._changeset.extra
676 676
677 677 def tags(self):
678 678 """Return a list of byte tag names"""
679 679 return self._repo.nodetags(self._node)
680 680
681 681 def bookmarks(self):
682 682 """Return a list of byte bookmark names."""
683 683 return self._repo.nodebookmarks(self._node)
684 684
685 685 def phase(self):
686 686 return self._repo._phasecache.phase(self._repo, self._rev)
687 687
688 688 def hidden(self):
689 689 return self._rev in repoview.filterrevs(self._repo, b'visible')
690 690
691 691 def isinmemory(self):
692 692 return False
693 693
694 694 def children(self):
695 695 """return list of changectx contexts for each child changeset.
696 696
697 697 This returns only the immediate child changesets. Use descendants() to
698 698 recursively walk children.
699 699 """
700 700 c = self._repo.changelog.children(self._node)
701 701 return [self._repo[x] for x in c]
702 702
703 703 def ancestors(self):
704 704 for a in self._repo.changelog.ancestors([self._rev]):
705 705 yield self._repo[a]
706 706
707 707 def descendants(self):
708 708 """Recursively yield all children of the changeset.
709 709
710 710 For just the immediate children, use children()
711 711 """
712 712 for d in self._repo.changelog.descendants([self._rev]):
713 713 yield self._repo[d]
714 714
715 715 def filectx(self, path, fileid=None, filelog=None):
716 716 """get a file context from this changeset"""
717 717 if fileid is None:
718 718 fileid = self.filenode(path)
719 719 return filectx(
720 720 self._repo, path, fileid=fileid, changectx=self, filelog=filelog
721 721 )
722 722
723 723 def ancestor(self, c2, warn=False):
724 724 """return the "best" ancestor context of self and c2
725 725
726 726 If there are multiple candidates, it will show a message and check
727 727 merge.preferancestor configuration before falling back to the
728 728 revlog ancestor."""
729 729 # deal with workingctxs
730 730 n2 = c2._node
731 731 if n2 is None:
732 732 n2 = c2._parents[0]._node
733 733 cahs = self._repo.changelog.commonancestorsheads(self._node, n2)
734 734 if not cahs:
735 735 anc = self._repo.nodeconstants.nullid
736 736 elif len(cahs) == 1:
737 737 anc = cahs[0]
738 738 else:
739 739 # experimental config: merge.preferancestor
740 740 for r in self._repo.ui.configlist(b'merge', b'preferancestor'):
741 741 try:
742 742 ctx = scmutil.revsymbol(self._repo, r)
743 743 except error.RepoLookupError:
744 744 continue
745 745 anc = ctx.node()
746 746 if anc in cahs:
747 747 break
748 748 else:
749 749 anc = self._repo.changelog.ancestor(self._node, n2)
750 750 if warn:
751 751 self._repo.ui.status(
752 752 (
753 753 _(b"note: using %s as ancestor of %s and %s\n")
754 754 % (short(anc), short(self._node), short(n2))
755 755 )
756 756 + b''.join(
757 757 _(
758 758 b" alternatively, use --config "
759 759 b"merge.preferancestor=%s\n"
760 760 )
761 761 % short(n)
762 762 for n in sorted(cahs)
763 763 if n != anc
764 764 )
765 765 )
766 766 return self._repo[anc]
767 767
768 768 def isancestorof(self, other):
769 769 """True if this changeset is an ancestor of other"""
770 770 return self._repo.changelog.isancestorrev(self._rev, other._rev)
771 771
772 772 def walk(self, match):
773 773 '''Generates matching file names.'''
774 774
775 775 # Wrap match.bad method to have message with nodeid
776 776 def bad(fn, msg):
777 777 # The manifest doesn't know about subrepos, so don't complain about
778 778 # paths into valid subrepos.
779 779 if any(fn == s or fn.startswith(s + b'/') for s in self.substate):
780 780 return
781 781 match.bad(fn, _(b'no such file in rev %s') % self)
782 782
783 783 m = matchmod.badmatch(self._repo.narrowmatch(match), bad)
784 784 return self._manifest.walk(m)
785 785
786 786 def matches(self, match):
787 787 return self.walk(match)
788 788
789 789
790 790 class basefilectx(object):
791 791 """A filecontext object represents the common logic for its children:
792 792 filectx: read-only access to a filerevision that is already present
793 793 in the repo,
794 794 workingfilectx: a filecontext that represents files from the working
795 795 directory,
796 796 memfilectx: a filecontext that represents files in-memory,
797 797 """
798 798
799 799 @propertycache
800 800 def _filelog(self):
801 801 return self._repo.file(self._path)
802 802
803 803 @propertycache
804 804 def _changeid(self):
805 805 if '_changectx' in self.__dict__:
806 806 return self._changectx.rev()
807 807 elif '_descendantrev' in self.__dict__:
808 808 # this file context was created from a revision with a known
809 809 # descendant, we can (lazily) correct for linkrev aliases
810 810 return self._adjustlinkrev(self._descendantrev)
811 811 else:
812 812 return self._filelog.linkrev(self._filerev)
813 813
814 814 @propertycache
815 815 def _filenode(self):
816 816 if '_fileid' in self.__dict__:
817 817 return self._filelog.lookup(self._fileid)
818 818 else:
819 819 return self._changectx.filenode(self._path)
820 820
821 821 @propertycache
822 822 def _filerev(self):
823 823 return self._filelog.rev(self._filenode)
824 824
825 825 @propertycache
826 826 def _repopath(self):
827 827 return self._path
828 828
829 829 def __nonzero__(self):
830 830 try:
831 831 self._filenode
832 832 return True
833 833 except error.LookupError:
834 834 # file is missing
835 835 return False
836 836
837 837 __bool__ = __nonzero__
838 838
839 839 def __bytes__(self):
840 840 try:
841 841 return b"%s@%s" % (self.path(), self._changectx)
842 842 except error.LookupError:
843 843 return b"%s@???" % self.path()
844 844
845 845 __str__ = encoding.strmethod(__bytes__)
846 846
847 847 def __repr__(self):
848 848 return "<%s %s>" % (type(self).__name__, str(self))
849 849
850 850 def __hash__(self):
851 851 try:
852 852 return hash((self._path, self._filenode))
853 853 except AttributeError:
854 854 return id(self)
855 855
856 856 def __eq__(self, other):
857 857 try:
858 858 return (
859 859 type(self) == type(other)
860 860 and self._path == other._path
861 861 and self._filenode == other._filenode
862 862 )
863 863 except AttributeError:
864 864 return False
865 865
866 866 def __ne__(self, other):
867 867 return not (self == other)
868 868
869 869 def filerev(self):
870 870 return self._filerev
871 871
872 872 def filenode(self):
873 873 return self._filenode
874 874
875 875 @propertycache
876 876 def _flags(self):
877 877 return self._changectx.flags(self._path)
878 878
879 879 def flags(self):
880 880 return self._flags
881 881
882 882 def filelog(self):
883 883 return self._filelog
884 884
885 885 def rev(self):
886 886 return self._changeid
887 887
888 888 def linkrev(self):
889 889 return self._filelog.linkrev(self._filerev)
890 890
891 891 def node(self):
892 892 return self._changectx.node()
893 893
894 894 def hex(self):
895 895 return self._changectx.hex()
896 896
897 897 def user(self):
898 898 return self._changectx.user()
899 899
900 900 def date(self):
901 901 return self._changectx.date()
902 902
903 903 def files(self):
904 904 return self._changectx.files()
905 905
906 906 def description(self):
907 907 return self._changectx.description()
908 908
909 909 def branch(self):
910 910 return self._changectx.branch()
911 911
912 912 def extra(self):
913 913 return self._changectx.extra()
914 914
915 915 def phase(self):
916 916 return self._changectx.phase()
917 917
918 918 def phasestr(self):
919 919 return self._changectx.phasestr()
920 920
921 921 def obsolete(self):
922 922 return self._changectx.obsolete()
923 923
924 924 def instabilities(self):
925 925 return self._changectx.instabilities()
926 926
927 927 def manifest(self):
928 928 return self._changectx.manifest()
929 929
930 930 def changectx(self):
931 931 return self._changectx
932 932
933 933 def renamed(self):
934 934 return self._copied
935 935
936 936 def copysource(self):
937 937 return self._copied and self._copied[0]
938 938
939 939 def repo(self):
940 940 return self._repo
941 941
942 942 def size(self):
943 943 return len(self.data())
944 944
945 945 def path(self):
946 946 return self._path
947 947
948 948 def isbinary(self):
949 949 try:
950 950 return stringutil.binary(self.data())
951 951 except IOError:
952 952 return False
953 953
954 954 def isexec(self):
955 955 return b'x' in self.flags()
956 956
957 957 def islink(self):
958 958 return b'l' in self.flags()
959 959
960 960 def isabsent(self):
961 961 """whether this filectx represents a file not in self._changectx
962 962
963 963 This is mainly for merge code to detect change/delete conflicts. This is
964 964 expected to be True for all subclasses of basectx."""
965 965 return False
966 966
967 967 _customcmp = False
968 968
969 969 def cmp(self, fctx):
970 970 """compare with other file context
971 971
972 972 returns True if different than fctx.
973 973 """
974 974 if fctx._customcmp:
975 975 return fctx.cmp(self)
976 976
977 977 if self._filenode is None:
978 978 raise error.ProgrammingError(
979 979 b'filectx.cmp() must be reimplemented if not backed by revlog'
980 980 )
981 981
982 982 if fctx._filenode is None:
983 983 if self._repo._encodefilterpats:
984 984 # can't rely on size() because wdir content may be decoded
985 985 return self._filelog.cmp(self._filenode, fctx.data())
986 986 if self.size() - 4 == fctx.size():
987 987 # size() can match:
988 988 # if file data starts with '\1\n', empty metadata block is
989 989 # prepended, which adds 4 bytes to filelog.size().
990 990 return self._filelog.cmp(self._filenode, fctx.data())
991 991 if self.size() == fctx.size() or self.flags() == b'l':
992 992 # size() matches: need to compare content
993 993 # issue6456: Always compare symlinks because size can represent
994 994 # encrypted string for EXT-4 encryption(fscrypt).
995 995 return self._filelog.cmp(self._filenode, fctx.data())
996 996
997 997 # size() differs
998 998 return True
999 999
1000 1000 def _adjustlinkrev(self, srcrev, inclusive=False, stoprev=None):
1001 1001 """return the first ancestor of <srcrev> introducing <fnode>
1002 1002
1003 1003 If the linkrev of the file revision does not point to an ancestor of
1004 1004 srcrev, we'll walk down the ancestors until we find one introducing
1005 1005 this file revision.
1006 1006
1007 1007 :srcrev: the changeset revision we search ancestors from
1008 1008 :inclusive: if true, the src revision will also be checked
1009 1009 :stoprev: an optional revision to stop the walk at. If no introduction
1010 1010 of this file content could be found before this floor
1011 1011 revision, the function will returns "None" and stops its
1012 1012 iteration.
1013 1013 """
1014 1014 repo = self._repo
1015 1015 cl = repo.unfiltered().changelog
1016 1016 mfl = repo.manifestlog
1017 1017 # fetch the linkrev
1018 1018 lkr = self.linkrev()
1019 1019 if srcrev == lkr:
1020 1020 return lkr
1021 1021 # hack to reuse ancestor computation when searching for renames
1022 1022 memberanc = getattr(self, '_ancestrycontext', None)
1023 1023 iteranc = None
1024 1024 if srcrev is None:
1025 1025 # wctx case, used by workingfilectx during mergecopy
1026 1026 revs = [p.rev() for p in self._repo[None].parents()]
1027 1027 inclusive = True # we skipped the real (revless) source
1028 1028 else:
1029 1029 revs = [srcrev]
1030 1030 if memberanc is None:
1031 1031 memberanc = iteranc = cl.ancestors(revs, lkr, inclusive=inclusive)
1032 1032 # check if this linkrev is an ancestor of srcrev
1033 1033 if lkr not in memberanc:
1034 1034 if iteranc is None:
1035 1035 iteranc = cl.ancestors(revs, lkr, inclusive=inclusive)
1036 1036 fnode = self._filenode
1037 1037 path = self._path
1038 1038 for a in iteranc:
1039 1039 if stoprev is not None and a < stoprev:
1040 1040 return None
1041 1041 ac = cl.read(a) # get changeset data (we avoid object creation)
1042 1042 if path in ac[3]: # checking the 'files' field.
1043 1043 # The file has been touched, check if the content is
1044 1044 # similar to the one we search for.
1045 1045 if fnode == mfl[ac[0]].readfast().get(path):
1046 1046 return a
1047 1047 # In theory, we should never get out of that loop without a result.
1048 1048 # But if manifest uses a buggy file revision (not children of the
1049 1049 # one it replaces) we could. Such a buggy situation will likely
1050 1050 # result is crash somewhere else at to some point.
1051 1051 return lkr
1052 1052
1053 1053 def isintroducedafter(self, changelogrev):
1054 1054 """True if a filectx has been introduced after a given floor revision"""
1055 1055 if self.linkrev() >= changelogrev:
1056 1056 return True
1057 1057 introrev = self._introrev(stoprev=changelogrev)
1058 1058 if introrev is None:
1059 1059 return False
1060 1060 return introrev >= changelogrev
1061 1061
1062 1062 def introrev(self):
1063 1063 """return the rev of the changeset which introduced this file revision
1064 1064
1065 1065 This method is different from linkrev because it take into account the
1066 1066 changeset the filectx was created from. It ensures the returned
1067 1067 revision is one of its ancestors. This prevents bugs from
1068 1068 'linkrev-shadowing' when a file revision is used by multiple
1069 1069 changesets.
1070 1070 """
1071 1071 return self._introrev()
1072 1072
1073 1073 def _introrev(self, stoprev=None):
1074 1074 """
1075 1075 Same as `introrev` but, with an extra argument to limit changelog
1076 1076 iteration range in some internal usecase.
1077 1077
1078 1078 If `stoprev` is set, the `introrev` will not be searched past that
1079 1079 `stoprev` revision and "None" might be returned. This is useful to
1080 1080 limit the iteration range.
1081 1081 """
1082 1082 toprev = None
1083 1083 attrs = vars(self)
1084 1084 if '_changeid' in attrs:
1085 1085 # We have a cached value already
1086 1086 toprev = self._changeid
1087 1087 elif '_changectx' in attrs:
1088 1088 # We know which changelog entry we are coming from
1089 1089 toprev = self._changectx.rev()
1090 1090
1091 1091 if toprev is not None:
1092 1092 return self._adjustlinkrev(toprev, inclusive=True, stoprev=stoprev)
1093 1093 elif '_descendantrev' in attrs:
1094 1094 introrev = self._adjustlinkrev(self._descendantrev, stoprev=stoprev)
1095 1095 # be nice and cache the result of the computation
1096 1096 if introrev is not None:
1097 1097 self._changeid = introrev
1098 1098 return introrev
1099 1099 else:
1100 1100 return self.linkrev()
1101 1101
1102 1102 def introfilectx(self):
1103 1103 """Return filectx having identical contents, but pointing to the
1104 1104 changeset revision where this filectx was introduced"""
1105 1105 introrev = self.introrev()
1106 1106 if self.rev() == introrev:
1107 1107 return self
1108 1108 return self.filectx(self.filenode(), changeid=introrev)
1109 1109
1110 1110 def _parentfilectx(self, path, fileid, filelog):
1111 1111 """create parent filectx keeping ancestry info for _adjustlinkrev()"""
1112 1112 fctx = filectx(self._repo, path, fileid=fileid, filelog=filelog)
1113 1113 if '_changeid' in vars(self) or '_changectx' in vars(self):
1114 1114 # If self is associated with a changeset (probably explicitly
1115 1115 # fed), ensure the created filectx is associated with a
1116 1116 # changeset that is an ancestor of self.changectx.
1117 1117 # This lets us later use _adjustlinkrev to get a correct link.
1118 1118 fctx._descendantrev = self.rev()
1119 1119 fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
1120 1120 elif '_descendantrev' in vars(self):
1121 1121 # Otherwise propagate _descendantrev if we have one associated.
1122 1122 fctx._descendantrev = self._descendantrev
1123 1123 fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
1124 1124 return fctx
1125 1125
1126 1126 def parents(self):
1127 1127 _path = self._path
1128 1128 fl = self._filelog
1129 1129 parents = self._filelog.parents(self._filenode)
1130 1130 pl = [
1131 1131 (_path, node, fl)
1132 1132 for node in parents
1133 1133 if node != self._repo.nodeconstants.nullid
1134 1134 ]
1135 1135
1136 1136 r = fl.renamed(self._filenode)
1137 1137 if r:
1138 1138 # - In the simple rename case, both parent are nullid, pl is empty.
1139 1139 # - In case of merge, only one of the parent is null id and should
1140 1140 # be replaced with the rename information. This parent is -always-
1141 1141 # the first one.
1142 1142 #
1143 1143 # As null id have always been filtered out in the previous list
1144 1144 # comprehension, inserting to 0 will always result in "replacing
1145 1145 # first nullid parent with rename information.
1146 1146 pl.insert(0, (r[0], r[1], self._repo.file(r[0])))
1147 1147
1148 1148 return [self._parentfilectx(path, fnode, l) for path, fnode, l in pl]
1149 1149
1150 1150 def p1(self):
1151 1151 return self.parents()[0]
1152 1152
1153 1153 def p2(self):
1154 1154 p = self.parents()
1155 1155 if len(p) == 2:
1156 1156 return p[1]
1157 1157 return filectx(self._repo, self._path, fileid=-1, filelog=self._filelog)
1158 1158
1159 1159 def annotate(self, follow=False, skiprevs=None, diffopts=None):
1160 1160 """Returns a list of annotateline objects for each line in the file
1161 1161
1162 1162 - line.fctx is the filectx of the node where that line was last changed
1163 1163 - line.lineno is the line number at the first appearance in the managed
1164 1164 file
1165 1165 - line.text is the data on that line (including newline character)
1166 1166 """
1167 1167 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
1168 1168
1169 1169 def parents(f):
1170 1170 # Cut _descendantrev here to mitigate the penalty of lazy linkrev
1171 1171 # adjustment. Otherwise, p._adjustlinkrev() would walk changelog
1172 1172 # from the topmost introrev (= srcrev) down to p.linkrev() if it
1173 1173 # isn't an ancestor of the srcrev.
1174 1174 f._changeid
1175 1175 pl = f.parents()
1176 1176
1177 1177 # Don't return renamed parents if we aren't following.
1178 1178 if not follow:
1179 1179 pl = [p for p in pl if p.path() == f.path()]
1180 1180
1181 1181 # renamed filectx won't have a filelog yet, so set it
1182 1182 # from the cache to save time
1183 1183 for p in pl:
1184 1184 if not '_filelog' in p.__dict__:
1185 1185 p._filelog = getlog(p.path())
1186 1186
1187 1187 return pl
1188 1188
1189 1189 # use linkrev to find the first changeset where self appeared
1190 1190 base = self.introfilectx()
1191 1191 if getattr(base, '_ancestrycontext', None) is None:
1192 1192 # it is safe to use an unfiltered repository here because we are
1193 1193 # walking ancestors only.
1194 1194 cl = self._repo.unfiltered().changelog
1195 1195 if base.rev() is None:
1196 1196 # wctx is not inclusive, but works because _ancestrycontext
1197 1197 # is used to test filelog revisions
1198 1198 ac = cl.ancestors(
1199 1199 [p.rev() for p in base.parents()], inclusive=True
1200 1200 )
1201 1201 else:
1202 1202 ac = cl.ancestors([base.rev()], inclusive=True)
1203 1203 base._ancestrycontext = ac
1204 1204
1205 1205 return dagop.annotate(
1206 1206 base, parents, skiprevs=skiprevs, diffopts=diffopts
1207 1207 )
1208 1208
1209 1209 def ancestors(self, followfirst=False):
1210 1210 visit = {}
1211 1211 c = self
1212 1212 if followfirst:
1213 1213 cut = 1
1214 1214 else:
1215 1215 cut = None
1216 1216
1217 1217 while True:
1218 1218 for parent in c.parents()[:cut]:
1219 1219 visit[(parent.linkrev(), parent.filenode())] = parent
1220 1220 if not visit:
1221 1221 break
1222 1222 c = visit.pop(max(visit))
1223 1223 yield c
1224 1224
1225 1225 def decodeddata(self):
1226 1226 """Returns `data()` after running repository decoding filters.
1227 1227
1228 1228 This is often equivalent to how the data would be expressed on disk.
1229 1229 """
1230 1230 return self._repo.wwritedata(self.path(), self.data())
1231 1231
1232 1232
1233 1233 class filectx(basefilectx):
1234 1234 """A filecontext object makes access to data related to a particular
1235 1235 filerevision convenient."""
1236 1236
1237 1237 def __init__(
1238 1238 self,
1239 1239 repo,
1240 1240 path,
1241 1241 changeid=None,
1242 1242 fileid=None,
1243 1243 filelog=None,
1244 1244 changectx=None,
1245 1245 ):
1246 1246 """changeid must be a revision number, if specified.
1247 1247 fileid can be a file revision or node."""
1248 1248 self._repo = repo
1249 1249 self._path = path
1250 1250
1251 1251 assert (
1252 1252 changeid is not None or fileid is not None or changectx is not None
1253 1253 ), b"bad args: changeid=%r, fileid=%r, changectx=%r" % (
1254 1254 changeid,
1255 1255 fileid,
1256 1256 changectx,
1257 1257 )
1258 1258
1259 1259 if filelog is not None:
1260 1260 self._filelog = filelog
1261 1261
1262 1262 if changeid is not None:
1263 1263 self._changeid = changeid
1264 1264 if changectx is not None:
1265 1265 self._changectx = changectx
1266 1266 if fileid is not None:
1267 1267 self._fileid = fileid
1268 1268
1269 1269 @propertycache
1270 1270 def _changectx(self):
1271 1271 try:
1272 1272 return self._repo[self._changeid]
1273 1273 except error.FilteredRepoLookupError:
1274 1274 # Linkrev may point to any revision in the repository. When the
1275 1275 # repository is filtered this may lead to `filectx` trying to build
1276 1276 # `changectx` for filtered revision. In such case we fallback to
1277 1277 # creating `changectx` on the unfiltered version of the reposition.
1278 1278 # This fallback should not be an issue because `changectx` from
1279 1279 # `filectx` are not used in complex operations that care about
1280 1280 # filtering.
1281 1281 #
1282 1282 # This fallback is a cheap and dirty fix that prevent several
1283 1283 # crashes. It does not ensure the behavior is correct. However the
1284 1284 # behavior was not correct before filtering either and "incorrect
1285 1285 # behavior" is seen as better as "crash"
1286 1286 #
1287 1287 # Linkrevs have several serious troubles with filtering that are
1288 1288 # complicated to solve. Proper handling of the issue here should be
1289 1289 # considered when solving linkrev issue are on the table.
1290 1290 return self._repo.unfiltered()[self._changeid]
1291 1291
1292 1292 def filectx(self, fileid, changeid=None):
1293 1293 """opens an arbitrary revision of the file without
1294 1294 opening a new filelog"""
1295 1295 return filectx(
1296 1296 self._repo,
1297 1297 self._path,
1298 1298 fileid=fileid,
1299 1299 filelog=self._filelog,
1300 1300 changeid=changeid,
1301 1301 )
1302 1302
1303 1303 def rawdata(self):
1304 1304 return self._filelog.rawdata(self._filenode)
1305 1305
1306 1306 def rawflags(self):
1307 1307 """low-level revlog flags"""
1308 1308 return self._filelog.flags(self._filerev)
1309 1309
1310 1310 def data(self):
1311 1311 try:
1312 1312 return self._filelog.read(self._filenode)
1313 1313 except error.CensoredNodeError:
1314 1314 if self._repo.ui.config(b"censor", b"policy") == b"ignore":
1315 1315 return b""
1316 1316 raise error.Abort(
1317 1317 _(b"censored node: %s") % short(self._filenode),
1318 1318 hint=_(b"set censor.policy to ignore errors"),
1319 1319 )
1320 1320
1321 1321 def size(self):
1322 1322 return self._filelog.size(self._filerev)
1323 1323
1324 1324 @propertycache
1325 1325 def _copied(self):
1326 1326 """check if file was actually renamed in this changeset revision
1327 1327
1328 1328 If rename logged in file revision, we report copy for changeset only
1329 1329 if file revisions linkrev points back to the changeset in question
1330 1330 or both changeset parents contain different file revisions.
1331 1331 """
1332 1332
1333 1333 renamed = self._filelog.renamed(self._filenode)
1334 1334 if not renamed:
1335 1335 return None
1336 1336
1337 1337 if self.rev() == self.linkrev():
1338 1338 return renamed
1339 1339
1340 1340 name = self.path()
1341 1341 fnode = self._filenode
1342 1342 for p in self._changectx.parents():
1343 1343 try:
1344 1344 if fnode == p.filenode(name):
1345 1345 return None
1346 1346 except error.LookupError:
1347 1347 pass
1348 1348 return renamed
1349 1349
1350 1350 def children(self):
1351 1351 # hard for renames
1352 1352 c = self._filelog.children(self._filenode)
1353 1353 return [
1354 1354 filectx(self._repo, self._path, fileid=x, filelog=self._filelog)
1355 1355 for x in c
1356 1356 ]
1357 1357
1358 1358
1359 1359 class committablectx(basectx):
1360 1360 """A committablectx object provides common functionality for a context that
1361 1361 wants the ability to commit, e.g. workingctx or memctx."""
1362 1362
1363 1363 def __init__(
1364 1364 self,
1365 1365 repo,
1366 1366 text=b"",
1367 1367 user=None,
1368 1368 date=None,
1369 1369 extra=None,
1370 1370 changes=None,
1371 1371 branch=None,
1372 1372 ):
1373 1373 super(committablectx, self).__init__(repo)
1374 1374 self._rev = None
1375 1375 self._node = None
1376 1376 self._text = text
1377 1377 if date:
1378 1378 self._date = dateutil.parsedate(date)
1379 1379 if user:
1380 1380 self._user = user
1381 1381 if changes:
1382 1382 self._status = changes
1383 1383
1384 1384 self._extra = {}
1385 1385 if extra:
1386 1386 self._extra = extra.copy()
1387 1387 if branch is not None:
1388 1388 self._extra[b'branch'] = encoding.fromlocal(branch)
1389 1389 if not self._extra.get(b'branch'):
1390 1390 self._extra[b'branch'] = b'default'
1391 1391
1392 1392 def __bytes__(self):
1393 1393 return bytes(self._parents[0]) + b"+"
1394 1394
1395 1395 def hex(self):
1396 1396 self._repo.nodeconstants.wdirhex
1397 1397
1398 1398 __str__ = encoding.strmethod(__bytes__)
1399 1399
1400 1400 def __nonzero__(self):
1401 1401 return True
1402 1402
1403 1403 __bool__ = __nonzero__
1404 1404
1405 1405 @propertycache
1406 1406 def _status(self):
1407 1407 return self._repo.status()
1408 1408
1409 1409 @propertycache
1410 1410 def _user(self):
1411 1411 return self._repo.ui.username()
1412 1412
1413 1413 @propertycache
1414 1414 def _date(self):
1415 1415 ui = self._repo.ui
1416 1416 date = ui.configdate(b'devel', b'default-date')
1417 1417 if date is None:
1418 1418 date = dateutil.makedate()
1419 1419 return date
1420 1420
1421 1421 def subrev(self, subpath):
1422 1422 return None
1423 1423
1424 1424 def manifestnode(self):
1425 1425 return None
1426 1426
1427 1427 def user(self):
1428 1428 return self._user or self._repo.ui.username()
1429 1429
1430 1430 def date(self):
1431 1431 return self._date
1432 1432
1433 1433 def description(self):
1434 1434 return self._text
1435 1435
1436 1436 def files(self):
1437 1437 return sorted(
1438 1438 self._status.modified + self._status.added + self._status.removed
1439 1439 )
1440 1440
1441 1441 def modified(self):
1442 1442 return self._status.modified
1443 1443
1444 1444 def added(self):
1445 1445 return self._status.added
1446 1446
1447 1447 def removed(self):
1448 1448 return self._status.removed
1449 1449
1450 1450 def deleted(self):
1451 1451 return self._status.deleted
1452 1452
1453 1453 filesmodified = modified
1454 1454 filesadded = added
1455 1455 filesremoved = removed
1456 1456
1457 1457 def branch(self):
1458 1458 return encoding.tolocal(self._extra[b'branch'])
1459 1459
1460 1460 def closesbranch(self):
1461 1461 return b'close' in self._extra
1462 1462
1463 1463 def extra(self):
1464 1464 return self._extra
1465 1465
1466 1466 def isinmemory(self):
1467 1467 return False
1468 1468
1469 1469 def tags(self):
1470 1470 return []
1471 1471
1472 1472 def bookmarks(self):
1473 1473 b = []
1474 1474 for p in self.parents():
1475 1475 b.extend(p.bookmarks())
1476 1476 return b
1477 1477
1478 1478 def phase(self):
1479 1479 phase = phases.newcommitphase(self._repo.ui)
1480 1480 for p in self.parents():
1481 1481 phase = max(phase, p.phase())
1482 1482 return phase
1483 1483
1484 1484 def hidden(self):
1485 1485 return False
1486 1486
1487 1487 def children(self):
1488 1488 return []
1489 1489
1490 1490 def flags(self, path):
1491 1491 if '_manifest' in self.__dict__:
1492 1492 try:
1493 1493 return self._manifest.flags(path)
1494 1494 except KeyError:
1495 1495 return b''
1496 1496
1497 1497 try:
1498 1498 return self._flagfunc(path)
1499 1499 except OSError:
1500 1500 return b''
1501 1501
1502 1502 def ancestor(self, c2):
1503 1503 """return the "best" ancestor context of self and c2"""
1504 1504 return self._parents[0].ancestor(c2) # punt on two parents for now
1505 1505
1506 1506 def ancestors(self):
1507 1507 for p in self._parents:
1508 1508 yield p
1509 1509 for a in self._repo.changelog.ancestors(
1510 1510 [p.rev() for p in self._parents]
1511 1511 ):
1512 1512 yield self._repo[a]
1513 1513
1514 1514 def markcommitted(self, node):
1515 1515 """Perform post-commit cleanup necessary after committing this ctx
1516 1516
1517 1517 Specifically, this updates backing stores this working context
1518 1518 wraps to reflect the fact that the changes reflected by this
1519 1519 workingctx have been committed. For example, it marks
1520 1520 modified and added files as normal in the dirstate.
1521 1521
1522 1522 """
1523 1523
1524 1524 def dirty(self, missing=False, merge=True, branch=True):
1525 1525 return False
1526 1526
1527 1527
1528 1528 class workingctx(committablectx):
1529 1529 """A workingctx object makes access to data related to
1530 1530 the current working directory convenient.
1531 1531 date - any valid date string or (unixtime, offset), or None.
1532 1532 user - username string, or None.
1533 1533 extra - a dictionary of extra values, or None.
1534 1534 changes - a list of file lists as returned by localrepo.status()
1535 1535 or None to use the repository status.
1536 1536 """
1537 1537
1538 1538 def __init__(
1539 1539 self, repo, text=b"", user=None, date=None, extra=None, changes=None
1540 1540 ):
1541 1541 branch = None
1542 1542 if not extra or b'branch' not in extra:
1543 1543 try:
1544 1544 branch = repo.dirstate.branch()
1545 1545 except UnicodeDecodeError:
1546 1546 raise error.Abort(_(b'branch name not in UTF-8!'))
1547 1547 super(workingctx, self).__init__(
1548 1548 repo, text, user, date, extra, changes, branch=branch
1549 1549 )
1550 1550
1551 1551 def __iter__(self):
1552 1552 d = self._repo.dirstate
1553 1553 for f in d:
1554 1554 if d[f] != b'r':
1555 1555 yield f
1556 1556
1557 1557 def __contains__(self, key):
1558 1558 return self._repo.dirstate[key] not in b"?r"
1559 1559
1560 1560 def hex(self):
1561 1561 return self._repo.nodeconstants.wdirhex
1562 1562
1563 1563 @propertycache
1564 1564 def _parents(self):
1565 1565 p = self._repo.dirstate.parents()
1566 1566 if p[1] == self._repo.nodeconstants.nullid:
1567 1567 p = p[:-1]
1568 1568 # use unfiltered repo to delay/avoid loading obsmarkers
1569 1569 unfi = self._repo.unfiltered()
1570 1570 return [
1571 1571 changectx(
1572 1572 self._repo, unfi.changelog.rev(n), n, maybe_filtered=False
1573 1573 )
1574 1574 for n in p
1575 1575 ]
1576 1576
1577 1577 def setparents(self, p1node, p2node=None):
1578 1578 if p2node is None:
1579 1579 p2node = self._repo.nodeconstants.nullid
1580 1580 dirstate = self._repo.dirstate
1581 1581 with dirstate.parentchange():
1582 1582 copies = dirstate.setparents(p1node, p2node)
1583 1583 pctx = self._repo[p1node]
1584 1584 if copies:
1585 1585 # Adjust copy records, the dirstate cannot do it, it
1586 1586 # requires access to parents manifests. Preserve them
1587 1587 # only for entries added to first parent.
1588 1588 for f in copies:
1589 1589 if f not in pctx and copies[f] in pctx:
1590 1590 dirstate.copy(copies[f], f)
1591 1591 if p2node == self._repo.nodeconstants.nullid:
1592 1592 for f, s in sorted(dirstate.copies().items()):
1593 1593 if f not in pctx and s not in pctx:
1594 1594 dirstate.copy(None, f)
1595 1595
1596 1596 def _fileinfo(self, path):
1597 1597 # populate __dict__['_manifest'] as workingctx has no _manifestdelta
1598 1598 self._manifest
1599 1599 return super(workingctx, self)._fileinfo(path)
1600 1600
1601 1601 def _buildflagfunc(self):
1602 1602 # Create a fallback function for getting file flags when the
1603 1603 # filesystem doesn't support them
1604 1604
1605 1605 copiesget = self._repo.dirstate.copies().get
1606 1606 parents = self.parents()
1607 1607 if len(parents) < 2:
1608 1608 # when we have one parent, it's easy: copy from parent
1609 1609 man = parents[0].manifest()
1610 1610
1611 1611 def func(f):
1612 1612 f = copiesget(f, f)
1613 1613 return man.flags(f)
1614 1614
1615 1615 else:
1616 1616 # merges are tricky: we try to reconstruct the unstored
1617 1617 # result from the merge (issue1802)
1618 1618 p1, p2 = parents
1619 1619 pa = p1.ancestor(p2)
1620 1620 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
1621 1621
1622 1622 def func(f):
1623 1623 f = copiesget(f, f) # may be wrong for merges with copies
1624 1624 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
1625 1625 if fl1 == fl2:
1626 1626 return fl1
1627 1627 if fl1 == fla:
1628 1628 return fl2
1629 1629 if fl2 == fla:
1630 1630 return fl1
1631 1631 return b'' # punt for conflicts
1632 1632
1633 1633 return func
1634 1634
1635 1635 @propertycache
1636 1636 def _flagfunc(self):
1637 1637 return self._repo.dirstate.flagfunc(self._buildflagfunc)
1638 1638
1639 1639 def flags(self, path):
1640 1640 try:
1641 1641 return self._flagfunc(path)
1642 1642 except OSError:
1643 1643 return b''
1644 1644
1645 1645 def filectx(self, path, filelog=None):
1646 1646 """get a file context from the working directory"""
1647 1647 return workingfilectx(
1648 1648 self._repo, path, workingctx=self, filelog=filelog
1649 1649 )
1650 1650
1651 1651 def dirty(self, missing=False, merge=True, branch=True):
1652 1652 """check whether a working directory is modified"""
1653 1653 # check subrepos first
1654 1654 for s in sorted(self.substate):
1655 1655 if self.sub(s).dirty(missing=missing):
1656 1656 return True
1657 1657 # check current working dir
1658 1658 return (
1659 1659 (merge and self.p2())
1660 1660 or (branch and self.branch() != self.p1().branch())
1661 1661 or self.modified()
1662 1662 or self.added()
1663 1663 or self.removed()
1664 1664 or (missing and self.deleted())
1665 1665 )
1666 1666
1667 1667 def add(self, list, prefix=b""):
1668 1668 with self._repo.wlock():
1669 1669 ui, ds = self._repo.ui, self._repo.dirstate
1670 1670 uipath = lambda f: ds.pathto(pathutil.join(prefix, f))
1671 1671 rejected = []
1672 1672 lstat = self._repo.wvfs.lstat
1673 1673 for f in list:
1674 1674 # ds.pathto() returns an absolute file when this is invoked from
1675 1675 # the keyword extension. That gets flagged as non-portable on
1676 1676 # Windows, since it contains the drive letter and colon.
1677 1677 scmutil.checkportable(ui, os.path.join(prefix, f))
1678 1678 try:
1679 1679 st = lstat(f)
1680 1680 except OSError:
1681 1681 ui.warn(_(b"%s does not exist!\n") % uipath(f))
1682 1682 rejected.append(f)
1683 1683 continue
1684 1684 limit = ui.configbytes(b'ui', b'large-file-limit')
1685 1685 if limit != 0 and st.st_size > limit:
1686 1686 ui.warn(
1687 1687 _(
1688 1688 b"%s: up to %d MB of RAM may be required "
1689 1689 b"to manage this file\n"
1690 1690 b"(use 'hg revert %s' to cancel the "
1691 1691 b"pending addition)\n"
1692 1692 )
1693 1693 % (f, 3 * st.st_size // 1000000, uipath(f))
1694 1694 )
1695 1695 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1696 1696 ui.warn(
1697 1697 _(
1698 1698 b"%s not added: only files and symlinks "
1699 1699 b"supported currently\n"
1700 1700 )
1701 1701 % uipath(f)
1702 1702 )
1703 1703 rejected.append(f)
1704 elif ds[f] in b'amn':
1704 elif not ds.set_tracked(f):
1705 1705 ui.warn(_(b"%s already tracked!\n") % uipath(f))
1706 elif ds[f] == b'r':
1707 ds.normallookup(f)
1708 else:
1709 ds.add(f)
1710 1706 return rejected
1711 1707
1712 1708 def forget(self, files, prefix=b""):
1713 1709 with self._repo.wlock():
1714 1710 ds = self._repo.dirstate
1715 1711 uipath = lambda f: ds.pathto(pathutil.join(prefix, f))
1716 1712 rejected = []
1717 1713 for f in files:
1718 1714 if f not in ds:
1719 1715 self._repo.ui.warn(_(b"%s not tracked!\n") % uipath(f))
1720 1716 rejected.append(f)
1721 1717 elif ds[f] != b'a':
1722 1718 ds.remove(f)
1723 1719 else:
1724 1720 ds.drop(f)
1725 1721 return rejected
1726 1722
1727 1723 def copy(self, source, dest):
1728 1724 try:
1729 1725 st = self._repo.wvfs.lstat(dest)
1730 1726 except OSError as err:
1731 1727 if err.errno != errno.ENOENT:
1732 1728 raise
1733 1729 self._repo.ui.warn(
1734 1730 _(b"%s does not exist!\n") % self._repo.dirstate.pathto(dest)
1735 1731 )
1736 1732 return
1737 1733 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1738 1734 self._repo.ui.warn(
1739 1735 _(b"copy failed: %s is not a file or a symbolic link\n")
1740 1736 % self._repo.dirstate.pathto(dest)
1741 1737 )
1742 1738 else:
1743 1739 with self._repo.wlock():
1744 1740 ds = self._repo.dirstate
1745 1741 if ds[dest] in b'?':
1746 1742 ds.add(dest)
1747 1743 elif ds[dest] in b'r':
1748 1744 ds.normallookup(dest)
1749 1745 ds.copy(source, dest)
1750 1746
1751 1747 def match(
1752 1748 self,
1753 1749 pats=None,
1754 1750 include=None,
1755 1751 exclude=None,
1756 1752 default=b'glob',
1757 1753 listsubrepos=False,
1758 1754 badfn=None,
1759 1755 cwd=None,
1760 1756 ):
1761 1757 r = self._repo
1762 1758 if not cwd:
1763 1759 cwd = r.getcwd()
1764 1760
1765 1761 # Only a case insensitive filesystem needs magic to translate user input
1766 1762 # to actual case in the filesystem.
1767 1763 icasefs = not util.fscasesensitive(r.root)
1768 1764 return matchmod.match(
1769 1765 r.root,
1770 1766 cwd,
1771 1767 pats,
1772 1768 include,
1773 1769 exclude,
1774 1770 default,
1775 1771 auditor=r.auditor,
1776 1772 ctx=self,
1777 1773 listsubrepos=listsubrepos,
1778 1774 badfn=badfn,
1779 1775 icasefs=icasefs,
1780 1776 )
1781 1777
1782 1778 def _filtersuspectsymlink(self, files):
1783 1779 if not files or self._repo.dirstate._checklink:
1784 1780 return files
1785 1781
1786 1782 # Symlink placeholders may get non-symlink-like contents
1787 1783 # via user error or dereferencing by NFS or Samba servers,
1788 1784 # so we filter out any placeholders that don't look like a
1789 1785 # symlink
1790 1786 sane = []
1791 1787 for f in files:
1792 1788 if self.flags(f) == b'l':
1793 1789 d = self[f].data()
1794 1790 if (
1795 1791 d == b''
1796 1792 or len(d) >= 1024
1797 1793 or b'\n' in d
1798 1794 or stringutil.binary(d)
1799 1795 ):
1800 1796 self._repo.ui.debug(
1801 1797 b'ignoring suspect symlink placeholder "%s"\n' % f
1802 1798 )
1803 1799 continue
1804 1800 sane.append(f)
1805 1801 return sane
1806 1802
1807 1803 def _checklookup(self, files):
1808 1804 # check for any possibly clean files
1809 1805 if not files:
1810 1806 return [], [], []
1811 1807
1812 1808 modified = []
1813 1809 deleted = []
1814 1810 fixup = []
1815 1811 pctx = self._parents[0]
1816 1812 # do a full compare of any files that might have changed
1817 1813 for f in sorted(files):
1818 1814 try:
1819 1815 # This will return True for a file that got replaced by a
1820 1816 # directory in the interim, but fixing that is pretty hard.
1821 1817 if (
1822 1818 f not in pctx
1823 1819 or self.flags(f) != pctx.flags(f)
1824 1820 or pctx[f].cmp(self[f])
1825 1821 ):
1826 1822 modified.append(f)
1827 1823 else:
1828 1824 fixup.append(f)
1829 1825 except (IOError, OSError):
1830 1826 # A file become inaccessible in between? Mark it as deleted,
1831 1827 # matching dirstate behavior (issue5584).
1832 1828 # The dirstate has more complex behavior around whether a
1833 1829 # missing file matches a directory, etc, but we don't need to
1834 1830 # bother with that: if f has made it to this point, we're sure
1835 1831 # it's in the dirstate.
1836 1832 deleted.append(f)
1837 1833
1838 1834 return modified, deleted, fixup
1839 1835
1840 1836 def _poststatusfixup(self, status, fixup):
1841 1837 """update dirstate for files that are actually clean"""
1842 1838 poststatus = self._repo.postdsstatus()
1843 1839 if fixup or poststatus or self._repo.dirstate._dirty:
1844 1840 try:
1845 1841 oldid = self._repo.dirstate.identity()
1846 1842
1847 1843 # updating the dirstate is optional
1848 1844 # so we don't wait on the lock
1849 1845 # wlock can invalidate the dirstate, so cache normal _after_
1850 1846 # taking the lock
1851 1847 with self._repo.wlock(False):
1852 1848 if self._repo.dirstate.identity() == oldid:
1853 1849 if fixup:
1854 1850 normal = self._repo.dirstate.normal
1855 1851 for f in fixup:
1856 1852 normal(f)
1857 1853 # write changes out explicitly, because nesting
1858 1854 # wlock at runtime may prevent 'wlock.release()'
1859 1855 # after this block from doing so for subsequent
1860 1856 # changing files
1861 1857 tr = self._repo.currenttransaction()
1862 1858 self._repo.dirstate.write(tr)
1863 1859
1864 1860 if poststatus:
1865 1861 for ps in poststatus:
1866 1862 ps(self, status)
1867 1863 else:
1868 1864 # in this case, writing changes out breaks
1869 1865 # consistency, because .hg/dirstate was
1870 1866 # already changed simultaneously after last
1871 1867 # caching (see also issue5584 for detail)
1872 1868 self._repo.ui.debug(
1873 1869 b'skip updating dirstate: identity mismatch\n'
1874 1870 )
1875 1871 except error.LockError:
1876 1872 pass
1877 1873 finally:
1878 1874 # Even if the wlock couldn't be grabbed, clear out the list.
1879 1875 self._repo.clearpostdsstatus()
1880 1876
1881 1877 def _dirstatestatus(self, match, ignored=False, clean=False, unknown=False):
1882 1878 '''Gets the status from the dirstate -- internal use only.'''
1883 1879 subrepos = []
1884 1880 if b'.hgsub' in self:
1885 1881 subrepos = sorted(self.substate)
1886 1882 cmp, s = self._repo.dirstate.status(
1887 1883 match, subrepos, ignored=ignored, clean=clean, unknown=unknown
1888 1884 )
1889 1885
1890 1886 # check for any possibly clean files
1891 1887 fixup = []
1892 1888 if cmp:
1893 1889 modified2, deleted2, fixup = self._checklookup(cmp)
1894 1890 s.modified.extend(modified2)
1895 1891 s.deleted.extend(deleted2)
1896 1892
1897 1893 if fixup and clean:
1898 1894 s.clean.extend(fixup)
1899 1895
1900 1896 self._poststatusfixup(s, fixup)
1901 1897
1902 1898 if match.always():
1903 1899 # cache for performance
1904 1900 if s.unknown or s.ignored or s.clean:
1905 1901 # "_status" is cached with list*=False in the normal route
1906 1902 self._status = scmutil.status(
1907 1903 s.modified, s.added, s.removed, s.deleted, [], [], []
1908 1904 )
1909 1905 else:
1910 1906 self._status = s
1911 1907
1912 1908 return s
1913 1909
1914 1910 @propertycache
1915 1911 def _copies(self):
1916 1912 p1copies = {}
1917 1913 p2copies = {}
1918 1914 parents = self._repo.dirstate.parents()
1919 1915 p1manifest = self._repo[parents[0]].manifest()
1920 1916 p2manifest = self._repo[parents[1]].manifest()
1921 1917 changedset = set(self.added()) | set(self.modified())
1922 1918 narrowmatch = self._repo.narrowmatch()
1923 1919 for dst, src in self._repo.dirstate.copies().items():
1924 1920 if dst not in changedset or not narrowmatch(dst):
1925 1921 continue
1926 1922 if src in p1manifest:
1927 1923 p1copies[dst] = src
1928 1924 elif src in p2manifest:
1929 1925 p2copies[dst] = src
1930 1926 return p1copies, p2copies
1931 1927
1932 1928 @propertycache
1933 1929 def _manifest(self):
1934 1930 """generate a manifest corresponding to the values in self._status
1935 1931
1936 1932 This reuse the file nodeid from parent, but we use special node
1937 1933 identifiers for added and modified files. This is used by manifests
1938 1934 merge to see that files are different and by update logic to avoid
1939 1935 deleting newly added files.
1940 1936 """
1941 1937 return self._buildstatusmanifest(self._status)
1942 1938
1943 1939 def _buildstatusmanifest(self, status):
1944 1940 """Builds a manifest that includes the given status results."""
1945 1941 parents = self.parents()
1946 1942
1947 1943 man = parents[0].manifest().copy()
1948 1944
1949 1945 ff = self._flagfunc
1950 1946 for i, l in (
1951 1947 (self._repo.nodeconstants.addednodeid, status.added),
1952 1948 (self._repo.nodeconstants.modifiednodeid, status.modified),
1953 1949 ):
1954 1950 for f in l:
1955 1951 man[f] = i
1956 1952 try:
1957 1953 man.setflag(f, ff(f))
1958 1954 except OSError:
1959 1955 pass
1960 1956
1961 1957 for f in status.deleted + status.removed:
1962 1958 if f in man:
1963 1959 del man[f]
1964 1960
1965 1961 return man
1966 1962
1967 1963 def _buildstatus(
1968 1964 self, other, s, match, listignored, listclean, listunknown
1969 1965 ):
1970 1966 """build a status with respect to another context
1971 1967
1972 1968 This includes logic for maintaining the fast path of status when
1973 1969 comparing the working directory against its parent, which is to skip
1974 1970 building a new manifest if self (working directory) is not comparing
1975 1971 against its parent (repo['.']).
1976 1972 """
1977 1973 s = self._dirstatestatus(match, listignored, listclean, listunknown)
1978 1974 # Filter out symlinks that, in the case of FAT32 and NTFS filesystems,
1979 1975 # might have accidentally ended up with the entire contents of the file
1980 1976 # they are supposed to be linking to.
1981 1977 s.modified[:] = self._filtersuspectsymlink(s.modified)
1982 1978 if other != self._repo[b'.']:
1983 1979 s = super(workingctx, self)._buildstatus(
1984 1980 other, s, match, listignored, listclean, listunknown
1985 1981 )
1986 1982 return s
1987 1983
1988 1984 def _matchstatus(self, other, match):
1989 1985 """override the match method with a filter for directory patterns
1990 1986
1991 1987 We use inheritance to customize the match.bad method only in cases of
1992 1988 workingctx since it belongs only to the working directory when
1993 1989 comparing against the parent changeset.
1994 1990
1995 1991 If we aren't comparing against the working directory's parent, then we
1996 1992 just use the default match object sent to us.
1997 1993 """
1998 1994 if other != self._repo[b'.']:
1999 1995
2000 1996 def bad(f, msg):
2001 1997 # 'f' may be a directory pattern from 'match.files()',
2002 1998 # so 'f not in ctx1' is not enough
2003 1999 if f not in other and not other.hasdir(f):
2004 2000 self._repo.ui.warn(
2005 2001 b'%s: %s\n' % (self._repo.dirstate.pathto(f), msg)
2006 2002 )
2007 2003
2008 2004 match.bad = bad
2009 2005 return match
2010 2006
2011 2007 def walk(self, match):
2012 2008 '''Generates matching file names.'''
2013 2009 return sorted(
2014 2010 self._repo.dirstate.walk(
2015 2011 self._repo.narrowmatch(match),
2016 2012 subrepos=sorted(self.substate),
2017 2013 unknown=True,
2018 2014 ignored=False,
2019 2015 )
2020 2016 )
2021 2017
2022 2018 def matches(self, match):
2023 2019 match = self._repo.narrowmatch(match)
2024 2020 ds = self._repo.dirstate
2025 2021 return sorted(f for f in ds.matches(match) if ds[f] != b'r')
2026 2022
2027 2023 def markcommitted(self, node):
2028 2024 with self._repo.dirstate.parentchange():
2029 2025 for f in self.modified() + self.added():
2030 2026 self._repo.dirstate.normal(f)
2031 2027 for f in self.removed():
2032 2028 self._repo.dirstate.drop(f)
2033 2029 self._repo.dirstate.setparents(node)
2034 2030 self._repo._quick_access_changeid_invalidate()
2035 2031
2036 2032 # write changes out explicitly, because nesting wlock at
2037 2033 # runtime may prevent 'wlock.release()' in 'repo.commit()'
2038 2034 # from immediately doing so for subsequent changing files
2039 2035 self._repo.dirstate.write(self._repo.currenttransaction())
2040 2036
2041 2037 sparse.aftercommit(self._repo, node)
2042 2038
2043 2039 def mergestate(self, clean=False):
2044 2040 if clean:
2045 2041 return mergestatemod.mergestate.clean(self._repo)
2046 2042 return mergestatemod.mergestate.read(self._repo)
2047 2043
2048 2044
2049 2045 class committablefilectx(basefilectx):
2050 2046 """A committablefilectx provides common functionality for a file context
2051 2047 that wants the ability to commit, e.g. workingfilectx or memfilectx."""
2052 2048
2053 2049 def __init__(self, repo, path, filelog=None, ctx=None):
2054 2050 self._repo = repo
2055 2051 self._path = path
2056 2052 self._changeid = None
2057 2053 self._filerev = self._filenode = None
2058 2054
2059 2055 if filelog is not None:
2060 2056 self._filelog = filelog
2061 2057 if ctx:
2062 2058 self._changectx = ctx
2063 2059
2064 2060 def __nonzero__(self):
2065 2061 return True
2066 2062
2067 2063 __bool__ = __nonzero__
2068 2064
2069 2065 def linkrev(self):
2070 2066 # linked to self._changectx no matter if file is modified or not
2071 2067 return self.rev()
2072 2068
2073 2069 def renamed(self):
2074 2070 path = self.copysource()
2075 2071 if not path:
2076 2072 return None
2077 2073 return (
2078 2074 path,
2079 2075 self._changectx._parents[0]._manifest.get(
2080 2076 path, self._repo.nodeconstants.nullid
2081 2077 ),
2082 2078 )
2083 2079
2084 2080 def parents(self):
2085 2081 '''return parent filectxs, following copies if necessary'''
2086 2082
2087 2083 def filenode(ctx, path):
2088 2084 return ctx._manifest.get(path, self._repo.nodeconstants.nullid)
2089 2085
2090 2086 path = self._path
2091 2087 fl = self._filelog
2092 2088 pcl = self._changectx._parents
2093 2089 renamed = self.renamed()
2094 2090
2095 2091 if renamed:
2096 2092 pl = [renamed + (None,)]
2097 2093 else:
2098 2094 pl = [(path, filenode(pcl[0], path), fl)]
2099 2095
2100 2096 for pc in pcl[1:]:
2101 2097 pl.append((path, filenode(pc, path), fl))
2102 2098
2103 2099 return [
2104 2100 self._parentfilectx(p, fileid=n, filelog=l)
2105 2101 for p, n, l in pl
2106 2102 if n != self._repo.nodeconstants.nullid
2107 2103 ]
2108 2104
2109 2105 def children(self):
2110 2106 return []
2111 2107
2112 2108
2113 2109 class workingfilectx(committablefilectx):
2114 2110 """A workingfilectx object makes access to data related to a particular
2115 2111 file in the working directory convenient."""
2116 2112
2117 2113 def __init__(self, repo, path, filelog=None, workingctx=None):
2118 2114 super(workingfilectx, self).__init__(repo, path, filelog, workingctx)
2119 2115
2120 2116 @propertycache
2121 2117 def _changectx(self):
2122 2118 return workingctx(self._repo)
2123 2119
2124 2120 def data(self):
2125 2121 return self._repo.wread(self._path)
2126 2122
2127 2123 def copysource(self):
2128 2124 return self._repo.dirstate.copied(self._path)
2129 2125
2130 2126 def size(self):
2131 2127 return self._repo.wvfs.lstat(self._path).st_size
2132 2128
2133 2129 def lstat(self):
2134 2130 return self._repo.wvfs.lstat(self._path)
2135 2131
2136 2132 def date(self):
2137 2133 t, tz = self._changectx.date()
2138 2134 try:
2139 2135 return (self._repo.wvfs.lstat(self._path)[stat.ST_MTIME], tz)
2140 2136 except OSError as err:
2141 2137 if err.errno != errno.ENOENT:
2142 2138 raise
2143 2139 return (t, tz)
2144 2140
2145 2141 def exists(self):
2146 2142 return self._repo.wvfs.exists(self._path)
2147 2143
2148 2144 def lexists(self):
2149 2145 return self._repo.wvfs.lexists(self._path)
2150 2146
2151 2147 def audit(self):
2152 2148 return self._repo.wvfs.audit(self._path)
2153 2149
2154 2150 def cmp(self, fctx):
2155 2151 """compare with other file context
2156 2152
2157 2153 returns True if different than fctx.
2158 2154 """
2159 2155 # fctx should be a filectx (not a workingfilectx)
2160 2156 # invert comparison to reuse the same code path
2161 2157 return fctx.cmp(self)
2162 2158
2163 2159 def remove(self, ignoremissing=False):
2164 2160 """wraps unlink for a repo's working directory"""
2165 2161 rmdir = self._repo.ui.configbool(b'experimental', b'removeemptydirs')
2166 2162 self._repo.wvfs.unlinkpath(
2167 2163 self._path, ignoremissing=ignoremissing, rmdir=rmdir
2168 2164 )
2169 2165
2170 2166 def write(self, data, flags, backgroundclose=False, **kwargs):
2171 2167 """wraps repo.wwrite"""
2172 2168 return self._repo.wwrite(
2173 2169 self._path, data, flags, backgroundclose=backgroundclose, **kwargs
2174 2170 )
2175 2171
2176 2172 def markcopied(self, src):
2177 2173 """marks this file a copy of `src`"""
2178 2174 self._repo.dirstate.copy(src, self._path)
2179 2175
2180 2176 def clearunknown(self):
2181 2177 """Removes conflicting items in the working directory so that
2182 2178 ``write()`` can be called successfully.
2183 2179 """
2184 2180 wvfs = self._repo.wvfs
2185 2181 f = self._path
2186 2182 wvfs.audit(f)
2187 2183 if self._repo.ui.configbool(
2188 2184 b'experimental', b'merge.checkpathconflicts'
2189 2185 ):
2190 2186 # remove files under the directory as they should already be
2191 2187 # warned and backed up
2192 2188 if wvfs.isdir(f) and not wvfs.islink(f):
2193 2189 wvfs.rmtree(f, forcibly=True)
2194 2190 for p in reversed(list(pathutil.finddirs(f))):
2195 2191 if wvfs.isfileorlink(p):
2196 2192 wvfs.unlink(p)
2197 2193 break
2198 2194 else:
2199 2195 # don't remove files if path conflicts are not processed
2200 2196 if wvfs.isdir(f) and not wvfs.islink(f):
2201 2197 wvfs.removedirs(f)
2202 2198
2203 2199 def setflags(self, l, x):
2204 2200 self._repo.wvfs.setflags(self._path, l, x)
2205 2201
2206 2202
2207 2203 class overlayworkingctx(committablectx):
2208 2204 """Wraps another mutable context with a write-back cache that can be
2209 2205 converted into a commit context.
2210 2206
2211 2207 self._cache[path] maps to a dict with keys: {
2212 2208 'exists': bool?
2213 2209 'date': date?
2214 2210 'data': str?
2215 2211 'flags': str?
2216 2212 'copied': str? (path or None)
2217 2213 }
2218 2214 If `exists` is True, `flags` must be non-None and 'date' is non-None. If it
2219 2215 is `False`, the file was deleted.
2220 2216 """
2221 2217
2222 2218 def __init__(self, repo):
2223 2219 super(overlayworkingctx, self).__init__(repo)
2224 2220 self.clean()
2225 2221
2226 2222 def setbase(self, wrappedctx):
2227 2223 self._wrappedctx = wrappedctx
2228 2224 self._parents = [wrappedctx]
2229 2225 # Drop old manifest cache as it is now out of date.
2230 2226 # This is necessary when, e.g., rebasing several nodes with one
2231 2227 # ``overlayworkingctx`` (e.g. with --collapse).
2232 2228 util.clearcachedproperty(self, b'_manifest')
2233 2229
2234 2230 def setparents(self, p1node, p2node=None):
2235 2231 if p2node is None:
2236 2232 p2node = self._repo.nodeconstants.nullid
2237 2233 assert p1node == self._wrappedctx.node()
2238 2234 self._parents = [self._wrappedctx, self._repo.unfiltered()[p2node]]
2239 2235
2240 2236 def data(self, path):
2241 2237 if self.isdirty(path):
2242 2238 if self._cache[path][b'exists']:
2243 2239 if self._cache[path][b'data'] is not None:
2244 2240 return self._cache[path][b'data']
2245 2241 else:
2246 2242 # Must fallback here, too, because we only set flags.
2247 2243 return self._wrappedctx[path].data()
2248 2244 else:
2249 2245 raise error.ProgrammingError(
2250 2246 b"No such file or directory: %s" % path
2251 2247 )
2252 2248 else:
2253 2249 return self._wrappedctx[path].data()
2254 2250
2255 2251 @propertycache
2256 2252 def _manifest(self):
2257 2253 parents = self.parents()
2258 2254 man = parents[0].manifest().copy()
2259 2255
2260 2256 flag = self._flagfunc
2261 2257 for path in self.added():
2262 2258 man[path] = self._repo.nodeconstants.addednodeid
2263 2259 man.setflag(path, flag(path))
2264 2260 for path in self.modified():
2265 2261 man[path] = self._repo.nodeconstants.modifiednodeid
2266 2262 man.setflag(path, flag(path))
2267 2263 for path in self.removed():
2268 2264 del man[path]
2269 2265 return man
2270 2266
2271 2267 @propertycache
2272 2268 def _flagfunc(self):
2273 2269 def f(path):
2274 2270 return self._cache[path][b'flags']
2275 2271
2276 2272 return f
2277 2273
2278 2274 def files(self):
2279 2275 return sorted(self.added() + self.modified() + self.removed())
2280 2276
2281 2277 def modified(self):
2282 2278 return [
2283 2279 f
2284 2280 for f in self._cache.keys()
2285 2281 if self._cache[f][b'exists'] and self._existsinparent(f)
2286 2282 ]
2287 2283
2288 2284 def added(self):
2289 2285 return [
2290 2286 f
2291 2287 for f in self._cache.keys()
2292 2288 if self._cache[f][b'exists'] and not self._existsinparent(f)
2293 2289 ]
2294 2290
2295 2291 def removed(self):
2296 2292 return [
2297 2293 f
2298 2294 for f in self._cache.keys()
2299 2295 if not self._cache[f][b'exists'] and self._existsinparent(f)
2300 2296 ]
2301 2297
2302 2298 def p1copies(self):
2303 2299 copies = {}
2304 2300 narrowmatch = self._repo.narrowmatch()
2305 2301 for f in self._cache.keys():
2306 2302 if not narrowmatch(f):
2307 2303 continue
2308 2304 copies.pop(f, None) # delete if it exists
2309 2305 source = self._cache[f][b'copied']
2310 2306 if source:
2311 2307 copies[f] = source
2312 2308 return copies
2313 2309
2314 2310 def p2copies(self):
2315 2311 copies = {}
2316 2312 narrowmatch = self._repo.narrowmatch()
2317 2313 for f in self._cache.keys():
2318 2314 if not narrowmatch(f):
2319 2315 continue
2320 2316 copies.pop(f, None) # delete if it exists
2321 2317 source = self._cache[f][b'copied']
2322 2318 if source:
2323 2319 copies[f] = source
2324 2320 return copies
2325 2321
2326 2322 def isinmemory(self):
2327 2323 return True
2328 2324
2329 2325 def filedate(self, path):
2330 2326 if self.isdirty(path):
2331 2327 return self._cache[path][b'date']
2332 2328 else:
2333 2329 return self._wrappedctx[path].date()
2334 2330
2335 2331 def markcopied(self, path, origin):
2336 2332 self._markdirty(
2337 2333 path,
2338 2334 exists=True,
2339 2335 date=self.filedate(path),
2340 2336 flags=self.flags(path),
2341 2337 copied=origin,
2342 2338 )
2343 2339
2344 2340 def copydata(self, path):
2345 2341 if self.isdirty(path):
2346 2342 return self._cache[path][b'copied']
2347 2343 else:
2348 2344 return None
2349 2345
2350 2346 def flags(self, path):
2351 2347 if self.isdirty(path):
2352 2348 if self._cache[path][b'exists']:
2353 2349 return self._cache[path][b'flags']
2354 2350 else:
2355 2351 raise error.ProgrammingError(
2356 2352 b"No such file or directory: %s" % path
2357 2353 )
2358 2354 else:
2359 2355 return self._wrappedctx[path].flags()
2360 2356
2361 2357 def __contains__(self, key):
2362 2358 if key in self._cache:
2363 2359 return self._cache[key][b'exists']
2364 2360 return key in self.p1()
2365 2361
2366 2362 def _existsinparent(self, path):
2367 2363 try:
2368 2364 # ``commitctx` raises a ``ManifestLookupError`` if a path does not
2369 2365 # exist, unlike ``workingctx``, which returns a ``workingfilectx``
2370 2366 # with an ``exists()`` function.
2371 2367 self._wrappedctx[path]
2372 2368 return True
2373 2369 except error.ManifestLookupError:
2374 2370 return False
2375 2371
2376 2372 def _auditconflicts(self, path):
2377 2373 """Replicates conflict checks done by wvfs.write().
2378 2374
2379 2375 Since we never write to the filesystem and never call `applyupdates` in
2380 2376 IMM, we'll never check that a path is actually writable -- e.g., because
2381 2377 it adds `a/foo`, but `a` is actually a file in the other commit.
2382 2378 """
2383 2379
2384 2380 def fail(path, component):
2385 2381 # p1() is the base and we're receiving "writes" for p2()'s
2386 2382 # files.
2387 2383 if b'l' in self.p1()[component].flags():
2388 2384 raise error.Abort(
2389 2385 b"error: %s conflicts with symlink %s "
2390 2386 b"in %d." % (path, component, self.p1().rev())
2391 2387 )
2392 2388 else:
2393 2389 raise error.Abort(
2394 2390 b"error: '%s' conflicts with file '%s' in "
2395 2391 b"%d." % (path, component, self.p1().rev())
2396 2392 )
2397 2393
2398 2394 # Test that each new directory to be created to write this path from p2
2399 2395 # is not a file in p1.
2400 2396 components = path.split(b'/')
2401 2397 for i in pycompat.xrange(len(components)):
2402 2398 component = b"/".join(components[0:i])
2403 2399 if component in self:
2404 2400 fail(path, component)
2405 2401
2406 2402 # Test the other direction -- that this path from p2 isn't a directory
2407 2403 # in p1 (test that p1 doesn't have any paths matching `path/*`).
2408 2404 match = self.match([path], default=b'path')
2409 2405 mfiles = list(self.p1().manifest().walk(match))
2410 2406 if len(mfiles) > 0:
2411 2407 if len(mfiles) == 1 and mfiles[0] == path:
2412 2408 return
2413 2409 # omit the files which are deleted in current IMM wctx
2414 2410 mfiles = [m for m in mfiles if m in self]
2415 2411 if not mfiles:
2416 2412 return
2417 2413 raise error.Abort(
2418 2414 b"error: file '%s' cannot be written because "
2419 2415 b" '%s/' is a directory in %s (containing %d "
2420 2416 b"entries: %s)"
2421 2417 % (path, path, self.p1(), len(mfiles), b', '.join(mfiles))
2422 2418 )
2423 2419
2424 2420 def write(self, path, data, flags=b'', **kwargs):
2425 2421 if data is None:
2426 2422 raise error.ProgrammingError(b"data must be non-None")
2427 2423 self._auditconflicts(path)
2428 2424 self._markdirty(
2429 2425 path, exists=True, data=data, date=dateutil.makedate(), flags=flags
2430 2426 )
2431 2427
2432 2428 def setflags(self, path, l, x):
2433 2429 flag = b''
2434 2430 if l:
2435 2431 flag = b'l'
2436 2432 elif x:
2437 2433 flag = b'x'
2438 2434 self._markdirty(path, exists=True, date=dateutil.makedate(), flags=flag)
2439 2435
2440 2436 def remove(self, path):
2441 2437 self._markdirty(path, exists=False)
2442 2438
2443 2439 def exists(self, path):
2444 2440 """exists behaves like `lexists`, but needs to follow symlinks and
2445 2441 return False if they are broken.
2446 2442 """
2447 2443 if self.isdirty(path):
2448 2444 # If this path exists and is a symlink, "follow" it by calling
2449 2445 # exists on the destination path.
2450 2446 if (
2451 2447 self._cache[path][b'exists']
2452 2448 and b'l' in self._cache[path][b'flags']
2453 2449 ):
2454 2450 return self.exists(self._cache[path][b'data'].strip())
2455 2451 else:
2456 2452 return self._cache[path][b'exists']
2457 2453
2458 2454 return self._existsinparent(path)
2459 2455
2460 2456 def lexists(self, path):
2461 2457 """lexists returns True if the path exists"""
2462 2458 if self.isdirty(path):
2463 2459 return self._cache[path][b'exists']
2464 2460
2465 2461 return self._existsinparent(path)
2466 2462
2467 2463 def size(self, path):
2468 2464 if self.isdirty(path):
2469 2465 if self._cache[path][b'exists']:
2470 2466 return len(self._cache[path][b'data'])
2471 2467 else:
2472 2468 raise error.ProgrammingError(
2473 2469 b"No such file or directory: %s" % path
2474 2470 )
2475 2471 return self._wrappedctx[path].size()
2476 2472
2477 2473 def tomemctx(
2478 2474 self,
2479 2475 text,
2480 2476 branch=None,
2481 2477 extra=None,
2482 2478 date=None,
2483 2479 parents=None,
2484 2480 user=None,
2485 2481 editor=None,
2486 2482 ):
2487 2483 """Converts this ``overlayworkingctx`` into a ``memctx`` ready to be
2488 2484 committed.
2489 2485
2490 2486 ``text`` is the commit message.
2491 2487 ``parents`` (optional) are rev numbers.
2492 2488 """
2493 2489 # Default parents to the wrapped context if not passed.
2494 2490 if parents is None:
2495 2491 parents = self.parents()
2496 2492 if len(parents) == 1:
2497 2493 parents = (parents[0], None)
2498 2494
2499 2495 # ``parents`` is passed as rev numbers; convert to ``commitctxs``.
2500 2496 if parents[1] is None:
2501 2497 parents = (self._repo[parents[0]], None)
2502 2498 else:
2503 2499 parents = (self._repo[parents[0]], self._repo[parents[1]])
2504 2500
2505 2501 files = self.files()
2506 2502
2507 2503 def getfile(repo, memctx, path):
2508 2504 if self._cache[path][b'exists']:
2509 2505 return memfilectx(
2510 2506 repo,
2511 2507 memctx,
2512 2508 path,
2513 2509 self._cache[path][b'data'],
2514 2510 b'l' in self._cache[path][b'flags'],
2515 2511 b'x' in self._cache[path][b'flags'],
2516 2512 self._cache[path][b'copied'],
2517 2513 )
2518 2514 else:
2519 2515 # Returning None, but including the path in `files`, is
2520 2516 # necessary for memctx to register a deletion.
2521 2517 return None
2522 2518
2523 2519 if branch is None:
2524 2520 branch = self._wrappedctx.branch()
2525 2521
2526 2522 return memctx(
2527 2523 self._repo,
2528 2524 parents,
2529 2525 text,
2530 2526 files,
2531 2527 getfile,
2532 2528 date=date,
2533 2529 extra=extra,
2534 2530 user=user,
2535 2531 branch=branch,
2536 2532 editor=editor,
2537 2533 )
2538 2534
2539 2535 def tomemctx_for_amend(self, precursor):
2540 2536 extra = precursor.extra().copy()
2541 2537 extra[b'amend_source'] = precursor.hex()
2542 2538 return self.tomemctx(
2543 2539 text=precursor.description(),
2544 2540 branch=precursor.branch(),
2545 2541 extra=extra,
2546 2542 date=precursor.date(),
2547 2543 user=precursor.user(),
2548 2544 )
2549 2545
2550 2546 def isdirty(self, path):
2551 2547 return path in self._cache
2552 2548
2553 2549 def clean(self):
2554 2550 self._mergestate = None
2555 2551 self._cache = {}
2556 2552
2557 2553 def _compact(self):
2558 2554 """Removes keys from the cache that are actually clean, by comparing
2559 2555 them with the underlying context.
2560 2556
2561 2557 This can occur during the merge process, e.g. by passing --tool :local
2562 2558 to resolve a conflict.
2563 2559 """
2564 2560 keys = []
2565 2561 # This won't be perfect, but can help performance significantly when
2566 2562 # using things like remotefilelog.
2567 2563 scmutil.prefetchfiles(
2568 2564 self.repo(),
2569 2565 [
2570 2566 (
2571 2567 self.p1().rev(),
2572 2568 scmutil.matchfiles(self.repo(), self._cache.keys()),
2573 2569 )
2574 2570 ],
2575 2571 )
2576 2572
2577 2573 for path in self._cache.keys():
2578 2574 cache = self._cache[path]
2579 2575 try:
2580 2576 underlying = self._wrappedctx[path]
2581 2577 if (
2582 2578 underlying.data() == cache[b'data']
2583 2579 and underlying.flags() == cache[b'flags']
2584 2580 ):
2585 2581 keys.append(path)
2586 2582 except error.ManifestLookupError:
2587 2583 # Path not in the underlying manifest (created).
2588 2584 continue
2589 2585
2590 2586 for path in keys:
2591 2587 del self._cache[path]
2592 2588 return keys
2593 2589
2594 2590 def _markdirty(
2595 2591 self, path, exists, data=None, date=None, flags=b'', copied=None
2596 2592 ):
2597 2593 # data not provided, let's see if we already have some; if not, let's
2598 2594 # grab it from our underlying context, so that we always have data if
2599 2595 # the file is marked as existing.
2600 2596 if exists and data is None:
2601 2597 oldentry = self._cache.get(path) or {}
2602 2598 data = oldentry.get(b'data')
2603 2599 if data is None:
2604 2600 data = self._wrappedctx[path].data()
2605 2601
2606 2602 self._cache[path] = {
2607 2603 b'exists': exists,
2608 2604 b'data': data,
2609 2605 b'date': date,
2610 2606 b'flags': flags,
2611 2607 b'copied': copied,
2612 2608 }
2613 2609 util.clearcachedproperty(self, b'_manifest')
2614 2610
2615 2611 def filectx(self, path, filelog=None):
2616 2612 return overlayworkingfilectx(
2617 2613 self._repo, path, parent=self, filelog=filelog
2618 2614 )
2619 2615
2620 2616 def mergestate(self, clean=False):
2621 2617 if clean or self._mergestate is None:
2622 2618 self._mergestate = mergestatemod.memmergestate(self._repo)
2623 2619 return self._mergestate
2624 2620
2625 2621
2626 2622 class overlayworkingfilectx(committablefilectx):
2627 2623 """Wrap a ``workingfilectx`` but intercepts all writes into an in-memory
2628 2624 cache, which can be flushed through later by calling ``flush()``."""
2629 2625
2630 2626 def __init__(self, repo, path, filelog=None, parent=None):
2631 2627 super(overlayworkingfilectx, self).__init__(repo, path, filelog, parent)
2632 2628 self._repo = repo
2633 2629 self._parent = parent
2634 2630 self._path = path
2635 2631
2636 2632 def cmp(self, fctx):
2637 2633 return self.data() != fctx.data()
2638 2634
2639 2635 def changectx(self):
2640 2636 return self._parent
2641 2637
2642 2638 def data(self):
2643 2639 return self._parent.data(self._path)
2644 2640
2645 2641 def date(self):
2646 2642 return self._parent.filedate(self._path)
2647 2643
2648 2644 def exists(self):
2649 2645 return self.lexists()
2650 2646
2651 2647 def lexists(self):
2652 2648 return self._parent.exists(self._path)
2653 2649
2654 2650 def copysource(self):
2655 2651 return self._parent.copydata(self._path)
2656 2652
2657 2653 def size(self):
2658 2654 return self._parent.size(self._path)
2659 2655
2660 2656 def markcopied(self, origin):
2661 2657 self._parent.markcopied(self._path, origin)
2662 2658
2663 2659 def audit(self):
2664 2660 pass
2665 2661
2666 2662 def flags(self):
2667 2663 return self._parent.flags(self._path)
2668 2664
2669 2665 def setflags(self, islink, isexec):
2670 2666 return self._parent.setflags(self._path, islink, isexec)
2671 2667
2672 2668 def write(self, data, flags, backgroundclose=False, **kwargs):
2673 2669 return self._parent.write(self._path, data, flags, **kwargs)
2674 2670
2675 2671 def remove(self, ignoremissing=False):
2676 2672 return self._parent.remove(self._path)
2677 2673
2678 2674 def clearunknown(self):
2679 2675 pass
2680 2676
2681 2677
2682 2678 class workingcommitctx(workingctx):
2683 2679 """A workingcommitctx object makes access to data related to
2684 2680 the revision being committed convenient.
2685 2681
2686 2682 This hides changes in the working directory, if they aren't
2687 2683 committed in this context.
2688 2684 """
2689 2685
2690 2686 def __init__(
2691 2687 self, repo, changes, text=b"", user=None, date=None, extra=None
2692 2688 ):
2693 2689 super(workingcommitctx, self).__init__(
2694 2690 repo, text, user, date, extra, changes
2695 2691 )
2696 2692
2697 2693 def _dirstatestatus(self, match, ignored=False, clean=False, unknown=False):
2698 2694 """Return matched files only in ``self._status``
2699 2695
2700 2696 Uncommitted files appear "clean" via this context, even if
2701 2697 they aren't actually so in the working directory.
2702 2698 """
2703 2699 if clean:
2704 2700 clean = [f for f in self._manifest if f not in self._changedset]
2705 2701 else:
2706 2702 clean = []
2707 2703 return scmutil.status(
2708 2704 [f for f in self._status.modified if match(f)],
2709 2705 [f for f in self._status.added if match(f)],
2710 2706 [f for f in self._status.removed if match(f)],
2711 2707 [],
2712 2708 [],
2713 2709 [],
2714 2710 clean,
2715 2711 )
2716 2712
2717 2713 @propertycache
2718 2714 def _changedset(self):
2719 2715 """Return the set of files changed in this context"""
2720 2716 changed = set(self._status.modified)
2721 2717 changed.update(self._status.added)
2722 2718 changed.update(self._status.removed)
2723 2719 return changed
2724 2720
2725 2721
2726 2722 def makecachingfilectxfn(func):
2727 2723 """Create a filectxfn that caches based on the path.
2728 2724
2729 2725 We can't use util.cachefunc because it uses all arguments as the cache
2730 2726 key and this creates a cycle since the arguments include the repo and
2731 2727 memctx.
2732 2728 """
2733 2729 cache = {}
2734 2730
2735 2731 def getfilectx(repo, memctx, path):
2736 2732 if path not in cache:
2737 2733 cache[path] = func(repo, memctx, path)
2738 2734 return cache[path]
2739 2735
2740 2736 return getfilectx
2741 2737
2742 2738
2743 2739 def memfilefromctx(ctx):
2744 2740 """Given a context return a memfilectx for ctx[path]
2745 2741
2746 2742 This is a convenience method for building a memctx based on another
2747 2743 context.
2748 2744 """
2749 2745
2750 2746 def getfilectx(repo, memctx, path):
2751 2747 fctx = ctx[path]
2752 2748 copysource = fctx.copysource()
2753 2749 return memfilectx(
2754 2750 repo,
2755 2751 memctx,
2756 2752 path,
2757 2753 fctx.data(),
2758 2754 islink=fctx.islink(),
2759 2755 isexec=fctx.isexec(),
2760 2756 copysource=copysource,
2761 2757 )
2762 2758
2763 2759 return getfilectx
2764 2760
2765 2761
2766 2762 def memfilefrompatch(patchstore):
2767 2763 """Given a patch (e.g. patchstore object) return a memfilectx
2768 2764
2769 2765 This is a convenience method for building a memctx based on a patchstore.
2770 2766 """
2771 2767
2772 2768 def getfilectx(repo, memctx, path):
2773 2769 data, mode, copysource = patchstore.getfile(path)
2774 2770 if data is None:
2775 2771 return None
2776 2772 islink, isexec = mode
2777 2773 return memfilectx(
2778 2774 repo,
2779 2775 memctx,
2780 2776 path,
2781 2777 data,
2782 2778 islink=islink,
2783 2779 isexec=isexec,
2784 2780 copysource=copysource,
2785 2781 )
2786 2782
2787 2783 return getfilectx
2788 2784
2789 2785
2790 2786 class memctx(committablectx):
2791 2787 """Use memctx to perform in-memory commits via localrepo.commitctx().
2792 2788
2793 2789 Revision information is supplied at initialization time while
2794 2790 related files data and is made available through a callback
2795 2791 mechanism. 'repo' is the current localrepo, 'parents' is a
2796 2792 sequence of two parent revisions identifiers (pass None for every
2797 2793 missing parent), 'text' is the commit message and 'files' lists
2798 2794 names of files touched by the revision (normalized and relative to
2799 2795 repository root).
2800 2796
2801 2797 filectxfn(repo, memctx, path) is a callable receiving the
2802 2798 repository, the current memctx object and the normalized path of
2803 2799 requested file, relative to repository root. It is fired by the
2804 2800 commit function for every file in 'files', but calls order is
2805 2801 undefined. If the file is available in the revision being
2806 2802 committed (updated or added), filectxfn returns a memfilectx
2807 2803 object. If the file was removed, filectxfn return None for recent
2808 2804 Mercurial. Moved files are represented by marking the source file
2809 2805 removed and the new file added with copy information (see
2810 2806 memfilectx).
2811 2807
2812 2808 user receives the committer name and defaults to current
2813 2809 repository username, date is the commit date in any format
2814 2810 supported by dateutil.parsedate() and defaults to current date, extra
2815 2811 is a dictionary of metadata or is left empty.
2816 2812 """
2817 2813
2818 2814 # Mercurial <= 3.1 expects the filectxfn to raise IOError for missing files.
2819 2815 # Extensions that need to retain compatibility across Mercurial 3.1 can use
2820 2816 # this field to determine what to do in filectxfn.
2821 2817 _returnnoneformissingfiles = True
2822 2818
2823 2819 def __init__(
2824 2820 self,
2825 2821 repo,
2826 2822 parents,
2827 2823 text,
2828 2824 files,
2829 2825 filectxfn,
2830 2826 user=None,
2831 2827 date=None,
2832 2828 extra=None,
2833 2829 branch=None,
2834 2830 editor=None,
2835 2831 ):
2836 2832 super(memctx, self).__init__(
2837 2833 repo, text, user, date, extra, branch=branch
2838 2834 )
2839 2835 self._rev = None
2840 2836 self._node = None
2841 2837 parents = [(p or self._repo.nodeconstants.nullid) for p in parents]
2842 2838 p1, p2 = parents
2843 2839 self._parents = [self._repo[p] for p in (p1, p2)]
2844 2840 files = sorted(set(files))
2845 2841 self._files = files
2846 2842 self.substate = {}
2847 2843
2848 2844 if isinstance(filectxfn, patch.filestore):
2849 2845 filectxfn = memfilefrompatch(filectxfn)
2850 2846 elif not callable(filectxfn):
2851 2847 # if store is not callable, wrap it in a function
2852 2848 filectxfn = memfilefromctx(filectxfn)
2853 2849
2854 2850 # memoizing increases performance for e.g. vcs convert scenarios.
2855 2851 self._filectxfn = makecachingfilectxfn(filectxfn)
2856 2852
2857 2853 if editor:
2858 2854 self._text = editor(self._repo, self, [])
2859 2855 self._repo.savecommitmessage(self._text)
2860 2856
2861 2857 def filectx(self, path, filelog=None):
2862 2858 """get a file context from the working directory
2863 2859
2864 2860 Returns None if file doesn't exist and should be removed."""
2865 2861 return self._filectxfn(self._repo, self, path)
2866 2862
2867 2863 def commit(self):
2868 2864 """commit context to the repo"""
2869 2865 return self._repo.commitctx(self)
2870 2866
2871 2867 @propertycache
2872 2868 def _manifest(self):
2873 2869 """generate a manifest based on the return values of filectxfn"""
2874 2870
2875 2871 # keep this simple for now; just worry about p1
2876 2872 pctx = self._parents[0]
2877 2873 man = pctx.manifest().copy()
2878 2874
2879 2875 for f in self._status.modified:
2880 2876 man[f] = self._repo.nodeconstants.modifiednodeid
2881 2877
2882 2878 for f in self._status.added:
2883 2879 man[f] = self._repo.nodeconstants.addednodeid
2884 2880
2885 2881 for f in self._status.removed:
2886 2882 if f in man:
2887 2883 del man[f]
2888 2884
2889 2885 return man
2890 2886
2891 2887 @propertycache
2892 2888 def _status(self):
2893 2889 """Calculate exact status from ``files`` specified at construction"""
2894 2890 man1 = self.p1().manifest()
2895 2891 p2 = self._parents[1]
2896 2892 # "1 < len(self._parents)" can't be used for checking
2897 2893 # existence of the 2nd parent, because "memctx._parents" is
2898 2894 # explicitly initialized by the list, of which length is 2.
2899 2895 if p2.rev() != nullrev:
2900 2896 man2 = p2.manifest()
2901 2897 managing = lambda f: f in man1 or f in man2
2902 2898 else:
2903 2899 managing = lambda f: f in man1
2904 2900
2905 2901 modified, added, removed = [], [], []
2906 2902 for f in self._files:
2907 2903 if not managing(f):
2908 2904 added.append(f)
2909 2905 elif self[f]:
2910 2906 modified.append(f)
2911 2907 else:
2912 2908 removed.append(f)
2913 2909
2914 2910 return scmutil.status(modified, added, removed, [], [], [], [])
2915 2911
2916 2912 def parents(self):
2917 2913 if self._parents[1].rev() == nullrev:
2918 2914 return [self._parents[0]]
2919 2915 return self._parents
2920 2916
2921 2917
2922 2918 class memfilectx(committablefilectx):
2923 2919 """memfilectx represents an in-memory file to commit.
2924 2920
2925 2921 See memctx and committablefilectx for more details.
2926 2922 """
2927 2923
2928 2924 def __init__(
2929 2925 self,
2930 2926 repo,
2931 2927 changectx,
2932 2928 path,
2933 2929 data,
2934 2930 islink=False,
2935 2931 isexec=False,
2936 2932 copysource=None,
2937 2933 ):
2938 2934 """
2939 2935 path is the normalized file path relative to repository root.
2940 2936 data is the file content as a string.
2941 2937 islink is True if the file is a symbolic link.
2942 2938 isexec is True if the file is executable.
2943 2939 copied is the source file path if current file was copied in the
2944 2940 revision being committed, or None."""
2945 2941 super(memfilectx, self).__init__(repo, path, None, changectx)
2946 2942 self._data = data
2947 2943 if islink:
2948 2944 self._flags = b'l'
2949 2945 elif isexec:
2950 2946 self._flags = b'x'
2951 2947 else:
2952 2948 self._flags = b''
2953 2949 self._copysource = copysource
2954 2950
2955 2951 def copysource(self):
2956 2952 return self._copysource
2957 2953
2958 2954 def cmp(self, fctx):
2959 2955 return self.data() != fctx.data()
2960 2956
2961 2957 def data(self):
2962 2958 return self._data
2963 2959
2964 2960 def remove(self, ignoremissing=False):
2965 2961 """wraps unlink for a repo's working directory"""
2966 2962 # need to figure out what to do here
2967 2963 del self._changectx[self._path]
2968 2964
2969 2965 def write(self, data, flags, **kwargs):
2970 2966 """wraps repo.wwrite"""
2971 2967 self._data = data
2972 2968
2973 2969
2974 2970 class metadataonlyctx(committablectx):
2975 2971 """Like memctx but it's reusing the manifest of different commit.
2976 2972 Intended to be used by lightweight operations that are creating
2977 2973 metadata-only changes.
2978 2974
2979 2975 Revision information is supplied at initialization time. 'repo' is the
2980 2976 current localrepo, 'ctx' is original revision which manifest we're reuisng
2981 2977 'parents' is a sequence of two parent revisions identifiers (pass None for
2982 2978 every missing parent), 'text' is the commit.
2983 2979
2984 2980 user receives the committer name and defaults to current repository
2985 2981 username, date is the commit date in any format supported by
2986 2982 dateutil.parsedate() and defaults to current date, extra is a dictionary of
2987 2983 metadata or is left empty.
2988 2984 """
2989 2985
2990 2986 def __init__(
2991 2987 self,
2992 2988 repo,
2993 2989 originalctx,
2994 2990 parents=None,
2995 2991 text=None,
2996 2992 user=None,
2997 2993 date=None,
2998 2994 extra=None,
2999 2995 editor=None,
3000 2996 ):
3001 2997 if text is None:
3002 2998 text = originalctx.description()
3003 2999 super(metadataonlyctx, self).__init__(repo, text, user, date, extra)
3004 3000 self._rev = None
3005 3001 self._node = None
3006 3002 self._originalctx = originalctx
3007 3003 self._manifestnode = originalctx.manifestnode()
3008 3004 if parents is None:
3009 3005 parents = originalctx.parents()
3010 3006 else:
3011 3007 parents = [repo[p] for p in parents if p is not None]
3012 3008 parents = parents[:]
3013 3009 while len(parents) < 2:
3014 3010 parents.append(repo[nullrev])
3015 3011 p1, p2 = self._parents = parents
3016 3012
3017 3013 # sanity check to ensure that the reused manifest parents are
3018 3014 # manifests of our commit parents
3019 3015 mp1, mp2 = self.manifestctx().parents
3020 3016 if p1 != self._repo.nodeconstants.nullid and p1.manifestnode() != mp1:
3021 3017 raise RuntimeError(
3022 3018 r"can't reuse the manifest: its p1 "
3023 3019 r"doesn't match the new ctx p1"
3024 3020 )
3025 3021 if p2 != self._repo.nodeconstants.nullid and p2.manifestnode() != mp2:
3026 3022 raise RuntimeError(
3027 3023 r"can't reuse the manifest: "
3028 3024 r"its p2 doesn't match the new ctx p2"
3029 3025 )
3030 3026
3031 3027 self._files = originalctx.files()
3032 3028 self.substate = {}
3033 3029
3034 3030 if editor:
3035 3031 self._text = editor(self._repo, self, [])
3036 3032 self._repo.savecommitmessage(self._text)
3037 3033
3038 3034 def manifestnode(self):
3039 3035 return self._manifestnode
3040 3036
3041 3037 @property
3042 3038 def _manifestctx(self):
3043 3039 return self._repo.manifestlog[self._manifestnode]
3044 3040
3045 3041 def filectx(self, path, filelog=None):
3046 3042 return self._originalctx.filectx(path, filelog=filelog)
3047 3043
3048 3044 def commit(self):
3049 3045 """commit context to the repo"""
3050 3046 return self._repo.commitctx(self)
3051 3047
3052 3048 @property
3053 3049 def _manifest(self):
3054 3050 return self._originalctx.manifest()
3055 3051
3056 3052 @propertycache
3057 3053 def _status(self):
3058 3054 """Calculate exact status from ``files`` specified in the ``origctx``
3059 3055 and parents manifests.
3060 3056 """
3061 3057 man1 = self.p1().manifest()
3062 3058 p2 = self._parents[1]
3063 3059 # "1 < len(self._parents)" can't be used for checking
3064 3060 # existence of the 2nd parent, because "metadataonlyctx._parents" is
3065 3061 # explicitly initialized by the list, of which length is 2.
3066 3062 if p2.rev() != nullrev:
3067 3063 man2 = p2.manifest()
3068 3064 managing = lambda f: f in man1 or f in man2
3069 3065 else:
3070 3066 managing = lambda f: f in man1
3071 3067
3072 3068 modified, added, removed = [], [], []
3073 3069 for f in self._files:
3074 3070 if not managing(f):
3075 3071 added.append(f)
3076 3072 elif f in self:
3077 3073 modified.append(f)
3078 3074 else:
3079 3075 removed.append(f)
3080 3076
3081 3077 return scmutil.status(modified, added, removed, [], [], [], [])
3082 3078
3083 3079
3084 3080 class arbitraryfilectx(object):
3085 3081 """Allows you to use filectx-like functions on a file in an arbitrary
3086 3082 location on disk, possibly not in the working directory.
3087 3083 """
3088 3084
3089 3085 def __init__(self, path, repo=None):
3090 3086 # Repo is optional because contrib/simplemerge uses this class.
3091 3087 self._repo = repo
3092 3088 self._path = path
3093 3089
3094 3090 def cmp(self, fctx):
3095 3091 # filecmp follows symlinks whereas `cmp` should not, so skip the fast
3096 3092 # path if either side is a symlink.
3097 3093 symlinks = b'l' in self.flags() or b'l' in fctx.flags()
3098 3094 if not symlinks and isinstance(fctx, workingfilectx) and self._repo:
3099 3095 # Add a fast-path for merge if both sides are disk-backed.
3100 3096 # Note that filecmp uses the opposite return values (True if same)
3101 3097 # from our cmp functions (True if different).
3102 3098 return not filecmp.cmp(self.path(), self._repo.wjoin(fctx.path()))
3103 3099 return self.data() != fctx.data()
3104 3100
3105 3101 def path(self):
3106 3102 return self._path
3107 3103
3108 3104 def flags(self):
3109 3105 return b''
3110 3106
3111 3107 def data(self):
3112 3108 return util.readfile(self._path)
3113 3109
3114 3110 def decodeddata(self):
3115 3111 with open(self._path, b"rb") as f:
3116 3112 return f.read()
3117 3113
3118 3114 def remove(self):
3119 3115 util.unlink(self._path)
3120 3116
3121 3117 def write(self, data, flags, **kwargs):
3122 3118 assert not flags
3123 3119 with open(self._path, b"wb") as f:
3124 3120 f.write(data)
@@ -1,1526 +1,1526 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 if not self.pendingparentchange():
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_parents_change
484 484 def update_file_reference(
485 485 self,
486 486 filename,
487 487 p1_tracked,
488 488 ):
489 489 """Set a file as tracked in the parent (or not)
490 490
491 491 This is to be called when adjust the dirstate to a new parent after an history
492 492 rewriting operation.
493 493
494 494 It should not be called during a merge (p2 != nullid) and only within
495 495 a `with dirstate.parentchange():` context.
496 496 """
497 497 if self.in_merge:
498 498 msg = b'update_file_reference should not be called when merging'
499 499 raise error.ProgrammingError(msg)
500 500 entry = self._map.get(filename)
501 501 if entry is None:
502 502 wc_tracked = False
503 503 else:
504 504 wc_tracked = entry.tracked
505 505 if p1_tracked and wc_tracked:
506 506 # the underlying reference might have changed, we will have to
507 507 # check it.
508 508 self.normallookup(filename)
509 509 elif not (p1_tracked or wc_tracked):
510 510 # the file is no longer relevant to anyone
511 511 self._drop(filename)
512 512 elif (not p1_tracked) and wc_tracked:
513 513 if not entry.added:
514 514 self._add(filename)
515 515 elif p1_tracked and not wc_tracked:
516 516 if entry is None or not entry.removed:
517 517 self._remove(filename)
518 518 else:
519 519 assert False, 'unreachable'
520 520
521 521 def _addpath(
522 522 self,
523 523 f,
524 524 mode=0,
525 525 size=None,
526 526 mtime=None,
527 527 added=False,
528 528 merged=False,
529 529 from_p2=False,
530 530 possibly_dirty=False,
531 531 ):
532 532 entry = self._map.get(f)
533 533 if added or entry is not None and entry.removed:
534 534 scmutil.checkfilename(f)
535 535 if self._map.hastrackeddir(f):
536 536 msg = _(b'directory %r already in dirstate')
537 537 msg %= pycompat.bytestr(f)
538 538 raise error.Abort(msg)
539 539 # shadows
540 540 for d in pathutil.finddirs(f):
541 541 if self._map.hastrackeddir(d):
542 542 break
543 543 entry = self._map.get(d)
544 544 if entry is not None and not entry.removed:
545 545 msg = _(b'file %r in dirstate clashes with %r')
546 546 msg %= (pycompat.bytestr(d), pycompat.bytestr(f))
547 547 raise error.Abort(msg)
548 548 self._dirty = True
549 549 self._updatedfiles.add(f)
550 550 self._map.addfile(
551 551 f,
552 552 mode=mode,
553 553 size=size,
554 554 mtime=mtime,
555 555 added=added,
556 556 merged=merged,
557 557 from_p2=from_p2,
558 558 possibly_dirty=possibly_dirty,
559 559 )
560 560
561 561 def normal(self, f, parentfiledata=None):
562 562 """Mark a file normal and clean.
563 563
564 564 parentfiledata: (mode, size, mtime) of the clean file
565 565
566 566 parentfiledata should be computed from memory (for mode,
567 567 size), as or close as possible from the point where we
568 568 determined the file was clean, to limit the risk of the
569 569 file having been changed by an external process between the
570 570 moment where the file was determined to be clean and now."""
571 571 if parentfiledata:
572 572 (mode, size, mtime) = parentfiledata
573 573 else:
574 574 s = os.lstat(self._join(f))
575 575 mode = s.st_mode
576 576 size = s.st_size
577 577 mtime = s[stat.ST_MTIME]
578 578 self._addpath(f, mode=mode, size=size, mtime=mtime)
579 579 self._map.copymap.pop(f, None)
580 580 if f in self._map.nonnormalset:
581 581 self._map.nonnormalset.remove(f)
582 582 if mtime > self._lastnormaltime:
583 583 # Remember the most recent modification timeslot for status(),
584 584 # to make sure we won't miss future size-preserving file content
585 585 # modifications that happen within the same timeslot.
586 586 self._lastnormaltime = mtime
587 587
588 588 def normallookup(self, f):
589 589 '''Mark a file normal, but possibly dirty.'''
590 590 if self.in_merge:
591 591 # if there is a merge going on and the file was either
592 592 # "merged" or coming from other parent (-2) before
593 593 # being removed, restore that state.
594 594 entry = self._map.get(f)
595 595 if entry is not None:
596 596 # XXX this should probably be dealt with a a lower level
597 597 # (see `merged_removed` and `from_p2_removed`)
598 598 if entry.merged_removed or entry.from_p2_removed:
599 599 source = self._map.copymap.get(f)
600 600 if entry.merged_removed:
601 601 self.merge(f)
602 602 elif entry.from_p2_removed:
603 603 self.otherparent(f)
604 604 if source is not None:
605 605 self.copy(source, f)
606 606 return
607 607 elif entry.merged or entry.from_p2:
608 608 return
609 609 self._addpath(f, possibly_dirty=True)
610 610 self._map.copymap.pop(f, None)
611 611
612 612 def otherparent(self, f):
613 613 '''Mark as coming from the other parent, always dirty.'''
614 614 if not self.in_merge:
615 615 msg = _(b"setting %r to other parent only allowed in merges") % f
616 616 raise error.Abort(msg)
617 617 entry = self._map.get(f)
618 618 if entry is not None and entry.tracked:
619 619 # merge-like
620 620 self._addpath(f, merged=True)
621 621 else:
622 622 # add-like
623 623 self._addpath(f, from_p2=True)
624 624 self._map.copymap.pop(f, None)
625 625
626 626 def add(self, f):
627 627 '''Mark a file added.'''
628 628 self._add(f)
629 629
630 630 def _add(self, filename):
631 631 """internal function to mark a file as added"""
632 632 self._addpath(filename, added=True)
633 633 self._map.copymap.pop(filename, None)
634 634
635 635 def remove(self, f):
636 636 '''Mark a file removed'''
637 637 self._remove(f)
638 638
639 639 def _remove(self, filename):
640 640 """internal function to mark a file removed"""
641 641 self._dirty = True
642 642 self._updatedfiles.add(filename)
643 643 self._map.removefile(filename, in_merge=self.in_merge)
644 644
645 645 def merge(self, f):
646 646 '''Mark a file merged.'''
647 647 if not self.in_merge:
648 648 return self.normallookup(f)
649 649 return self.otherparent(f)
650 650
651 651 def drop(self, f):
652 652 '''Drop a file from the dirstate'''
653 653 self._drop(f)
654 654
655 655 def _drop(self, filename):
656 656 """internal function to drop a file from the dirstate"""
657 657 if self._map.dropfile(filename):
658 658 self._dirty = True
659 659 self._updatedfiles.add(filename)
660 660 self._map.copymap.pop(filename, None)
661 661
662 662 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
663 663 if exists is None:
664 664 exists = os.path.lexists(os.path.join(self._root, path))
665 665 if not exists:
666 666 # Maybe a path component exists
667 667 if not ignoremissing and b'/' in path:
668 668 d, f = path.rsplit(b'/', 1)
669 669 d = self._normalize(d, False, ignoremissing, None)
670 670 folded = d + b"/" + f
671 671 else:
672 672 # No path components, preserve original case
673 673 folded = path
674 674 else:
675 675 # recursively normalize leading directory components
676 676 # against dirstate
677 677 if b'/' in normed:
678 678 d, f = normed.rsplit(b'/', 1)
679 679 d = self._normalize(d, False, ignoremissing, True)
680 680 r = self._root + b"/" + d
681 681 folded = d + b"/" + util.fspath(f, r)
682 682 else:
683 683 folded = util.fspath(normed, self._root)
684 684 storemap[normed] = folded
685 685
686 686 return folded
687 687
688 688 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
689 689 normed = util.normcase(path)
690 690 folded = self._map.filefoldmap.get(normed, None)
691 691 if folded is None:
692 692 if isknown:
693 693 folded = path
694 694 else:
695 695 folded = self._discoverpath(
696 696 path, normed, ignoremissing, exists, self._map.filefoldmap
697 697 )
698 698 return folded
699 699
700 700 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
701 701 normed = util.normcase(path)
702 702 folded = self._map.filefoldmap.get(normed, None)
703 703 if folded is None:
704 704 folded = self._map.dirfoldmap.get(normed, None)
705 705 if folded is None:
706 706 if isknown:
707 707 folded = path
708 708 else:
709 709 # store discovered result in dirfoldmap so that future
710 710 # normalizefile calls don't start matching directories
711 711 folded = self._discoverpath(
712 712 path, normed, ignoremissing, exists, self._map.dirfoldmap
713 713 )
714 714 return folded
715 715
716 716 def normalize(self, path, isknown=False, ignoremissing=False):
717 717 """
718 718 normalize the case of a pathname when on a casefolding filesystem
719 719
720 720 isknown specifies whether the filename came from walking the
721 721 disk, to avoid extra filesystem access.
722 722
723 723 If ignoremissing is True, missing path are returned
724 724 unchanged. Otherwise, we try harder to normalize possibly
725 725 existing path components.
726 726
727 727 The normalized case is determined based on the following precedence:
728 728
729 729 - version of name already stored in the dirstate
730 730 - version of name stored on disk
731 731 - version provided via command arguments
732 732 """
733 733
734 734 if self._checkcase:
735 735 return self._normalize(path, isknown, ignoremissing)
736 736 return path
737 737
738 738 def clear(self):
739 739 self._map.clear()
740 740 self._lastnormaltime = 0
741 741 self._updatedfiles.clear()
742 742 self._dirty = True
743 743
744 744 def rebuild(self, parent, allfiles, changedfiles=None):
745 745 if changedfiles is None:
746 746 # Rebuild entire dirstate
747 747 to_lookup = allfiles
748 748 to_drop = []
749 749 lastnormaltime = self._lastnormaltime
750 750 self.clear()
751 751 self._lastnormaltime = lastnormaltime
752 752 elif len(changedfiles) < 10:
753 753 # Avoid turning allfiles into a set, which can be expensive if it's
754 754 # large.
755 755 to_lookup = []
756 756 to_drop = []
757 757 for f in changedfiles:
758 758 if f in allfiles:
759 759 to_lookup.append(f)
760 760 else:
761 761 to_drop.append(f)
762 762 else:
763 763 changedfilesset = set(changedfiles)
764 764 to_lookup = changedfilesset & set(allfiles)
765 765 to_drop = changedfilesset - to_lookup
766 766
767 767 if self._origpl is None:
768 768 self._origpl = self._pl
769 769 self._map.setparents(parent, self._nodeconstants.nullid)
770 770
771 771 for f in to_lookup:
772 772 self.normallookup(f)
773 773 for f in to_drop:
774 774 self._drop(f)
775 775
776 776 self._dirty = True
777 777
778 778 def identity(self):
779 779 """Return identity of dirstate itself to detect changing in storage
780 780
781 781 If identity of previous dirstate is equal to this, writing
782 782 changes based on the former dirstate out can keep consistency.
783 783 """
784 784 return self._map.identity
785 785
786 786 def write(self, tr):
787 787 if not self._dirty:
788 788 return
789 789
790 790 filename = self._filename
791 791 if tr:
792 792 # 'dirstate.write()' is not only for writing in-memory
793 793 # changes out, but also for dropping ambiguous timestamp.
794 794 # delayed writing re-raise "ambiguous timestamp issue".
795 795 # See also the wiki page below for detail:
796 796 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
797 797
798 798 # emulate dropping timestamp in 'parsers.pack_dirstate'
799 799 now = _getfsnow(self._opener)
800 800 self._map.clearambiguoustimes(self._updatedfiles, now)
801 801
802 802 # emulate that all 'dirstate.normal' results are written out
803 803 self._lastnormaltime = 0
804 804 self._updatedfiles.clear()
805 805
806 806 # delay writing in-memory changes out
807 807 tr.addfilegenerator(
808 808 b'dirstate',
809 809 (self._filename,),
810 810 self._writedirstate,
811 811 location=b'plain',
812 812 )
813 813 return
814 814
815 815 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
816 816 self._writedirstate(st)
817 817
818 818 def addparentchangecallback(self, category, callback):
819 819 """add a callback to be called when the wd parents are changed
820 820
821 821 Callback will be called with the following arguments:
822 822 dirstate, (oldp1, oldp2), (newp1, newp2)
823 823
824 824 Category is a unique identifier to allow overwriting an old callback
825 825 with a newer callback.
826 826 """
827 827 self._plchangecallbacks[category] = callback
828 828
829 829 def _writedirstate(self, st):
830 830 # notify callbacks about parents change
831 831 if self._origpl is not None and self._origpl != self._pl:
832 832 for c, callback in sorted(
833 833 pycompat.iteritems(self._plchangecallbacks)
834 834 ):
835 835 callback(self, self._origpl, self._pl)
836 836 self._origpl = None
837 837 # use the modification time of the newly created temporary file as the
838 838 # filesystem's notion of 'now'
839 839 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
840 840
841 841 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
842 842 # timestamp of each entries in dirstate, because of 'now > mtime'
843 843 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
844 844 if delaywrite > 0:
845 845 # do we have any files to delay for?
846 846 for f, e in pycompat.iteritems(self._map):
847 847 if e.need_delay(now):
848 848 import time # to avoid useless import
849 849
850 850 # rather than sleep n seconds, sleep until the next
851 851 # multiple of n seconds
852 852 clock = time.time()
853 853 start = int(clock) - (int(clock) % delaywrite)
854 854 end = start + delaywrite
855 855 time.sleep(end - clock)
856 856 now = end # trust our estimate that the end is near now
857 857 break
858 858
859 859 self._map.write(st, now)
860 860 self._lastnormaltime = 0
861 861 self._dirty = False
862 862
863 863 def _dirignore(self, f):
864 864 if self._ignore(f):
865 865 return True
866 866 for p in pathutil.finddirs(f):
867 867 if self._ignore(p):
868 868 return True
869 869 return False
870 870
871 871 def _ignorefiles(self):
872 872 files = []
873 873 if os.path.exists(self._join(b'.hgignore')):
874 874 files.append(self._join(b'.hgignore'))
875 875 for name, path in self._ui.configitems(b"ui"):
876 876 if name == b'ignore' or name.startswith(b'ignore.'):
877 877 # we need to use os.path.join here rather than self._join
878 878 # because path is arbitrary and user-specified
879 879 files.append(os.path.join(self._rootdir, util.expandpath(path)))
880 880 return files
881 881
882 882 def _ignorefileandline(self, f):
883 883 files = collections.deque(self._ignorefiles())
884 884 visited = set()
885 885 while files:
886 886 i = files.popleft()
887 887 patterns = matchmod.readpatternfile(
888 888 i, self._ui.warn, sourceinfo=True
889 889 )
890 890 for pattern, lineno, line in patterns:
891 891 kind, p = matchmod._patsplit(pattern, b'glob')
892 892 if kind == b"subinclude":
893 893 if p not in visited:
894 894 files.append(p)
895 895 continue
896 896 m = matchmod.match(
897 897 self._root, b'', [], [pattern], warn=self._ui.warn
898 898 )
899 899 if m(f):
900 900 return (i, lineno, line)
901 901 visited.add(i)
902 902 return (None, -1, b"")
903 903
904 904 def _walkexplicit(self, match, subrepos):
905 905 """Get stat data about the files explicitly specified by match.
906 906
907 907 Return a triple (results, dirsfound, dirsnotfound).
908 908 - results is a mapping from filename to stat result. It also contains
909 909 listings mapping subrepos and .hg to None.
910 910 - dirsfound is a list of files found to be directories.
911 911 - dirsnotfound is a list of files that the dirstate thinks are
912 912 directories and that were not found."""
913 913
914 914 def badtype(mode):
915 915 kind = _(b'unknown')
916 916 if stat.S_ISCHR(mode):
917 917 kind = _(b'character device')
918 918 elif stat.S_ISBLK(mode):
919 919 kind = _(b'block device')
920 920 elif stat.S_ISFIFO(mode):
921 921 kind = _(b'fifo')
922 922 elif stat.S_ISSOCK(mode):
923 923 kind = _(b'socket')
924 924 elif stat.S_ISDIR(mode):
925 925 kind = _(b'directory')
926 926 return _(b'unsupported file type (type is %s)') % kind
927 927
928 928 badfn = match.bad
929 929 dmap = self._map
930 930 lstat = os.lstat
931 931 getkind = stat.S_IFMT
932 932 dirkind = stat.S_IFDIR
933 933 regkind = stat.S_IFREG
934 934 lnkkind = stat.S_IFLNK
935 935 join = self._join
936 936 dirsfound = []
937 937 foundadd = dirsfound.append
938 938 dirsnotfound = []
939 939 notfoundadd = dirsnotfound.append
940 940
941 941 if not match.isexact() and self._checkcase:
942 942 normalize = self._normalize
943 943 else:
944 944 normalize = None
945 945
946 946 files = sorted(match.files())
947 947 subrepos.sort()
948 948 i, j = 0, 0
949 949 while i < len(files) and j < len(subrepos):
950 950 subpath = subrepos[j] + b"/"
951 951 if files[i] < subpath:
952 952 i += 1
953 953 continue
954 954 while i < len(files) and files[i].startswith(subpath):
955 955 del files[i]
956 956 j += 1
957 957
958 958 if not files or b'' in files:
959 959 files = [b'']
960 960 # constructing the foldmap is expensive, so don't do it for the
961 961 # common case where files is ['']
962 962 normalize = None
963 963 results = dict.fromkeys(subrepos)
964 964 results[b'.hg'] = None
965 965
966 966 for ff in files:
967 967 if normalize:
968 968 nf = normalize(ff, False, True)
969 969 else:
970 970 nf = ff
971 971 if nf in results:
972 972 continue
973 973
974 974 try:
975 975 st = lstat(join(nf))
976 976 kind = getkind(st.st_mode)
977 977 if kind == dirkind:
978 978 if nf in dmap:
979 979 # file replaced by dir on disk but still in dirstate
980 980 results[nf] = None
981 981 foundadd((nf, ff))
982 982 elif kind == regkind or kind == lnkkind:
983 983 results[nf] = st
984 984 else:
985 985 badfn(ff, badtype(kind))
986 986 if nf in dmap:
987 987 results[nf] = None
988 988 except OSError as inst: # nf not found on disk - it is dirstate only
989 989 if nf in dmap: # does it exactly match a missing file?
990 990 results[nf] = None
991 991 else: # does it match a missing directory?
992 992 if self._map.hasdir(nf):
993 993 notfoundadd(nf)
994 994 else:
995 995 badfn(ff, encoding.strtolocal(inst.strerror))
996 996
997 997 # match.files() may contain explicitly-specified paths that shouldn't
998 998 # be taken; drop them from the list of files found. dirsfound/notfound
999 999 # aren't filtered here because they will be tested later.
1000 1000 if match.anypats():
1001 1001 for f in list(results):
1002 1002 if f == b'.hg' or f in subrepos:
1003 1003 # keep sentinel to disable further out-of-repo walks
1004 1004 continue
1005 1005 if not match(f):
1006 1006 del results[f]
1007 1007
1008 1008 # Case insensitive filesystems cannot rely on lstat() failing to detect
1009 1009 # a case-only rename. Prune the stat object for any file that does not
1010 1010 # match the case in the filesystem, if there are multiple files that
1011 1011 # normalize to the same path.
1012 1012 if match.isexact() and self._checkcase:
1013 1013 normed = {}
1014 1014
1015 1015 for f, st in pycompat.iteritems(results):
1016 1016 if st is None:
1017 1017 continue
1018 1018
1019 1019 nc = util.normcase(f)
1020 1020 paths = normed.get(nc)
1021 1021
1022 1022 if paths is None:
1023 1023 paths = set()
1024 1024 normed[nc] = paths
1025 1025
1026 1026 paths.add(f)
1027 1027
1028 1028 for norm, paths in pycompat.iteritems(normed):
1029 1029 if len(paths) > 1:
1030 1030 for path in paths:
1031 1031 folded = self._discoverpath(
1032 1032 path, norm, True, None, self._map.dirfoldmap
1033 1033 )
1034 1034 if path != folded:
1035 1035 results[path] = None
1036 1036
1037 1037 return results, dirsfound, dirsnotfound
1038 1038
1039 1039 def walk(self, match, subrepos, unknown, ignored, full=True):
1040 1040 """
1041 1041 Walk recursively through the directory tree, finding all files
1042 1042 matched by match.
1043 1043
1044 1044 If full is False, maybe skip some known-clean files.
1045 1045
1046 1046 Return a dict mapping filename to stat-like object (either
1047 1047 mercurial.osutil.stat instance or return value of os.stat()).
1048 1048
1049 1049 """
1050 1050 # full is a flag that extensions that hook into walk can use -- this
1051 1051 # implementation doesn't use it at all. This satisfies the contract
1052 1052 # because we only guarantee a "maybe".
1053 1053
1054 1054 if ignored:
1055 1055 ignore = util.never
1056 1056 dirignore = util.never
1057 1057 elif unknown:
1058 1058 ignore = self._ignore
1059 1059 dirignore = self._dirignore
1060 1060 else:
1061 1061 # if not unknown and not ignored, drop dir recursion and step 2
1062 1062 ignore = util.always
1063 1063 dirignore = util.always
1064 1064
1065 1065 matchfn = match.matchfn
1066 1066 matchalways = match.always()
1067 1067 matchtdir = match.traversedir
1068 1068 dmap = self._map
1069 1069 listdir = util.listdir
1070 1070 lstat = os.lstat
1071 1071 dirkind = stat.S_IFDIR
1072 1072 regkind = stat.S_IFREG
1073 1073 lnkkind = stat.S_IFLNK
1074 1074 join = self._join
1075 1075
1076 1076 exact = skipstep3 = False
1077 1077 if match.isexact(): # match.exact
1078 1078 exact = True
1079 1079 dirignore = util.always # skip step 2
1080 1080 elif match.prefix(): # match.match, no patterns
1081 1081 skipstep3 = True
1082 1082
1083 1083 if not exact and self._checkcase:
1084 1084 normalize = self._normalize
1085 1085 normalizefile = self._normalizefile
1086 1086 skipstep3 = False
1087 1087 else:
1088 1088 normalize = self._normalize
1089 1089 normalizefile = None
1090 1090
1091 1091 # step 1: find all explicit files
1092 1092 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
1093 1093 if matchtdir:
1094 1094 for d in work:
1095 1095 matchtdir(d[0])
1096 1096 for d in dirsnotfound:
1097 1097 matchtdir(d)
1098 1098
1099 1099 skipstep3 = skipstep3 and not (work or dirsnotfound)
1100 1100 work = [d for d in work if not dirignore(d[0])]
1101 1101
1102 1102 # step 2: visit subdirectories
1103 1103 def traverse(work, alreadynormed):
1104 1104 wadd = work.append
1105 1105 while work:
1106 1106 tracing.counter('dirstate.walk work', len(work))
1107 1107 nd = work.pop()
1108 1108 visitentries = match.visitchildrenset(nd)
1109 1109 if not visitentries:
1110 1110 continue
1111 1111 if visitentries == b'this' or visitentries == b'all':
1112 1112 visitentries = None
1113 1113 skip = None
1114 1114 if nd != b'':
1115 1115 skip = b'.hg'
1116 1116 try:
1117 1117 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1118 1118 entries = listdir(join(nd), stat=True, skip=skip)
1119 1119 except OSError as inst:
1120 1120 if inst.errno in (errno.EACCES, errno.ENOENT):
1121 1121 match.bad(
1122 1122 self.pathto(nd), encoding.strtolocal(inst.strerror)
1123 1123 )
1124 1124 continue
1125 1125 raise
1126 1126 for f, kind, st in entries:
1127 1127 # Some matchers may return files in the visitentries set,
1128 1128 # instead of 'this', if the matcher explicitly mentions them
1129 1129 # and is not an exactmatcher. This is acceptable; we do not
1130 1130 # make any hard assumptions about file-or-directory below
1131 1131 # based on the presence of `f` in visitentries. If
1132 1132 # visitchildrenset returned a set, we can always skip the
1133 1133 # entries *not* in the set it provided regardless of whether
1134 1134 # they're actually a file or a directory.
1135 1135 if visitentries and f not in visitentries:
1136 1136 continue
1137 1137 if normalizefile:
1138 1138 # even though f might be a directory, we're only
1139 1139 # interested in comparing it to files currently in the
1140 1140 # dmap -- therefore normalizefile is enough
1141 1141 nf = normalizefile(
1142 1142 nd and (nd + b"/" + f) or f, True, True
1143 1143 )
1144 1144 else:
1145 1145 nf = nd and (nd + b"/" + f) or f
1146 1146 if nf not in results:
1147 1147 if kind == dirkind:
1148 1148 if not ignore(nf):
1149 1149 if matchtdir:
1150 1150 matchtdir(nf)
1151 1151 wadd(nf)
1152 1152 if nf in dmap and (matchalways or matchfn(nf)):
1153 1153 results[nf] = None
1154 1154 elif kind == regkind or kind == lnkkind:
1155 1155 if nf in dmap:
1156 1156 if matchalways or matchfn(nf):
1157 1157 results[nf] = st
1158 1158 elif (matchalways or matchfn(nf)) and not ignore(
1159 1159 nf
1160 1160 ):
1161 1161 # unknown file -- normalize if necessary
1162 1162 if not alreadynormed:
1163 1163 nf = normalize(nf, False, True)
1164 1164 results[nf] = st
1165 1165 elif nf in dmap and (matchalways or matchfn(nf)):
1166 1166 results[nf] = None
1167 1167
1168 1168 for nd, d in work:
1169 1169 # alreadynormed means that processwork doesn't have to do any
1170 1170 # expensive directory normalization
1171 1171 alreadynormed = not normalize or nd == d
1172 1172 traverse([d], alreadynormed)
1173 1173
1174 1174 for s in subrepos:
1175 1175 del results[s]
1176 1176 del results[b'.hg']
1177 1177
1178 1178 # step 3: visit remaining files from dmap
1179 1179 if not skipstep3 and not exact:
1180 1180 # If a dmap file is not in results yet, it was either
1181 1181 # a) not matching matchfn b) ignored, c) missing, or d) under a
1182 1182 # symlink directory.
1183 1183 if not results and matchalways:
1184 1184 visit = [f for f in dmap]
1185 1185 else:
1186 1186 visit = [f for f in dmap if f not in results and matchfn(f)]
1187 1187 visit.sort()
1188 1188
1189 1189 if unknown:
1190 1190 # unknown == True means we walked all dirs under the roots
1191 1191 # that wasn't ignored, and everything that matched was stat'ed
1192 1192 # and is already in results.
1193 1193 # The rest must thus be ignored or under a symlink.
1194 1194 audit_path = pathutil.pathauditor(self._root, cached=True)
1195 1195
1196 1196 for nf in iter(visit):
1197 1197 # If a stat for the same file was already added with a
1198 1198 # different case, don't add one for this, since that would
1199 1199 # make it appear as if the file exists under both names
1200 1200 # on disk.
1201 1201 if (
1202 1202 normalizefile
1203 1203 and normalizefile(nf, True, True) in results
1204 1204 ):
1205 1205 results[nf] = None
1206 1206 # Report ignored items in the dmap as long as they are not
1207 1207 # under a symlink directory.
1208 1208 elif audit_path.check(nf):
1209 1209 try:
1210 1210 results[nf] = lstat(join(nf))
1211 1211 # file was just ignored, no links, and exists
1212 1212 except OSError:
1213 1213 # file doesn't exist
1214 1214 results[nf] = None
1215 1215 else:
1216 1216 # It's either missing or under a symlink directory
1217 1217 # which we in this case report as missing
1218 1218 results[nf] = None
1219 1219 else:
1220 1220 # We may not have walked the full directory tree above,
1221 1221 # so stat and check everything we missed.
1222 1222 iv = iter(visit)
1223 1223 for st in util.statfiles([join(i) for i in visit]):
1224 1224 results[next(iv)] = st
1225 1225 return results
1226 1226
1227 1227 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1228 1228 # Force Rayon (Rust parallelism library) to respect the number of
1229 1229 # workers. This is a temporary workaround until Rust code knows
1230 1230 # how to read the config file.
1231 1231 numcpus = self._ui.configint(b"worker", b"numcpus")
1232 1232 if numcpus is not None:
1233 1233 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1234 1234
1235 1235 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1236 1236 if not workers_enabled:
1237 1237 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1238 1238
1239 1239 (
1240 1240 lookup,
1241 1241 modified,
1242 1242 added,
1243 1243 removed,
1244 1244 deleted,
1245 1245 clean,
1246 1246 ignored,
1247 1247 unknown,
1248 1248 warnings,
1249 1249 bad,
1250 1250 traversed,
1251 1251 dirty,
1252 1252 ) = rustmod.status(
1253 1253 self._map._rustmap,
1254 1254 matcher,
1255 1255 self._rootdir,
1256 1256 self._ignorefiles(),
1257 1257 self._checkexec,
1258 1258 self._lastnormaltime,
1259 1259 bool(list_clean),
1260 1260 bool(list_ignored),
1261 1261 bool(list_unknown),
1262 1262 bool(matcher.traversedir),
1263 1263 )
1264 1264
1265 1265 self._dirty |= dirty
1266 1266
1267 1267 if matcher.traversedir:
1268 1268 for dir in traversed:
1269 1269 matcher.traversedir(dir)
1270 1270
1271 1271 if self._ui.warn:
1272 1272 for item in warnings:
1273 1273 if isinstance(item, tuple):
1274 1274 file_path, syntax = item
1275 1275 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1276 1276 file_path,
1277 1277 syntax,
1278 1278 )
1279 1279 self._ui.warn(msg)
1280 1280 else:
1281 1281 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1282 1282 self._ui.warn(
1283 1283 msg
1284 1284 % (
1285 1285 pathutil.canonpath(
1286 1286 self._rootdir, self._rootdir, item
1287 1287 ),
1288 1288 b"No such file or directory",
1289 1289 )
1290 1290 )
1291 1291
1292 1292 for (fn, message) in bad:
1293 1293 matcher.bad(fn, encoding.strtolocal(message))
1294 1294
1295 1295 status = scmutil.status(
1296 1296 modified=modified,
1297 1297 added=added,
1298 1298 removed=removed,
1299 1299 deleted=deleted,
1300 1300 unknown=unknown,
1301 1301 ignored=ignored,
1302 1302 clean=clean,
1303 1303 )
1304 1304 return (lookup, status)
1305 1305
1306 1306 def status(self, match, subrepos, ignored, clean, unknown):
1307 1307 """Determine the status of the working copy relative to the
1308 1308 dirstate and return a pair of (unsure, status), where status is of type
1309 1309 scmutil.status and:
1310 1310
1311 1311 unsure:
1312 1312 files that might have been modified since the dirstate was
1313 1313 written, but need to be read to be sure (size is the same
1314 1314 but mtime differs)
1315 1315 status.modified:
1316 1316 files that have definitely been modified since the dirstate
1317 1317 was written (different size or mode)
1318 1318 status.clean:
1319 1319 files that have definitely not been modified since the
1320 1320 dirstate was written
1321 1321 """
1322 1322 listignored, listclean, listunknown = ignored, clean, unknown
1323 1323 lookup, modified, added, unknown, ignored = [], [], [], [], []
1324 1324 removed, deleted, clean = [], [], []
1325 1325
1326 1326 dmap = self._map
1327 1327 dmap.preload()
1328 1328
1329 1329 use_rust = True
1330 1330
1331 1331 allowed_matchers = (
1332 1332 matchmod.alwaysmatcher,
1333 1333 matchmod.exactmatcher,
1334 1334 matchmod.includematcher,
1335 1335 )
1336 1336
1337 1337 if rustmod is None:
1338 1338 use_rust = False
1339 1339 elif self._checkcase:
1340 1340 # Case-insensitive filesystems are not handled yet
1341 1341 use_rust = False
1342 1342 elif subrepos:
1343 1343 use_rust = False
1344 1344 elif sparse.enabled:
1345 1345 use_rust = False
1346 1346 elif not isinstance(match, allowed_matchers):
1347 1347 # Some matchers have yet to be implemented
1348 1348 use_rust = False
1349 1349
1350 1350 if use_rust:
1351 1351 try:
1352 1352 return self._rust_status(
1353 1353 match, listclean, listignored, listunknown
1354 1354 )
1355 1355 except rustmod.FallbackError:
1356 1356 pass
1357 1357
1358 1358 def noop(f):
1359 1359 pass
1360 1360
1361 1361 dcontains = dmap.__contains__
1362 1362 dget = dmap.__getitem__
1363 1363 ladd = lookup.append # aka "unsure"
1364 1364 madd = modified.append
1365 1365 aadd = added.append
1366 1366 uadd = unknown.append if listunknown else noop
1367 1367 iadd = ignored.append if listignored else noop
1368 1368 radd = removed.append
1369 1369 dadd = deleted.append
1370 1370 cadd = clean.append if listclean else noop
1371 1371 mexact = match.exact
1372 1372 dirignore = self._dirignore
1373 1373 checkexec = self._checkexec
1374 1374 copymap = self._map.copymap
1375 1375 lastnormaltime = self._lastnormaltime
1376 1376
1377 1377 # We need to do full walks when either
1378 1378 # - we're listing all clean files, or
1379 1379 # - match.traversedir does something, because match.traversedir should
1380 1380 # be called for every dir in the working dir
1381 1381 full = listclean or match.traversedir is not None
1382 1382 for fn, st in pycompat.iteritems(
1383 1383 self.walk(match, subrepos, listunknown, listignored, full=full)
1384 1384 ):
1385 1385 if not dcontains(fn):
1386 1386 if (listignored or mexact(fn)) and dirignore(fn):
1387 1387 if listignored:
1388 1388 iadd(fn)
1389 1389 else:
1390 1390 uadd(fn)
1391 1391 continue
1392 1392
1393 1393 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1394 1394 # written like that for performance reasons. dmap[fn] is not a
1395 1395 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1396 1396 # opcode has fast paths when the value to be unpacked is a tuple or
1397 1397 # a list, but falls back to creating a full-fledged iterator in
1398 1398 # general. That is much slower than simply accessing and storing the
1399 1399 # tuple members one by one.
1400 1400 t = dget(fn)
1401 1401 mode = t.mode
1402 1402 size = t.size
1403 1403 time = t.mtime
1404 1404
1405 1405 if not st and t.tracked:
1406 1406 dadd(fn)
1407 1407 elif t.merged:
1408 1408 madd(fn)
1409 1409 elif t.added:
1410 1410 aadd(fn)
1411 1411 elif t.removed:
1412 1412 radd(fn)
1413 1413 elif t.tracked:
1414 1414 if (
1415 1415 size >= 0
1416 1416 and (
1417 1417 (size != st.st_size and size != st.st_size & _rangemask)
1418 1418 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1419 1419 )
1420 1420 or t.from_p2
1421 1421 or fn in copymap
1422 1422 ):
1423 1423 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1424 1424 # issue6456: Size returned may be longer due to
1425 1425 # encryption on EXT-4 fscrypt, undecided.
1426 1426 ladd(fn)
1427 1427 else:
1428 1428 madd(fn)
1429 1429 elif (
1430 1430 time != st[stat.ST_MTIME]
1431 1431 and time != st[stat.ST_MTIME] & _rangemask
1432 1432 ):
1433 1433 ladd(fn)
1434 1434 elif st[stat.ST_MTIME] == lastnormaltime:
1435 1435 # fn may have just been marked as normal and it may have
1436 1436 # changed in the same second without changing its size.
1437 1437 # This can happen if we quickly do multiple commits.
1438 1438 # Force lookup, so we don't miss such a racy file change.
1439 1439 ladd(fn)
1440 1440 elif listclean:
1441 1441 cadd(fn)
1442 1442 status = scmutil.status(
1443 1443 modified, added, removed, deleted, unknown, ignored, clean
1444 1444 )
1445 1445 return (lookup, status)
1446 1446
1447 1447 def matches(self, match):
1448 1448 """
1449 1449 return files in the dirstate (in whatever state) filtered by match
1450 1450 """
1451 1451 dmap = self._map
1452 1452 if rustmod is not None:
1453 1453 dmap = self._map._rustmap
1454 1454
1455 1455 if match.always():
1456 1456 return dmap.keys()
1457 1457 files = match.files()
1458 1458 if match.isexact():
1459 1459 # fast path -- filter the other way around, since typically files is
1460 1460 # much smaller than dmap
1461 1461 return [f for f in files if f in dmap]
1462 1462 if match.prefix() and all(fn in dmap for fn in files):
1463 1463 # fast path -- all the values are known to be files, so just return
1464 1464 # that
1465 1465 return list(files)
1466 1466 return [f for f in dmap if match(f)]
1467 1467
1468 1468 def _actualfilename(self, tr):
1469 1469 if tr:
1470 1470 return self._pendingfilename
1471 1471 else:
1472 1472 return self._filename
1473 1473
1474 1474 def savebackup(self, tr, backupname):
1475 1475 '''Save current dirstate into backup file'''
1476 1476 filename = self._actualfilename(tr)
1477 1477 assert backupname != filename
1478 1478
1479 1479 # use '_writedirstate' instead of 'write' to write changes certainly,
1480 1480 # because the latter omits writing out if transaction is running.
1481 1481 # output file will be used to create backup of dirstate at this point.
1482 1482 if self._dirty or not self._opener.exists(filename):
1483 1483 self._writedirstate(
1484 1484 self._opener(filename, b"w", atomictemp=True, checkambig=True)
1485 1485 )
1486 1486
1487 1487 if tr:
1488 1488 # ensure that subsequent tr.writepending returns True for
1489 1489 # changes written out above, even if dirstate is never
1490 1490 # changed after this
1491 1491 tr.addfilegenerator(
1492 1492 b'dirstate',
1493 1493 (self._filename,),
1494 1494 self._writedirstate,
1495 1495 location=b'plain',
1496 1496 )
1497 1497
1498 1498 # ensure that pending file written above is unlinked at
1499 1499 # failure, even if tr.writepending isn't invoked until the
1500 1500 # end of this transaction
1501 1501 tr.registertmp(filename, location=b'plain')
1502 1502
1503 1503 self._opener.tryunlink(backupname)
1504 1504 # hardlink backup is okay because _writedirstate is always called
1505 1505 # with an "atomictemp=True" file.
1506 1506 util.copyfile(
1507 1507 self._opener.join(filename),
1508 1508 self._opener.join(backupname),
1509 1509 hardlink=True,
1510 1510 )
1511 1511
1512 1512 def restorebackup(self, tr, backupname):
1513 1513 '''Restore dirstate by backup file'''
1514 1514 # this "invalidate()" prevents "wlock.release()" from writing
1515 1515 # changes of dirstate out after restoring from backup file
1516 1516 self.invalidate()
1517 1517 filename = self._actualfilename(tr)
1518 1518 o = self._opener
1519 1519 if util.samefile(o.join(backupname), o.join(filename)):
1520 1520 o.unlink(backupname)
1521 1521 else:
1522 1522 o.rename(backupname, filename, checkambig=True)
1523 1523
1524 1524 def clearbackup(self, tr, backupname):
1525 1525 '''Clear backup file'''
1526 1526 self._opener.unlink(backupname)
General Comments 0
You need to be logged in to leave comments. Login now