##// END OF EJS Templates
revlog: recommit 49fd21f32695 with a fix for issue6528...
Joerg Sonnenberger -
r49876:5b65721a default
parent child Browse files
Show More
@@ -1,3145 +1,3155 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
9 9 import errno
10 10 import filecmp
11 11 import os
12 12 import stat
13 13
14 14 from .i18n import _
15 15 from .node import (
16 16 hex,
17 17 nullrev,
18 18 short,
19 19 )
20 20 from .pycompat import (
21 21 getattr,
22 22 )
23 23 from . import (
24 24 dagop,
25 25 encoding,
26 26 error,
27 27 fileset,
28 28 match as matchmod,
29 29 mergestate as mergestatemod,
30 30 metadata,
31 31 obsolete as obsmod,
32 32 patch,
33 33 pathutil,
34 34 phases,
35 35 pycompat,
36 36 repoview,
37 37 scmutil,
38 38 sparse,
39 39 subrepo,
40 40 subrepoutil,
41 41 util,
42 42 )
43 43 from .utils import (
44 44 dateutil,
45 45 stringutil,
46 46 )
47 47 from .dirstateutils import (
48 48 timestamp,
49 49 )
50 50
51 51 propertycache = util.propertycache
52 52
53 53
54 54 class basectx:
55 55 """A basectx object represents the common logic for its children:
56 56 changectx: read-only context that is already present in the repo,
57 57 workingctx: a context that represents the working directory and can
58 58 be committed,
59 59 memctx: a context that represents changes in-memory and can also
60 60 be committed."""
61 61
62 62 def __init__(self, repo):
63 63 self._repo = repo
64 64
65 65 def __bytes__(self):
66 66 return short(self.node())
67 67
68 68 __str__ = encoding.strmethod(__bytes__)
69 69
70 70 def __repr__(self):
71 71 return "<%s %s>" % (type(self).__name__, str(self))
72 72
73 73 def __eq__(self, other):
74 74 try:
75 75 return type(self) == type(other) and self._rev == other._rev
76 76 except AttributeError:
77 77 return False
78 78
79 79 def __ne__(self, other):
80 80 return not (self == other)
81 81
82 82 def __contains__(self, key):
83 83 return key in self._manifest
84 84
85 85 def __getitem__(self, key):
86 86 return self.filectx(key)
87 87
88 88 def __iter__(self):
89 89 return iter(self._manifest)
90 90
91 91 def _buildstatusmanifest(self, status):
92 92 """Builds a manifest that includes the given status results, if this is
93 93 a working copy context. For non-working copy contexts, it just returns
94 94 the normal manifest."""
95 95 return self.manifest()
96 96
97 97 def _matchstatus(self, other, match):
98 98 """This internal method provides a way for child objects to override the
99 99 match operator.
100 100 """
101 101 return match
102 102
103 103 def _buildstatus(
104 104 self, other, s, match, listignored, listclean, listunknown
105 105 ):
106 106 """build a status with respect to another context"""
107 107 # Load earliest manifest first for caching reasons. More specifically,
108 108 # if you have revisions 1000 and 1001, 1001 is probably stored as a
109 109 # delta against 1000. Thus, if you read 1000 first, we'll reconstruct
110 110 # 1000 and cache it so that when you read 1001, we just need to apply a
111 111 # delta to what's in the cache. So that's one full reconstruction + one
112 112 # delta application.
113 113 mf2 = None
114 114 if self.rev() is not None and self.rev() < other.rev():
115 115 mf2 = self._buildstatusmanifest(s)
116 116 mf1 = other._buildstatusmanifest(s)
117 117 if mf2 is None:
118 118 mf2 = self._buildstatusmanifest(s)
119 119
120 120 modified, added = [], []
121 121 removed = []
122 122 clean = []
123 123 deleted, unknown, ignored = s.deleted, s.unknown, s.ignored
124 124 deletedset = set(deleted)
125 125 d = mf1.diff(mf2, match=match, clean=listclean)
126 126 for fn, value in d.items():
127 127 if fn in deletedset:
128 128 continue
129 129 if value is None:
130 130 clean.append(fn)
131 131 continue
132 132 (node1, flag1), (node2, flag2) = value
133 133 if node1 is None:
134 134 added.append(fn)
135 135 elif node2 is None:
136 136 removed.append(fn)
137 137 elif flag1 != flag2:
138 138 modified.append(fn)
139 139 elif node2 not in self._repo.nodeconstants.wdirfilenodeids:
140 140 # When comparing files between two commits, we save time by
141 141 # not comparing the file contents when the nodeids differ.
142 142 # Note that this means we incorrectly report a reverted change
143 143 # to a file as a modification.
144 144 modified.append(fn)
145 145 elif self[fn].cmp(other[fn]):
146 146 modified.append(fn)
147 147 else:
148 148 clean.append(fn)
149 149
150 150 if removed:
151 151 # need to filter files if they are already reported as removed
152 152 unknown = [
153 153 fn
154 154 for fn in unknown
155 155 if fn not in mf1 and (not match or match(fn))
156 156 ]
157 157 ignored = [
158 158 fn
159 159 for fn in ignored
160 160 if fn not in mf1 and (not match or match(fn))
161 161 ]
162 162 # if they're deleted, don't report them as removed
163 163 removed = [fn for fn in removed if fn not in deletedset]
164 164
165 165 return scmutil.status(
166 166 modified, added, removed, deleted, unknown, ignored, clean
167 167 )
168 168
169 169 @propertycache
170 170 def substate(self):
171 171 return subrepoutil.state(self, self._repo.ui)
172 172
173 173 def subrev(self, subpath):
174 174 return self.substate[subpath][1]
175 175
176 176 def rev(self):
177 177 return self._rev
178 178
179 179 def node(self):
180 180 return self._node
181 181
182 182 def hex(self):
183 183 return hex(self.node())
184 184
185 185 def manifest(self):
186 186 return self._manifest
187 187
188 188 def manifestctx(self):
189 189 return self._manifestctx
190 190
191 191 def repo(self):
192 192 return self._repo
193 193
194 194 def phasestr(self):
195 195 return phases.phasenames[self.phase()]
196 196
197 197 def mutable(self):
198 198 return self.phase() > phases.public
199 199
200 200 def matchfileset(self, cwd, expr, badfn=None):
201 201 return fileset.match(self, cwd, expr, badfn=badfn)
202 202
203 203 def obsolete(self):
204 204 """True if the changeset is obsolete"""
205 205 return self.rev() in obsmod.getrevs(self._repo, b'obsolete')
206 206
207 207 def extinct(self):
208 208 """True if the changeset is extinct"""
209 209 return self.rev() in obsmod.getrevs(self._repo, b'extinct')
210 210
211 211 def orphan(self):
212 212 """True if the changeset is not obsolete, but its ancestor is"""
213 213 return self.rev() in obsmod.getrevs(self._repo, b'orphan')
214 214
215 215 def phasedivergent(self):
216 216 """True if the changeset tries to be a successor of a public changeset
217 217
218 218 Only non-public and non-obsolete changesets may be phase-divergent.
219 219 """
220 220 return self.rev() in obsmod.getrevs(self._repo, b'phasedivergent')
221 221
222 222 def contentdivergent(self):
223 223 """Is a successor of a changeset with multiple possible successor sets
224 224
225 225 Only non-public and non-obsolete changesets may be content-divergent.
226 226 """
227 227 return self.rev() in obsmod.getrevs(self._repo, b'contentdivergent')
228 228
229 229 def isunstable(self):
230 230 """True if the changeset is either orphan, phase-divergent or
231 231 content-divergent"""
232 232 return self.orphan() or self.phasedivergent() or self.contentdivergent()
233 233
234 234 def instabilities(self):
235 235 """return the list of instabilities affecting this changeset.
236 236
237 237 Instabilities are returned as strings. possible values are:
238 238 - orphan,
239 239 - phase-divergent,
240 240 - content-divergent.
241 241 """
242 242 instabilities = []
243 243 if self.orphan():
244 244 instabilities.append(b'orphan')
245 245 if self.phasedivergent():
246 246 instabilities.append(b'phase-divergent')
247 247 if self.contentdivergent():
248 248 instabilities.append(b'content-divergent')
249 249 return instabilities
250 250
251 251 def parents(self):
252 252 """return contexts for each parent changeset"""
253 253 return self._parents
254 254
255 255 def p1(self):
256 256 return self._parents[0]
257 257
258 258 def p2(self):
259 259 parents = self._parents
260 260 if len(parents) == 2:
261 261 return parents[1]
262 262 return self._repo[nullrev]
263 263
264 264 def _fileinfo(self, path):
265 265 if '_manifest' in self.__dict__:
266 266 try:
267 267 return self._manifest.find(path)
268 268 except KeyError:
269 269 raise error.ManifestLookupError(
270 270 self._node or b'None', path, _(b'not found in manifest')
271 271 )
272 272 if '_manifestdelta' in self.__dict__ or path in self.files():
273 273 if path in self._manifestdelta:
274 274 return (
275 275 self._manifestdelta[path],
276 276 self._manifestdelta.flags(path),
277 277 )
278 278 mfl = self._repo.manifestlog
279 279 try:
280 280 node, flag = mfl[self._changeset.manifest].find(path)
281 281 except KeyError:
282 282 raise error.ManifestLookupError(
283 283 self._node or b'None', path, _(b'not found in manifest')
284 284 )
285 285
286 286 return node, flag
287 287
288 288 def filenode(self, path):
289 289 return self._fileinfo(path)[0]
290 290
291 291 def flags(self, path):
292 292 try:
293 293 return self._fileinfo(path)[1]
294 294 except error.LookupError:
295 295 return b''
296 296
297 297 @propertycache
298 298 def _copies(self):
299 299 return metadata.computechangesetcopies(self)
300 300
301 301 def p1copies(self):
302 302 return self._copies[0]
303 303
304 304 def p2copies(self):
305 305 return self._copies[1]
306 306
307 307 def sub(self, path, allowcreate=True):
308 308 '''return a subrepo for the stored revision of path, never wdir()'''
309 309 return subrepo.subrepo(self, path, allowcreate=allowcreate)
310 310
311 311 def nullsub(self, path, pctx):
312 312 return subrepo.nullsubrepo(self, path, pctx)
313 313
314 314 def workingsub(self, path):
315 315 """return a subrepo for the stored revision, or wdir if this is a wdir
316 316 context.
317 317 """
318 318 return subrepo.subrepo(self, path, allowwdir=True)
319 319
320 320 def match(
321 321 self,
322 322 pats=None,
323 323 include=None,
324 324 exclude=None,
325 325 default=b'glob',
326 326 listsubrepos=False,
327 327 badfn=None,
328 328 cwd=None,
329 329 ):
330 330 r = self._repo
331 331 if not cwd:
332 332 cwd = r.getcwd()
333 333 return matchmod.match(
334 334 r.root,
335 335 cwd,
336 336 pats,
337 337 include,
338 338 exclude,
339 339 default,
340 340 auditor=r.nofsauditor,
341 341 ctx=self,
342 342 listsubrepos=listsubrepos,
343 343 badfn=badfn,
344 344 )
345 345
346 346 def diff(
347 347 self,
348 348 ctx2=None,
349 349 match=None,
350 350 changes=None,
351 351 opts=None,
352 352 losedatafn=None,
353 353 pathfn=None,
354 354 copy=None,
355 355 copysourcematch=None,
356 356 hunksfilterfn=None,
357 357 ):
358 358 """Returns a diff generator for the given contexts and matcher"""
359 359 if ctx2 is None:
360 360 ctx2 = self.p1()
361 361 if ctx2 is not None:
362 362 ctx2 = self._repo[ctx2]
363 363 return patch.diff(
364 364 self._repo,
365 365 ctx2,
366 366 self,
367 367 match=match,
368 368 changes=changes,
369 369 opts=opts,
370 370 losedatafn=losedatafn,
371 371 pathfn=pathfn,
372 372 copy=copy,
373 373 copysourcematch=copysourcematch,
374 374 hunksfilterfn=hunksfilterfn,
375 375 )
376 376
377 377 def dirs(self):
378 378 return self._manifest.dirs()
379 379
380 380 def hasdir(self, dir):
381 381 return self._manifest.hasdir(dir)
382 382
383 383 def status(
384 384 self,
385 385 other=None,
386 386 match=None,
387 387 listignored=False,
388 388 listclean=False,
389 389 listunknown=False,
390 390 listsubrepos=False,
391 391 ):
392 392 """return status of files between two nodes or node and working
393 393 directory.
394 394
395 395 If other is None, compare this node with working directory.
396 396
397 397 ctx1.status(ctx2) returns the status of change from ctx1 to ctx2
398 398
399 399 Returns a mercurial.scmutils.status object.
400 400
401 401 Data can be accessed using either tuple notation:
402 402
403 403 (modified, added, removed, deleted, unknown, ignored, clean)
404 404
405 405 or direct attribute access:
406 406
407 407 s.modified, s.added, ...
408 408 """
409 409
410 410 ctx1 = self
411 411 ctx2 = self._repo[other]
412 412
413 413 # This next code block is, admittedly, fragile logic that tests for
414 414 # reversing the contexts and wouldn't need to exist if it weren't for
415 415 # the fast (and common) code path of comparing the working directory
416 416 # with its first parent.
417 417 #
418 418 # What we're aiming for here is the ability to call:
419 419 #
420 420 # workingctx.status(parentctx)
421 421 #
422 422 # If we always built the manifest for each context and compared those,
423 423 # then we'd be done. But the special case of the above call means we
424 424 # just copy the manifest of the parent.
425 425 reversed = False
426 426 if not isinstance(ctx1, changectx) and isinstance(ctx2, changectx):
427 427 reversed = True
428 428 ctx1, ctx2 = ctx2, ctx1
429 429
430 430 match = self._repo.narrowmatch(match)
431 431 match = ctx2._matchstatus(ctx1, match)
432 432 r = scmutil.status([], [], [], [], [], [], [])
433 433 r = ctx2._buildstatus(
434 434 ctx1, r, match, listignored, listclean, listunknown
435 435 )
436 436
437 437 if reversed:
438 438 # Reverse added and removed. Clear deleted, unknown and ignored as
439 439 # these make no sense to reverse.
440 440 r = scmutil.status(
441 441 r.modified, r.removed, r.added, [], [], [], r.clean
442 442 )
443 443
444 444 if listsubrepos:
445 445 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
446 446 try:
447 447 rev2 = ctx2.subrev(subpath)
448 448 except KeyError:
449 449 # A subrepo that existed in node1 was deleted between
450 450 # node1 and node2 (inclusive). Thus, ctx2's substate
451 451 # won't contain that subpath. The best we can do ignore it.
452 452 rev2 = None
453 453 submatch = matchmod.subdirmatcher(subpath, match)
454 454 s = sub.status(
455 455 rev2,
456 456 match=submatch,
457 457 ignored=listignored,
458 458 clean=listclean,
459 459 unknown=listunknown,
460 460 listsubrepos=True,
461 461 )
462 462 for k in (
463 463 'modified',
464 464 'added',
465 465 'removed',
466 466 'deleted',
467 467 'unknown',
468 468 'ignored',
469 469 'clean',
470 470 ):
471 471 rfiles, sfiles = getattr(r, k), getattr(s, k)
472 472 rfiles.extend(b"%s/%s" % (subpath, f) for f in sfiles)
473 473
474 474 r.modified.sort()
475 475 r.added.sort()
476 476 r.removed.sort()
477 477 r.deleted.sort()
478 478 r.unknown.sort()
479 479 r.ignored.sort()
480 480 r.clean.sort()
481 481
482 482 return r
483 483
484 484 def mergestate(self, clean=False):
485 485 """Get a mergestate object for this context."""
486 486 raise NotImplementedError(
487 487 '%s does not implement mergestate()' % self.__class__
488 488 )
489 489
490 490 def isempty(self):
491 491 return not (
492 492 len(self.parents()) > 1
493 493 or self.branch() != self.p1().branch()
494 494 or self.closesbranch()
495 495 or self.files()
496 496 )
497 497
498 498
499 499 class changectx(basectx):
500 500 """A changecontext object makes access to data related to a particular
501 501 changeset convenient. It represents a read-only context already present in
502 502 the repo."""
503 503
504 504 def __init__(self, repo, rev, node, maybe_filtered=True):
505 505 super(changectx, self).__init__(repo)
506 506 self._rev = rev
507 507 self._node = node
508 508 # When maybe_filtered is True, the revision might be affected by
509 509 # changelog filtering and operation through the filtered changelog must be used.
510 510 #
511 511 # When maybe_filtered is False, the revision has already been checked
512 512 # against filtering and is not filtered. Operation through the
513 513 # unfiltered changelog might be used in some case.
514 514 self._maybe_filtered = maybe_filtered
515 515
516 516 def __hash__(self):
517 517 try:
518 518 return hash(self._rev)
519 519 except AttributeError:
520 520 return id(self)
521 521
522 522 def __nonzero__(self):
523 523 return self._rev != nullrev
524 524
525 525 __bool__ = __nonzero__
526 526
527 527 @propertycache
528 528 def _changeset(self):
529 529 if self._maybe_filtered:
530 530 repo = self._repo
531 531 else:
532 532 repo = self._repo.unfiltered()
533 533 return repo.changelog.changelogrevision(self.rev())
534 534
535 535 @propertycache
536 536 def _manifest(self):
537 537 return self._manifestctx.read()
538 538
539 539 @property
540 540 def _manifestctx(self):
541 541 return self._repo.manifestlog[self._changeset.manifest]
542 542
543 543 @propertycache
544 544 def _manifestdelta(self):
545 545 return self._manifestctx.readdelta()
546 546
547 547 @propertycache
548 548 def _parents(self):
549 549 repo = self._repo
550 550 if self._maybe_filtered:
551 551 cl = repo.changelog
552 552 else:
553 553 cl = repo.unfiltered().changelog
554 554
555 555 p1, p2 = cl.parentrevs(self._rev)
556 556 if p2 == nullrev:
557 557 return [changectx(repo, p1, cl.node(p1), maybe_filtered=False)]
558 558 return [
559 559 changectx(repo, p1, cl.node(p1), maybe_filtered=False),
560 560 changectx(repo, p2, cl.node(p2), maybe_filtered=False),
561 561 ]
562 562
563 563 def changeset(self):
564 564 c = self._changeset
565 565 return (
566 566 c.manifest,
567 567 c.user,
568 568 c.date,
569 569 c.files,
570 570 c.description,
571 571 c.extra,
572 572 )
573 573
574 574 def manifestnode(self):
575 575 return self._changeset.manifest
576 576
577 577 def user(self):
578 578 return self._changeset.user
579 579
580 580 def date(self):
581 581 return self._changeset.date
582 582
583 583 def files(self):
584 584 return self._changeset.files
585 585
586 586 def filesmodified(self):
587 587 modified = set(self.files())
588 588 modified.difference_update(self.filesadded())
589 589 modified.difference_update(self.filesremoved())
590 590 return sorted(modified)
591 591
592 592 def filesadded(self):
593 593 filesadded = self._changeset.filesadded
594 594 compute_on_none = True
595 595 if self._repo.filecopiesmode == b'changeset-sidedata':
596 596 compute_on_none = False
597 597 else:
598 598 source = self._repo.ui.config(b'experimental', b'copies.read-from')
599 599 if source == b'changeset-only':
600 600 compute_on_none = False
601 601 elif source != b'compatibility':
602 602 # filelog mode, ignore any changelog content
603 603 filesadded = None
604 604 if filesadded is None:
605 605 if compute_on_none:
606 606 filesadded = metadata.computechangesetfilesadded(self)
607 607 else:
608 608 filesadded = []
609 609 return filesadded
610 610
611 611 def filesremoved(self):
612 612 filesremoved = self._changeset.filesremoved
613 613 compute_on_none = True
614 614 if self._repo.filecopiesmode == b'changeset-sidedata':
615 615 compute_on_none = False
616 616 else:
617 617 source = self._repo.ui.config(b'experimental', b'copies.read-from')
618 618 if source == b'changeset-only':
619 619 compute_on_none = False
620 620 elif source != b'compatibility':
621 621 # filelog mode, ignore any changelog content
622 622 filesremoved = None
623 623 if filesremoved is None:
624 624 if compute_on_none:
625 625 filesremoved = metadata.computechangesetfilesremoved(self)
626 626 else:
627 627 filesremoved = []
628 628 return filesremoved
629 629
630 630 @propertycache
631 631 def _copies(self):
632 632 p1copies = self._changeset.p1copies
633 633 p2copies = self._changeset.p2copies
634 634 compute_on_none = True
635 635 if self._repo.filecopiesmode == b'changeset-sidedata':
636 636 compute_on_none = False
637 637 else:
638 638 source = self._repo.ui.config(b'experimental', b'copies.read-from')
639 639 # If config says to get copy metadata only from changeset, then
640 640 # return that, defaulting to {} if there was no copy metadata. In
641 641 # compatibility mode, we return copy data from the changeset if it
642 642 # was recorded there, and otherwise we fall back to getting it from
643 643 # the filelogs (below).
644 644 #
645 645 # If we are in compatiblity mode and there is not data in the
646 646 # changeset), we get the copy metadata from the filelogs.
647 647 #
648 648 # otherwise, when config said to read only from filelog, we get the
649 649 # copy metadata from the filelogs.
650 650 if source == b'changeset-only':
651 651 compute_on_none = False
652 652 elif source != b'compatibility':
653 653 # filelog mode, ignore any changelog content
654 654 p1copies = p2copies = None
655 655 if p1copies is None:
656 656 if compute_on_none:
657 657 p1copies, p2copies = super(changectx, self)._copies
658 658 else:
659 659 if p1copies is None:
660 660 p1copies = {}
661 661 if p2copies is None:
662 662 p2copies = {}
663 663 return p1copies, p2copies
664 664
665 665 def description(self):
666 666 return self._changeset.description
667 667
668 668 def branch(self):
669 669 return encoding.tolocal(self._changeset.extra.get(b"branch"))
670 670
671 671 def closesbranch(self):
672 672 return b'close' in self._changeset.extra
673 673
674 674 def extra(self):
675 675 """Return a dict of extra information."""
676 676 return self._changeset.extra
677 677
678 678 def tags(self):
679 679 """Return a list of byte tag names"""
680 680 return self._repo.nodetags(self._node)
681 681
682 682 def bookmarks(self):
683 683 """Return a list of byte bookmark names."""
684 684 return self._repo.nodebookmarks(self._node)
685 685
686 686 def fast_rank(self):
687 687 repo = self._repo
688 688 if self._maybe_filtered:
689 689 cl = repo.changelog
690 690 else:
691 691 cl = repo.unfiltered().changelog
692 692 return cl.fast_rank(self._rev)
693 693
694 694 def phase(self):
695 695 return self._repo._phasecache.phase(self._repo, self._rev)
696 696
697 697 def hidden(self):
698 698 return self._rev in repoview.filterrevs(self._repo, b'visible')
699 699
700 700 def isinmemory(self):
701 701 return False
702 702
703 703 def children(self):
704 704 """return list of changectx contexts for each child changeset.
705 705
706 706 This returns only the immediate child changesets. Use descendants() to
707 707 recursively walk children.
708 708 """
709 709 c = self._repo.changelog.children(self._node)
710 710 return [self._repo[x] for x in c]
711 711
712 712 def ancestors(self):
713 713 for a in self._repo.changelog.ancestors([self._rev]):
714 714 yield self._repo[a]
715 715
716 716 def descendants(self):
717 717 """Recursively yield all children of the changeset.
718 718
719 719 For just the immediate children, use children()
720 720 """
721 721 for d in self._repo.changelog.descendants([self._rev]):
722 722 yield self._repo[d]
723 723
724 724 def filectx(self, path, fileid=None, filelog=None):
725 725 """get a file context from this changeset"""
726 726 if fileid is None:
727 727 fileid = self.filenode(path)
728 728 return filectx(
729 729 self._repo, path, fileid=fileid, changectx=self, filelog=filelog
730 730 )
731 731
732 732 def ancestor(self, c2, warn=False):
733 733 """return the "best" ancestor context of self and c2
734 734
735 735 If there are multiple candidates, it will show a message and check
736 736 merge.preferancestor configuration before falling back to the
737 737 revlog ancestor."""
738 738 # deal with workingctxs
739 739 n2 = c2._node
740 740 if n2 is None:
741 741 n2 = c2._parents[0]._node
742 742 cahs = self._repo.changelog.commonancestorsheads(self._node, n2)
743 743 if not cahs:
744 744 anc = self._repo.nodeconstants.nullid
745 745 elif len(cahs) == 1:
746 746 anc = cahs[0]
747 747 else:
748 748 # experimental config: merge.preferancestor
749 749 for r in self._repo.ui.configlist(b'merge', b'preferancestor'):
750 750 try:
751 751 ctx = scmutil.revsymbol(self._repo, r)
752 752 except error.RepoLookupError:
753 753 continue
754 754 anc = ctx.node()
755 755 if anc in cahs:
756 756 break
757 757 else:
758 758 anc = self._repo.changelog.ancestor(self._node, n2)
759 759 if warn:
760 760 self._repo.ui.status(
761 761 (
762 762 _(b"note: using %s as ancestor of %s and %s\n")
763 763 % (short(anc), short(self._node), short(n2))
764 764 )
765 765 + b''.join(
766 766 _(
767 767 b" alternatively, use --config "
768 768 b"merge.preferancestor=%s\n"
769 769 )
770 770 % short(n)
771 771 for n in sorted(cahs)
772 772 if n != anc
773 773 )
774 774 )
775 775 return self._repo[anc]
776 776
777 777 def isancestorof(self, other):
778 778 """True if this changeset is an ancestor of other"""
779 779 return self._repo.changelog.isancestorrev(self._rev, other._rev)
780 780
781 781 def walk(self, match):
782 782 '''Generates matching file names.'''
783 783
784 784 # Wrap match.bad method to have message with nodeid
785 785 def bad(fn, msg):
786 786 # The manifest doesn't know about subrepos, so don't complain about
787 787 # paths into valid subrepos.
788 788 if any(fn == s or fn.startswith(s + b'/') for s in self.substate):
789 789 return
790 790 match.bad(fn, _(b'no such file in rev %s') % self)
791 791
792 792 m = matchmod.badmatch(self._repo.narrowmatch(match), bad)
793 793 return self._manifest.walk(m)
794 794
795 795 def matches(self, match):
796 796 return self.walk(match)
797 797
798 798
799 799 class basefilectx:
800 800 """A filecontext object represents the common logic for its children:
801 801 filectx: read-only access to a filerevision that is already present
802 802 in the repo,
803 803 workingfilectx: a filecontext that represents files from the working
804 804 directory,
805 805 memfilectx: a filecontext that represents files in-memory,
806 806 """
807 807
808 808 @propertycache
809 809 def _filelog(self):
810 810 return self._repo.file(self._path)
811 811
812 812 @propertycache
813 813 def _changeid(self):
814 814 if '_changectx' in self.__dict__:
815 815 return self._changectx.rev()
816 816 elif '_descendantrev' in self.__dict__:
817 817 # this file context was created from a revision with a known
818 818 # descendant, we can (lazily) correct for linkrev aliases
819 819 return self._adjustlinkrev(self._descendantrev)
820 820 else:
821 821 return self._filelog.linkrev(self._filerev)
822 822
823 823 @propertycache
824 824 def _filenode(self):
825 825 if '_fileid' in self.__dict__:
826 826 return self._filelog.lookup(self._fileid)
827 827 else:
828 828 return self._changectx.filenode(self._path)
829 829
830 830 @propertycache
831 831 def _filerev(self):
832 832 return self._filelog.rev(self._filenode)
833 833
834 834 @propertycache
835 835 def _repopath(self):
836 836 return self._path
837 837
838 838 def __nonzero__(self):
839 839 try:
840 840 self._filenode
841 841 return True
842 842 except error.LookupError:
843 843 # file is missing
844 844 return False
845 845
846 846 __bool__ = __nonzero__
847 847
848 848 def __bytes__(self):
849 849 try:
850 850 return b"%s@%s" % (self.path(), self._changectx)
851 851 except error.LookupError:
852 852 return b"%s@???" % self.path()
853 853
854 854 __str__ = encoding.strmethod(__bytes__)
855 855
856 856 def __repr__(self):
857 857 return "<%s %s>" % (type(self).__name__, str(self))
858 858
859 859 def __hash__(self):
860 860 try:
861 861 return hash((self._path, self._filenode))
862 862 except AttributeError:
863 863 return id(self)
864 864
865 865 def __eq__(self, other):
866 866 try:
867 867 return (
868 868 type(self) == type(other)
869 869 and self._path == other._path
870 870 and self._filenode == other._filenode
871 871 )
872 872 except AttributeError:
873 873 return False
874 874
875 875 def __ne__(self, other):
876 876 return not (self == other)
877 877
878 878 def filerev(self):
879 879 return self._filerev
880 880
881 881 def filenode(self):
882 882 return self._filenode
883 883
884 884 @propertycache
885 885 def _flags(self):
886 886 return self._changectx.flags(self._path)
887 887
888 888 def flags(self):
889 889 return self._flags
890 890
891 891 def filelog(self):
892 892 return self._filelog
893 893
894 894 def rev(self):
895 895 return self._changeid
896 896
897 897 def linkrev(self):
898 898 return self._filelog.linkrev(self._filerev)
899 899
900 900 def node(self):
901 901 return self._changectx.node()
902 902
903 903 def hex(self):
904 904 return self._changectx.hex()
905 905
906 906 def user(self):
907 907 return self._changectx.user()
908 908
909 909 def date(self):
910 910 return self._changectx.date()
911 911
912 912 def files(self):
913 913 return self._changectx.files()
914 914
915 915 def description(self):
916 916 return self._changectx.description()
917 917
918 918 def branch(self):
919 919 return self._changectx.branch()
920 920
921 921 def extra(self):
922 922 return self._changectx.extra()
923 923
924 924 def phase(self):
925 925 return self._changectx.phase()
926 926
927 927 def phasestr(self):
928 928 return self._changectx.phasestr()
929 929
930 930 def obsolete(self):
931 931 return self._changectx.obsolete()
932 932
933 933 def instabilities(self):
934 934 return self._changectx.instabilities()
935 935
936 936 def manifest(self):
937 937 return self._changectx.manifest()
938 938
939 939 def changectx(self):
940 940 return self._changectx
941 941
942 942 def renamed(self):
943 943 return self._copied
944 944
945 945 def copysource(self):
946 946 return self._copied and self._copied[0]
947 947
948 948 def repo(self):
949 949 return self._repo
950 950
951 951 def size(self):
952 952 return len(self.data())
953 953
954 954 def path(self):
955 955 return self._path
956 956
957 957 def isbinary(self):
958 958 try:
959 959 return stringutil.binary(self.data())
960 960 except IOError:
961 961 return False
962 962
963 963 def isexec(self):
964 964 return b'x' in self.flags()
965 965
966 966 def islink(self):
967 967 return b'l' in self.flags()
968 968
969 969 def isabsent(self):
970 970 """whether this filectx represents a file not in self._changectx
971 971
972 972 This is mainly for merge code to detect change/delete conflicts. This is
973 973 expected to be True for all subclasses of basectx."""
974 974 return False
975 975
976 976 _customcmp = False
977 977
978 978 def cmp(self, fctx):
979 979 """compare with other file context
980 980
981 981 returns True if different than fctx.
982 982 """
983 983 if fctx._customcmp:
984 984 return fctx.cmp(self)
985 985
986 986 if self._filenode is None:
987 987 raise error.ProgrammingError(
988 988 b'filectx.cmp() must be reimplemented if not backed by revlog'
989 989 )
990 990
991 991 if fctx._filenode is None:
992 992 if self._repo._encodefilterpats:
993 993 # can't rely on size() because wdir content may be decoded
994 994 return self._filelog.cmp(self._filenode, fctx.data())
995 # filelog.size() has two special cases:
996 # - censored metadata
997 # - copy/rename tracking
998 # The first is detected by peaking into the delta,
999 # the second is detected by abusing parent order
1000 # in the revlog index as flag bit. This leaves files using
1001 # the dummy encoding and non-standard meta attributes.
1002 # The following check is a special case for the empty
1003 # metadata block used if the raw file content starts with '\1\n'.
1004 # Cases of arbitrary metadata flags are currently mishandled.
995 1005 if self.size() - 4 == fctx.size():
996 1006 # size() can match:
997 1007 # if file data starts with '\1\n', empty metadata block is
998 1008 # prepended, which adds 4 bytes to filelog.size().
999 1009 return self._filelog.cmp(self._filenode, fctx.data())
1000 1010 if self.size() == fctx.size() or self.flags() == b'l':
1001 1011 # size() matches: need to compare content
1002 1012 # issue6456: Always compare symlinks because size can represent
1003 1013 # encrypted string for EXT-4 encryption(fscrypt).
1004 1014 return self._filelog.cmp(self._filenode, fctx.data())
1005 1015
1006 1016 # size() differs
1007 1017 return True
1008 1018
1009 1019 def _adjustlinkrev(self, srcrev, inclusive=False, stoprev=None):
1010 1020 """return the first ancestor of <srcrev> introducing <fnode>
1011 1021
1012 1022 If the linkrev of the file revision does not point to an ancestor of
1013 1023 srcrev, we'll walk down the ancestors until we find one introducing
1014 1024 this file revision.
1015 1025
1016 1026 :srcrev: the changeset revision we search ancestors from
1017 1027 :inclusive: if true, the src revision will also be checked
1018 1028 :stoprev: an optional revision to stop the walk at. If no introduction
1019 1029 of this file content could be found before this floor
1020 1030 revision, the function will returns "None" and stops its
1021 1031 iteration.
1022 1032 """
1023 1033 repo = self._repo
1024 1034 cl = repo.unfiltered().changelog
1025 1035 mfl = repo.manifestlog
1026 1036 # fetch the linkrev
1027 1037 lkr = self.linkrev()
1028 1038 if srcrev == lkr:
1029 1039 return lkr
1030 1040 # hack to reuse ancestor computation when searching for renames
1031 1041 memberanc = getattr(self, '_ancestrycontext', None)
1032 1042 iteranc = None
1033 1043 if srcrev is None:
1034 1044 # wctx case, used by workingfilectx during mergecopy
1035 1045 revs = [p.rev() for p in self._repo[None].parents()]
1036 1046 inclusive = True # we skipped the real (revless) source
1037 1047 else:
1038 1048 revs = [srcrev]
1039 1049 if memberanc is None:
1040 1050 memberanc = iteranc = cl.ancestors(revs, lkr, inclusive=inclusive)
1041 1051 # check if this linkrev is an ancestor of srcrev
1042 1052 if lkr not in memberanc:
1043 1053 if iteranc is None:
1044 1054 iteranc = cl.ancestors(revs, lkr, inclusive=inclusive)
1045 1055 fnode = self._filenode
1046 1056 path = self._path
1047 1057 for a in iteranc:
1048 1058 if stoprev is not None and a < stoprev:
1049 1059 return None
1050 1060 ac = cl.read(a) # get changeset data (we avoid object creation)
1051 1061 if path in ac[3]: # checking the 'files' field.
1052 1062 # The file has been touched, check if the content is
1053 1063 # similar to the one we search for.
1054 1064 if fnode == mfl[ac[0]].readfast().get(path):
1055 1065 return a
1056 1066 # In theory, we should never get out of that loop without a result.
1057 1067 # But if manifest uses a buggy file revision (not children of the
1058 1068 # one it replaces) we could. Such a buggy situation will likely
1059 1069 # result is crash somewhere else at to some point.
1060 1070 return lkr
1061 1071
1062 1072 def isintroducedafter(self, changelogrev):
1063 1073 """True if a filectx has been introduced after a given floor revision"""
1064 1074 if self.linkrev() >= changelogrev:
1065 1075 return True
1066 1076 introrev = self._introrev(stoprev=changelogrev)
1067 1077 if introrev is None:
1068 1078 return False
1069 1079 return introrev >= changelogrev
1070 1080
1071 1081 def introrev(self):
1072 1082 """return the rev of the changeset which introduced this file revision
1073 1083
1074 1084 This method is different from linkrev because it take into account the
1075 1085 changeset the filectx was created from. It ensures the returned
1076 1086 revision is one of its ancestors. This prevents bugs from
1077 1087 'linkrev-shadowing' when a file revision is used by multiple
1078 1088 changesets.
1079 1089 """
1080 1090 return self._introrev()
1081 1091
1082 1092 def _introrev(self, stoprev=None):
1083 1093 """
1084 1094 Same as `introrev` but, with an extra argument to limit changelog
1085 1095 iteration range in some internal usecase.
1086 1096
1087 1097 If `stoprev` is set, the `introrev` will not be searched past that
1088 1098 `stoprev` revision and "None" might be returned. This is useful to
1089 1099 limit the iteration range.
1090 1100 """
1091 1101 toprev = None
1092 1102 attrs = vars(self)
1093 1103 if '_changeid' in attrs:
1094 1104 # We have a cached value already
1095 1105 toprev = self._changeid
1096 1106 elif '_changectx' in attrs:
1097 1107 # We know which changelog entry we are coming from
1098 1108 toprev = self._changectx.rev()
1099 1109
1100 1110 if toprev is not None:
1101 1111 return self._adjustlinkrev(toprev, inclusive=True, stoprev=stoprev)
1102 1112 elif '_descendantrev' in attrs:
1103 1113 introrev = self._adjustlinkrev(self._descendantrev, stoprev=stoprev)
1104 1114 # be nice and cache the result of the computation
1105 1115 if introrev is not None:
1106 1116 self._changeid = introrev
1107 1117 return introrev
1108 1118 else:
1109 1119 return self.linkrev()
1110 1120
1111 1121 def introfilectx(self):
1112 1122 """Return filectx having identical contents, but pointing to the
1113 1123 changeset revision where this filectx was introduced"""
1114 1124 introrev = self.introrev()
1115 1125 if self.rev() == introrev:
1116 1126 return self
1117 1127 return self.filectx(self.filenode(), changeid=introrev)
1118 1128
1119 1129 def _parentfilectx(self, path, fileid, filelog):
1120 1130 """create parent filectx keeping ancestry info for _adjustlinkrev()"""
1121 1131 fctx = filectx(self._repo, path, fileid=fileid, filelog=filelog)
1122 1132 if '_changeid' in vars(self) or '_changectx' in vars(self):
1123 1133 # If self is associated with a changeset (probably explicitly
1124 1134 # fed), ensure the created filectx is associated with a
1125 1135 # changeset that is an ancestor of self.changectx.
1126 1136 # This lets us later use _adjustlinkrev to get a correct link.
1127 1137 fctx._descendantrev = self.rev()
1128 1138 fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
1129 1139 elif '_descendantrev' in vars(self):
1130 1140 # Otherwise propagate _descendantrev if we have one associated.
1131 1141 fctx._descendantrev = self._descendantrev
1132 1142 fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
1133 1143 return fctx
1134 1144
1135 1145 def parents(self):
1136 1146 _path = self._path
1137 1147 fl = self._filelog
1138 1148 parents = self._filelog.parents(self._filenode)
1139 1149 pl = [
1140 1150 (_path, node, fl)
1141 1151 for node in parents
1142 1152 if node != self._repo.nodeconstants.nullid
1143 1153 ]
1144 1154
1145 1155 r = fl.renamed(self._filenode)
1146 1156 if r:
1147 1157 # - In the simple rename case, both parent are nullid, pl is empty.
1148 1158 # - In case of merge, only one of the parent is null id and should
1149 1159 # be replaced with the rename information. This parent is -always-
1150 1160 # the first one.
1151 1161 #
1152 1162 # As null id have always been filtered out in the previous list
1153 1163 # comprehension, inserting to 0 will always result in "replacing
1154 1164 # first nullid parent with rename information.
1155 1165 pl.insert(0, (r[0], r[1], self._repo.file(r[0])))
1156 1166
1157 1167 return [self._parentfilectx(path, fnode, l) for path, fnode, l in pl]
1158 1168
1159 1169 def p1(self):
1160 1170 return self.parents()[0]
1161 1171
1162 1172 def p2(self):
1163 1173 p = self.parents()
1164 1174 if len(p) == 2:
1165 1175 return p[1]
1166 1176 return filectx(self._repo, self._path, fileid=-1, filelog=self._filelog)
1167 1177
1168 1178 def annotate(self, follow=False, skiprevs=None, diffopts=None):
1169 1179 """Returns a list of annotateline objects for each line in the file
1170 1180
1171 1181 - line.fctx is the filectx of the node where that line was last changed
1172 1182 - line.lineno is the line number at the first appearance in the managed
1173 1183 file
1174 1184 - line.text is the data on that line (including newline character)
1175 1185 """
1176 1186 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
1177 1187
1178 1188 def parents(f):
1179 1189 # Cut _descendantrev here to mitigate the penalty of lazy linkrev
1180 1190 # adjustment. Otherwise, p._adjustlinkrev() would walk changelog
1181 1191 # from the topmost introrev (= srcrev) down to p.linkrev() if it
1182 1192 # isn't an ancestor of the srcrev.
1183 1193 f._changeid
1184 1194 pl = f.parents()
1185 1195
1186 1196 # Don't return renamed parents if we aren't following.
1187 1197 if not follow:
1188 1198 pl = [p for p in pl if p.path() == f.path()]
1189 1199
1190 1200 # renamed filectx won't have a filelog yet, so set it
1191 1201 # from the cache to save time
1192 1202 for p in pl:
1193 1203 if not '_filelog' in p.__dict__:
1194 1204 p._filelog = getlog(p.path())
1195 1205
1196 1206 return pl
1197 1207
1198 1208 # use linkrev to find the first changeset where self appeared
1199 1209 base = self.introfilectx()
1200 1210 if getattr(base, '_ancestrycontext', None) is None:
1201 1211 # it is safe to use an unfiltered repository here because we are
1202 1212 # walking ancestors only.
1203 1213 cl = self._repo.unfiltered().changelog
1204 1214 if base.rev() is None:
1205 1215 # wctx is not inclusive, but works because _ancestrycontext
1206 1216 # is used to test filelog revisions
1207 1217 ac = cl.ancestors(
1208 1218 [p.rev() for p in base.parents()], inclusive=True
1209 1219 )
1210 1220 else:
1211 1221 ac = cl.ancestors([base.rev()], inclusive=True)
1212 1222 base._ancestrycontext = ac
1213 1223
1214 1224 return dagop.annotate(
1215 1225 base, parents, skiprevs=skiprevs, diffopts=diffopts
1216 1226 )
1217 1227
1218 1228 def ancestors(self, followfirst=False):
1219 1229 visit = {}
1220 1230 c = self
1221 1231 if followfirst:
1222 1232 cut = 1
1223 1233 else:
1224 1234 cut = None
1225 1235
1226 1236 while True:
1227 1237 for parent in c.parents()[:cut]:
1228 1238 visit[(parent.linkrev(), parent.filenode())] = parent
1229 1239 if not visit:
1230 1240 break
1231 1241 c = visit.pop(max(visit))
1232 1242 yield c
1233 1243
1234 1244 def decodeddata(self):
1235 1245 """Returns `data()` after running repository decoding filters.
1236 1246
1237 1247 This is often equivalent to how the data would be expressed on disk.
1238 1248 """
1239 1249 return self._repo.wwritedata(self.path(), self.data())
1240 1250
1241 1251
1242 1252 class filectx(basefilectx):
1243 1253 """A filecontext object makes access to data related to a particular
1244 1254 filerevision convenient."""
1245 1255
1246 1256 def __init__(
1247 1257 self,
1248 1258 repo,
1249 1259 path,
1250 1260 changeid=None,
1251 1261 fileid=None,
1252 1262 filelog=None,
1253 1263 changectx=None,
1254 1264 ):
1255 1265 """changeid must be a revision number, if specified.
1256 1266 fileid can be a file revision or node."""
1257 1267 self._repo = repo
1258 1268 self._path = path
1259 1269
1260 1270 assert (
1261 1271 changeid is not None or fileid is not None or changectx is not None
1262 1272 ), b"bad args: changeid=%r, fileid=%r, changectx=%r" % (
1263 1273 changeid,
1264 1274 fileid,
1265 1275 changectx,
1266 1276 )
1267 1277
1268 1278 if filelog is not None:
1269 1279 self._filelog = filelog
1270 1280
1271 1281 if changeid is not None:
1272 1282 self._changeid = changeid
1273 1283 if changectx is not None:
1274 1284 self._changectx = changectx
1275 1285 if fileid is not None:
1276 1286 self._fileid = fileid
1277 1287
1278 1288 @propertycache
1279 1289 def _changectx(self):
1280 1290 try:
1281 1291 return self._repo[self._changeid]
1282 1292 except error.FilteredRepoLookupError:
1283 1293 # Linkrev may point to any revision in the repository. When the
1284 1294 # repository is filtered this may lead to `filectx` trying to build
1285 1295 # `changectx` for filtered revision. In such case we fallback to
1286 1296 # creating `changectx` on the unfiltered version of the reposition.
1287 1297 # This fallback should not be an issue because `changectx` from
1288 1298 # `filectx` are not used in complex operations that care about
1289 1299 # filtering.
1290 1300 #
1291 1301 # This fallback is a cheap and dirty fix that prevent several
1292 1302 # crashes. It does not ensure the behavior is correct. However the
1293 1303 # behavior was not correct before filtering either and "incorrect
1294 1304 # behavior" is seen as better as "crash"
1295 1305 #
1296 1306 # Linkrevs have several serious troubles with filtering that are
1297 1307 # complicated to solve. Proper handling of the issue here should be
1298 1308 # considered when solving linkrev issue are on the table.
1299 1309 return self._repo.unfiltered()[self._changeid]
1300 1310
1301 1311 def filectx(self, fileid, changeid=None):
1302 1312 """opens an arbitrary revision of the file without
1303 1313 opening a new filelog"""
1304 1314 return filectx(
1305 1315 self._repo,
1306 1316 self._path,
1307 1317 fileid=fileid,
1308 1318 filelog=self._filelog,
1309 1319 changeid=changeid,
1310 1320 )
1311 1321
1312 1322 def rawdata(self):
1313 1323 return self._filelog.rawdata(self._filenode)
1314 1324
1315 1325 def rawflags(self):
1316 1326 """low-level revlog flags"""
1317 1327 return self._filelog.flags(self._filerev)
1318 1328
1319 1329 def data(self):
1320 1330 try:
1321 1331 return self._filelog.read(self._filenode)
1322 1332 except error.CensoredNodeError:
1323 1333 if self._repo.ui.config(b"censor", b"policy") == b"ignore":
1324 1334 return b""
1325 1335 raise error.Abort(
1326 1336 _(b"censored node: %s") % short(self._filenode),
1327 1337 hint=_(b"set censor.policy to ignore errors"),
1328 1338 )
1329 1339
1330 1340 def size(self):
1331 1341 return self._filelog.size(self._filerev)
1332 1342
1333 1343 @propertycache
1334 1344 def _copied(self):
1335 1345 """check if file was actually renamed in this changeset revision
1336 1346
1337 1347 If rename logged in file revision, we report copy for changeset only
1338 1348 if file revisions linkrev points back to the changeset in question
1339 1349 or both changeset parents contain different file revisions.
1340 1350 """
1341 1351
1342 1352 renamed = self._filelog.renamed(self._filenode)
1343 1353 if not renamed:
1344 1354 return None
1345 1355
1346 1356 if self.rev() == self.linkrev():
1347 1357 return renamed
1348 1358
1349 1359 name = self.path()
1350 1360 fnode = self._filenode
1351 1361 for p in self._changectx.parents():
1352 1362 try:
1353 1363 if fnode == p.filenode(name):
1354 1364 return None
1355 1365 except error.LookupError:
1356 1366 pass
1357 1367 return renamed
1358 1368
1359 1369 def children(self):
1360 1370 # hard for renames
1361 1371 c = self._filelog.children(self._filenode)
1362 1372 return [
1363 1373 filectx(self._repo, self._path, fileid=x, filelog=self._filelog)
1364 1374 for x in c
1365 1375 ]
1366 1376
1367 1377
1368 1378 class committablectx(basectx):
1369 1379 """A committablectx object provides common functionality for a context that
1370 1380 wants the ability to commit, e.g. workingctx or memctx."""
1371 1381
1372 1382 def __init__(
1373 1383 self,
1374 1384 repo,
1375 1385 text=b"",
1376 1386 user=None,
1377 1387 date=None,
1378 1388 extra=None,
1379 1389 changes=None,
1380 1390 branch=None,
1381 1391 ):
1382 1392 super(committablectx, self).__init__(repo)
1383 1393 self._rev = None
1384 1394 self._node = None
1385 1395 self._text = text
1386 1396 if date:
1387 1397 self._date = dateutil.parsedate(date)
1388 1398 if user:
1389 1399 self._user = user
1390 1400 if changes:
1391 1401 self._status = changes
1392 1402
1393 1403 self._extra = {}
1394 1404 if extra:
1395 1405 self._extra = extra.copy()
1396 1406 if branch is not None:
1397 1407 self._extra[b'branch'] = encoding.fromlocal(branch)
1398 1408 if not self._extra.get(b'branch'):
1399 1409 self._extra[b'branch'] = b'default'
1400 1410
1401 1411 def __bytes__(self):
1402 1412 return bytes(self._parents[0]) + b"+"
1403 1413
1404 1414 def hex(self):
1405 1415 self._repo.nodeconstants.wdirhex
1406 1416
1407 1417 __str__ = encoding.strmethod(__bytes__)
1408 1418
1409 1419 def __nonzero__(self):
1410 1420 return True
1411 1421
1412 1422 __bool__ = __nonzero__
1413 1423
1414 1424 @propertycache
1415 1425 def _status(self):
1416 1426 return self._repo.status()
1417 1427
1418 1428 @propertycache
1419 1429 def _user(self):
1420 1430 return self._repo.ui.username()
1421 1431
1422 1432 @propertycache
1423 1433 def _date(self):
1424 1434 ui = self._repo.ui
1425 1435 date = ui.configdate(b'devel', b'default-date')
1426 1436 if date is None:
1427 1437 date = dateutil.makedate()
1428 1438 return date
1429 1439
1430 1440 def subrev(self, subpath):
1431 1441 return None
1432 1442
1433 1443 def manifestnode(self):
1434 1444 return None
1435 1445
1436 1446 def user(self):
1437 1447 return self._user or self._repo.ui.username()
1438 1448
1439 1449 def date(self):
1440 1450 return self._date
1441 1451
1442 1452 def description(self):
1443 1453 return self._text
1444 1454
1445 1455 def files(self):
1446 1456 return sorted(
1447 1457 self._status.modified + self._status.added + self._status.removed
1448 1458 )
1449 1459
1450 1460 def modified(self):
1451 1461 return self._status.modified
1452 1462
1453 1463 def added(self):
1454 1464 return self._status.added
1455 1465
1456 1466 def removed(self):
1457 1467 return self._status.removed
1458 1468
1459 1469 def deleted(self):
1460 1470 return self._status.deleted
1461 1471
1462 1472 filesmodified = modified
1463 1473 filesadded = added
1464 1474 filesremoved = removed
1465 1475
1466 1476 def branch(self):
1467 1477 return encoding.tolocal(self._extra[b'branch'])
1468 1478
1469 1479 def closesbranch(self):
1470 1480 return b'close' in self._extra
1471 1481
1472 1482 def extra(self):
1473 1483 return self._extra
1474 1484
1475 1485 def isinmemory(self):
1476 1486 return False
1477 1487
1478 1488 def tags(self):
1479 1489 return []
1480 1490
1481 1491 def bookmarks(self):
1482 1492 b = []
1483 1493 for p in self.parents():
1484 1494 b.extend(p.bookmarks())
1485 1495 return b
1486 1496
1487 1497 def phase(self):
1488 1498 phase = phases.newcommitphase(self._repo.ui)
1489 1499 for p in self.parents():
1490 1500 phase = max(phase, p.phase())
1491 1501 return phase
1492 1502
1493 1503 def hidden(self):
1494 1504 return False
1495 1505
1496 1506 def children(self):
1497 1507 return []
1498 1508
1499 1509 def flags(self, path):
1500 1510 if '_manifest' in self.__dict__:
1501 1511 try:
1502 1512 return self._manifest.flags(path)
1503 1513 except KeyError:
1504 1514 return b''
1505 1515
1506 1516 try:
1507 1517 return self._flagfunc(path)
1508 1518 except OSError:
1509 1519 return b''
1510 1520
1511 1521 def ancestor(self, c2):
1512 1522 """return the "best" ancestor context of self and c2"""
1513 1523 return self._parents[0].ancestor(c2) # punt on two parents for now
1514 1524
1515 1525 def ancestors(self):
1516 1526 for p in self._parents:
1517 1527 yield p
1518 1528 for a in self._repo.changelog.ancestors(
1519 1529 [p.rev() for p in self._parents]
1520 1530 ):
1521 1531 yield self._repo[a]
1522 1532
1523 1533 def markcommitted(self, node):
1524 1534 """Perform post-commit cleanup necessary after committing this ctx
1525 1535
1526 1536 Specifically, this updates backing stores this working context
1527 1537 wraps to reflect the fact that the changes reflected by this
1528 1538 workingctx have been committed. For example, it marks
1529 1539 modified and added files as normal in the dirstate.
1530 1540
1531 1541 """
1532 1542
1533 1543 def dirty(self, missing=False, merge=True, branch=True):
1534 1544 return False
1535 1545
1536 1546
1537 1547 class workingctx(committablectx):
1538 1548 """A workingctx object makes access to data related to
1539 1549 the current working directory convenient.
1540 1550 date - any valid date string or (unixtime, offset), or None.
1541 1551 user - username string, or None.
1542 1552 extra - a dictionary of extra values, or None.
1543 1553 changes - a list of file lists as returned by localrepo.status()
1544 1554 or None to use the repository status.
1545 1555 """
1546 1556
1547 1557 def __init__(
1548 1558 self, repo, text=b"", user=None, date=None, extra=None, changes=None
1549 1559 ):
1550 1560 branch = None
1551 1561 if not extra or b'branch' not in extra:
1552 1562 try:
1553 1563 branch = repo.dirstate.branch()
1554 1564 except UnicodeDecodeError:
1555 1565 raise error.Abort(_(b'branch name not in UTF-8!'))
1556 1566 super(workingctx, self).__init__(
1557 1567 repo, text, user, date, extra, changes, branch=branch
1558 1568 )
1559 1569
1560 1570 def __iter__(self):
1561 1571 d = self._repo.dirstate
1562 1572 for f in d:
1563 1573 if d.get_entry(f).tracked:
1564 1574 yield f
1565 1575
1566 1576 def __contains__(self, key):
1567 1577 return self._repo.dirstate.get_entry(key).tracked
1568 1578
1569 1579 def hex(self):
1570 1580 return self._repo.nodeconstants.wdirhex
1571 1581
1572 1582 @propertycache
1573 1583 def _parents(self):
1574 1584 p = self._repo.dirstate.parents()
1575 1585 if p[1] == self._repo.nodeconstants.nullid:
1576 1586 p = p[:-1]
1577 1587 # use unfiltered repo to delay/avoid loading obsmarkers
1578 1588 unfi = self._repo.unfiltered()
1579 1589 return [
1580 1590 changectx(
1581 1591 self._repo, unfi.changelog.rev(n), n, maybe_filtered=False
1582 1592 )
1583 1593 for n in p
1584 1594 ]
1585 1595
1586 1596 def setparents(self, p1node, p2node=None):
1587 1597 if p2node is None:
1588 1598 p2node = self._repo.nodeconstants.nullid
1589 1599 dirstate = self._repo.dirstate
1590 1600 with dirstate.parentchange():
1591 1601 copies = dirstate.setparents(p1node, p2node)
1592 1602 pctx = self._repo[p1node]
1593 1603 if copies:
1594 1604 # Adjust copy records, the dirstate cannot do it, it
1595 1605 # requires access to parents manifests. Preserve them
1596 1606 # only for entries added to first parent.
1597 1607 for f in copies:
1598 1608 if f not in pctx and copies[f] in pctx:
1599 1609 dirstate.copy(copies[f], f)
1600 1610 if p2node == self._repo.nodeconstants.nullid:
1601 1611 for f, s in sorted(dirstate.copies().items()):
1602 1612 if f not in pctx and s not in pctx:
1603 1613 dirstate.copy(None, f)
1604 1614
1605 1615 def _fileinfo(self, path):
1606 1616 # populate __dict__['_manifest'] as workingctx has no _manifestdelta
1607 1617 self._manifest
1608 1618 return super(workingctx, self)._fileinfo(path)
1609 1619
1610 1620 def _buildflagfunc(self):
1611 1621 # Create a fallback function for getting file flags when the
1612 1622 # filesystem doesn't support them
1613 1623
1614 1624 copiesget = self._repo.dirstate.copies().get
1615 1625 parents = self.parents()
1616 1626 if len(parents) < 2:
1617 1627 # when we have one parent, it's easy: copy from parent
1618 1628 man = parents[0].manifest()
1619 1629
1620 1630 def func(f):
1621 1631 f = copiesget(f, f)
1622 1632 return man.flags(f)
1623 1633
1624 1634 else:
1625 1635 # merges are tricky: we try to reconstruct the unstored
1626 1636 # result from the merge (issue1802)
1627 1637 p1, p2 = parents
1628 1638 pa = p1.ancestor(p2)
1629 1639 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
1630 1640
1631 1641 def func(f):
1632 1642 f = copiesget(f, f) # may be wrong for merges with copies
1633 1643 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
1634 1644 if fl1 == fl2:
1635 1645 return fl1
1636 1646 if fl1 == fla:
1637 1647 return fl2
1638 1648 if fl2 == fla:
1639 1649 return fl1
1640 1650 return b'' # punt for conflicts
1641 1651
1642 1652 return func
1643 1653
1644 1654 @propertycache
1645 1655 def _flagfunc(self):
1646 1656 return self._repo.dirstate.flagfunc(self._buildflagfunc)
1647 1657
1648 1658 def flags(self, path):
1649 1659 try:
1650 1660 return self._flagfunc(path)
1651 1661 except OSError:
1652 1662 return b''
1653 1663
1654 1664 def filectx(self, path, filelog=None):
1655 1665 """get a file context from the working directory"""
1656 1666 return workingfilectx(
1657 1667 self._repo, path, workingctx=self, filelog=filelog
1658 1668 )
1659 1669
1660 1670 def dirty(self, missing=False, merge=True, branch=True):
1661 1671 """check whether a working directory is modified"""
1662 1672 # check subrepos first
1663 1673 for s in sorted(self.substate):
1664 1674 if self.sub(s).dirty(missing=missing):
1665 1675 return True
1666 1676 # check current working dir
1667 1677 return (
1668 1678 (merge and self.p2())
1669 1679 or (branch and self.branch() != self.p1().branch())
1670 1680 or self.modified()
1671 1681 or self.added()
1672 1682 or self.removed()
1673 1683 or (missing and self.deleted())
1674 1684 )
1675 1685
1676 1686 def add(self, list, prefix=b""):
1677 1687 with self._repo.wlock():
1678 1688 ui, ds = self._repo.ui, self._repo.dirstate
1679 1689 uipath = lambda f: ds.pathto(pathutil.join(prefix, f))
1680 1690 rejected = []
1681 1691 lstat = self._repo.wvfs.lstat
1682 1692 for f in list:
1683 1693 # ds.pathto() returns an absolute file when this is invoked from
1684 1694 # the keyword extension. That gets flagged as non-portable on
1685 1695 # Windows, since it contains the drive letter and colon.
1686 1696 scmutil.checkportable(ui, os.path.join(prefix, f))
1687 1697 try:
1688 1698 st = lstat(f)
1689 1699 except OSError:
1690 1700 ui.warn(_(b"%s does not exist!\n") % uipath(f))
1691 1701 rejected.append(f)
1692 1702 continue
1693 1703 limit = ui.configbytes(b'ui', b'large-file-limit')
1694 1704 if limit != 0 and st.st_size > limit:
1695 1705 ui.warn(
1696 1706 _(
1697 1707 b"%s: up to %d MB of RAM may be required "
1698 1708 b"to manage this file\n"
1699 1709 b"(use 'hg revert %s' to cancel the "
1700 1710 b"pending addition)\n"
1701 1711 )
1702 1712 % (f, 3 * st.st_size // 1000000, uipath(f))
1703 1713 )
1704 1714 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1705 1715 ui.warn(
1706 1716 _(
1707 1717 b"%s not added: only files and symlinks "
1708 1718 b"supported currently\n"
1709 1719 )
1710 1720 % uipath(f)
1711 1721 )
1712 1722 rejected.append(f)
1713 1723 elif not ds.set_tracked(f):
1714 1724 ui.warn(_(b"%s already tracked!\n") % uipath(f))
1715 1725 return rejected
1716 1726
1717 1727 def forget(self, files, prefix=b""):
1718 1728 with self._repo.wlock():
1719 1729 ds = self._repo.dirstate
1720 1730 uipath = lambda f: ds.pathto(pathutil.join(prefix, f))
1721 1731 rejected = []
1722 1732 for f in files:
1723 1733 if not ds.set_untracked(f):
1724 1734 self._repo.ui.warn(_(b"%s not tracked!\n") % uipath(f))
1725 1735 rejected.append(f)
1726 1736 return rejected
1727 1737
1728 1738 def copy(self, source, dest):
1729 1739 try:
1730 1740 st = self._repo.wvfs.lstat(dest)
1731 1741 except OSError as err:
1732 1742 if err.errno != errno.ENOENT:
1733 1743 raise
1734 1744 self._repo.ui.warn(
1735 1745 _(b"%s does not exist!\n") % self._repo.dirstate.pathto(dest)
1736 1746 )
1737 1747 return
1738 1748 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1739 1749 self._repo.ui.warn(
1740 1750 _(b"copy failed: %s is not a file or a symbolic link\n")
1741 1751 % self._repo.dirstate.pathto(dest)
1742 1752 )
1743 1753 else:
1744 1754 with self._repo.wlock():
1745 1755 ds = self._repo.dirstate
1746 1756 ds.set_tracked(dest)
1747 1757 ds.copy(source, dest)
1748 1758
1749 1759 def match(
1750 1760 self,
1751 1761 pats=None,
1752 1762 include=None,
1753 1763 exclude=None,
1754 1764 default=b'glob',
1755 1765 listsubrepos=False,
1756 1766 badfn=None,
1757 1767 cwd=None,
1758 1768 ):
1759 1769 r = self._repo
1760 1770 if not cwd:
1761 1771 cwd = r.getcwd()
1762 1772
1763 1773 # Only a case insensitive filesystem needs magic to translate user input
1764 1774 # to actual case in the filesystem.
1765 1775 icasefs = not util.fscasesensitive(r.root)
1766 1776 return matchmod.match(
1767 1777 r.root,
1768 1778 cwd,
1769 1779 pats,
1770 1780 include,
1771 1781 exclude,
1772 1782 default,
1773 1783 auditor=r.auditor,
1774 1784 ctx=self,
1775 1785 listsubrepos=listsubrepos,
1776 1786 badfn=badfn,
1777 1787 icasefs=icasefs,
1778 1788 )
1779 1789
1780 1790 def _filtersuspectsymlink(self, files):
1781 1791 if not files or self._repo.dirstate._checklink:
1782 1792 return files
1783 1793
1784 1794 # Symlink placeholders may get non-symlink-like contents
1785 1795 # via user error or dereferencing by NFS or Samba servers,
1786 1796 # so we filter out any placeholders that don't look like a
1787 1797 # symlink
1788 1798 sane = []
1789 1799 for f in files:
1790 1800 if self.flags(f) == b'l':
1791 1801 d = self[f].data()
1792 1802 if (
1793 1803 d == b''
1794 1804 or len(d) >= 1024
1795 1805 or b'\n' in d
1796 1806 or stringutil.binary(d)
1797 1807 ):
1798 1808 self._repo.ui.debug(
1799 1809 b'ignoring suspect symlink placeholder "%s"\n' % f
1800 1810 )
1801 1811 continue
1802 1812 sane.append(f)
1803 1813 return sane
1804 1814
1805 1815 def _checklookup(self, files, mtime_boundary):
1806 1816 # check for any possibly clean files
1807 1817 if not files:
1808 1818 return [], [], [], []
1809 1819
1810 1820 modified = []
1811 1821 deleted = []
1812 1822 clean = []
1813 1823 fixup = []
1814 1824 pctx = self._parents[0]
1815 1825 # do a full compare of any files that might have changed
1816 1826 for f in sorted(files):
1817 1827 try:
1818 1828 # This will return True for a file that got replaced by a
1819 1829 # directory in the interim, but fixing that is pretty hard.
1820 1830 if (
1821 1831 f not in pctx
1822 1832 or self.flags(f) != pctx.flags(f)
1823 1833 or pctx[f].cmp(self[f])
1824 1834 ):
1825 1835 modified.append(f)
1826 1836 elif mtime_boundary is None:
1827 1837 clean.append(f)
1828 1838 else:
1829 1839 s = self[f].lstat()
1830 1840 mode = s.st_mode
1831 1841 size = s.st_size
1832 1842 file_mtime = timestamp.reliable_mtime_of(s, mtime_boundary)
1833 1843 if file_mtime is not None:
1834 1844 cache_info = (mode, size, file_mtime)
1835 1845 fixup.append((f, cache_info))
1836 1846 else:
1837 1847 clean.append(f)
1838 1848 except (IOError, OSError):
1839 1849 # A file become inaccessible in between? Mark it as deleted,
1840 1850 # matching dirstate behavior (issue5584).
1841 1851 # The dirstate has more complex behavior around whether a
1842 1852 # missing file matches a directory, etc, but we don't need to
1843 1853 # bother with that: if f has made it to this point, we're sure
1844 1854 # it's in the dirstate.
1845 1855 deleted.append(f)
1846 1856
1847 1857 return modified, deleted, clean, fixup
1848 1858
1849 1859 def _poststatusfixup(self, status, fixup):
1850 1860 """update dirstate for files that are actually clean"""
1851 1861 poststatus = self._repo.postdsstatus()
1852 1862 if fixup or poststatus or self._repo.dirstate._dirty:
1853 1863 try:
1854 1864 oldid = self._repo.dirstate.identity()
1855 1865
1856 1866 # updating the dirstate is optional
1857 1867 # so we don't wait on the lock
1858 1868 # wlock can invalidate the dirstate, so cache normal _after_
1859 1869 # taking the lock
1860 1870 with self._repo.wlock(False):
1861 1871 dirstate = self._repo.dirstate
1862 1872 if dirstate.identity() == oldid:
1863 1873 if fixup:
1864 1874 if dirstate.pendingparentchange():
1865 1875 normal = lambda f, pfd: dirstate.update_file(
1866 1876 f, p1_tracked=True, wc_tracked=True
1867 1877 )
1868 1878 else:
1869 1879 normal = dirstate.set_clean
1870 1880 for f, pdf in fixup:
1871 1881 normal(f, pdf)
1872 1882 # write changes out explicitly, because nesting
1873 1883 # wlock at runtime may prevent 'wlock.release()'
1874 1884 # after this block from doing so for subsequent
1875 1885 # changing files
1876 1886 tr = self._repo.currenttransaction()
1877 1887 self._repo.dirstate.write(tr)
1878 1888
1879 1889 if poststatus:
1880 1890 for ps in poststatus:
1881 1891 ps(self, status)
1882 1892 else:
1883 1893 # in this case, writing changes out breaks
1884 1894 # consistency, because .hg/dirstate was
1885 1895 # already changed simultaneously after last
1886 1896 # caching (see also issue5584 for detail)
1887 1897 self._repo.ui.debug(
1888 1898 b'skip updating dirstate: identity mismatch\n'
1889 1899 )
1890 1900 except error.LockError:
1891 1901 pass
1892 1902 finally:
1893 1903 # Even if the wlock couldn't be grabbed, clear out the list.
1894 1904 self._repo.clearpostdsstatus()
1895 1905
1896 1906 def _dirstatestatus(self, match, ignored=False, clean=False, unknown=False):
1897 1907 '''Gets the status from the dirstate -- internal use only.'''
1898 1908 subrepos = []
1899 1909 if b'.hgsub' in self:
1900 1910 subrepos = sorted(self.substate)
1901 1911 cmp, s, mtime_boundary = self._repo.dirstate.status(
1902 1912 match, subrepos, ignored=ignored, clean=clean, unknown=unknown
1903 1913 )
1904 1914
1905 1915 # check for any possibly clean files
1906 1916 fixup = []
1907 1917 if cmp:
1908 1918 modified2, deleted2, clean_set, fixup = self._checklookup(
1909 1919 cmp, mtime_boundary
1910 1920 )
1911 1921 s.modified.extend(modified2)
1912 1922 s.deleted.extend(deleted2)
1913 1923
1914 1924 if clean_set and clean:
1915 1925 s.clean.extend(clean_set)
1916 1926 if fixup and clean:
1917 1927 s.clean.extend((f for f, _ in fixup))
1918 1928
1919 1929 self._poststatusfixup(s, fixup)
1920 1930
1921 1931 if match.always():
1922 1932 # cache for performance
1923 1933 if s.unknown or s.ignored or s.clean:
1924 1934 # "_status" is cached with list*=False in the normal route
1925 1935 self._status = scmutil.status(
1926 1936 s.modified, s.added, s.removed, s.deleted, [], [], []
1927 1937 )
1928 1938 else:
1929 1939 self._status = s
1930 1940
1931 1941 return s
1932 1942
1933 1943 @propertycache
1934 1944 def _copies(self):
1935 1945 p1copies = {}
1936 1946 p2copies = {}
1937 1947 parents = self._repo.dirstate.parents()
1938 1948 p1manifest = self._repo[parents[0]].manifest()
1939 1949 p2manifest = self._repo[parents[1]].manifest()
1940 1950 changedset = set(self.added()) | set(self.modified())
1941 1951 narrowmatch = self._repo.narrowmatch()
1942 1952 for dst, src in self._repo.dirstate.copies().items():
1943 1953 if dst not in changedset or not narrowmatch(dst):
1944 1954 continue
1945 1955 if src in p1manifest:
1946 1956 p1copies[dst] = src
1947 1957 elif src in p2manifest:
1948 1958 p2copies[dst] = src
1949 1959 return p1copies, p2copies
1950 1960
1951 1961 @propertycache
1952 1962 def _manifest(self):
1953 1963 """generate a manifest corresponding to the values in self._status
1954 1964
1955 1965 This reuse the file nodeid from parent, but we use special node
1956 1966 identifiers for added and modified files. This is used by manifests
1957 1967 merge to see that files are different and by update logic to avoid
1958 1968 deleting newly added files.
1959 1969 """
1960 1970 return self._buildstatusmanifest(self._status)
1961 1971
1962 1972 def _buildstatusmanifest(self, status):
1963 1973 """Builds a manifest that includes the given status results."""
1964 1974 parents = self.parents()
1965 1975
1966 1976 man = parents[0].manifest().copy()
1967 1977
1968 1978 ff = self._flagfunc
1969 1979 for i, l in (
1970 1980 (self._repo.nodeconstants.addednodeid, status.added),
1971 1981 (self._repo.nodeconstants.modifiednodeid, status.modified),
1972 1982 ):
1973 1983 for f in l:
1974 1984 man[f] = i
1975 1985 try:
1976 1986 man.setflag(f, ff(f))
1977 1987 except OSError:
1978 1988 pass
1979 1989
1980 1990 for f in status.deleted + status.removed:
1981 1991 if f in man:
1982 1992 del man[f]
1983 1993
1984 1994 return man
1985 1995
1986 1996 def _buildstatus(
1987 1997 self, other, s, match, listignored, listclean, listunknown
1988 1998 ):
1989 1999 """build a status with respect to another context
1990 2000
1991 2001 This includes logic for maintaining the fast path of status when
1992 2002 comparing the working directory against its parent, which is to skip
1993 2003 building a new manifest if self (working directory) is not comparing
1994 2004 against its parent (repo['.']).
1995 2005 """
1996 2006 s = self._dirstatestatus(match, listignored, listclean, listunknown)
1997 2007 # Filter out symlinks that, in the case of FAT32 and NTFS filesystems,
1998 2008 # might have accidentally ended up with the entire contents of the file
1999 2009 # they are supposed to be linking to.
2000 2010 s.modified[:] = self._filtersuspectsymlink(s.modified)
2001 2011 if other != self._repo[b'.']:
2002 2012 s = super(workingctx, self)._buildstatus(
2003 2013 other, s, match, listignored, listclean, listunknown
2004 2014 )
2005 2015 return s
2006 2016
2007 2017 def _matchstatus(self, other, match):
2008 2018 """override the match method with a filter for directory patterns
2009 2019
2010 2020 We use inheritance to customize the match.bad method only in cases of
2011 2021 workingctx since it belongs only to the working directory when
2012 2022 comparing against the parent changeset.
2013 2023
2014 2024 If we aren't comparing against the working directory's parent, then we
2015 2025 just use the default match object sent to us.
2016 2026 """
2017 2027 if other != self._repo[b'.']:
2018 2028
2019 2029 def bad(f, msg):
2020 2030 # 'f' may be a directory pattern from 'match.files()',
2021 2031 # so 'f not in ctx1' is not enough
2022 2032 if f not in other and not other.hasdir(f):
2023 2033 self._repo.ui.warn(
2024 2034 b'%s: %s\n' % (self._repo.dirstate.pathto(f), msg)
2025 2035 )
2026 2036
2027 2037 match.bad = bad
2028 2038 return match
2029 2039
2030 2040 def walk(self, match):
2031 2041 '''Generates matching file names.'''
2032 2042 return sorted(
2033 2043 self._repo.dirstate.walk(
2034 2044 self._repo.narrowmatch(match),
2035 2045 subrepos=sorted(self.substate),
2036 2046 unknown=True,
2037 2047 ignored=False,
2038 2048 )
2039 2049 )
2040 2050
2041 2051 def matches(self, match):
2042 2052 match = self._repo.narrowmatch(match)
2043 2053 ds = self._repo.dirstate
2044 2054 return sorted(f for f in ds.matches(match) if ds.get_entry(f).tracked)
2045 2055
2046 2056 def markcommitted(self, node):
2047 2057 with self._repo.dirstate.parentchange():
2048 2058 for f in self.modified() + self.added():
2049 2059 self._repo.dirstate.update_file(
2050 2060 f, p1_tracked=True, wc_tracked=True
2051 2061 )
2052 2062 for f in self.removed():
2053 2063 self._repo.dirstate.update_file(
2054 2064 f, p1_tracked=False, wc_tracked=False
2055 2065 )
2056 2066 self._repo.dirstate.setparents(node)
2057 2067 self._repo._quick_access_changeid_invalidate()
2058 2068
2059 2069 sparse.aftercommit(self._repo, node)
2060 2070
2061 2071 # write changes out explicitly, because nesting wlock at
2062 2072 # runtime may prevent 'wlock.release()' in 'repo.commit()'
2063 2073 # from immediately doing so for subsequent changing files
2064 2074 self._repo.dirstate.write(self._repo.currenttransaction())
2065 2075
2066 2076 def mergestate(self, clean=False):
2067 2077 if clean:
2068 2078 return mergestatemod.mergestate.clean(self._repo)
2069 2079 return mergestatemod.mergestate.read(self._repo)
2070 2080
2071 2081
2072 2082 class committablefilectx(basefilectx):
2073 2083 """A committablefilectx provides common functionality for a file context
2074 2084 that wants the ability to commit, e.g. workingfilectx or memfilectx."""
2075 2085
2076 2086 def __init__(self, repo, path, filelog=None, ctx=None):
2077 2087 self._repo = repo
2078 2088 self._path = path
2079 2089 self._changeid = None
2080 2090 self._filerev = self._filenode = None
2081 2091
2082 2092 if filelog is not None:
2083 2093 self._filelog = filelog
2084 2094 if ctx:
2085 2095 self._changectx = ctx
2086 2096
2087 2097 def __nonzero__(self):
2088 2098 return True
2089 2099
2090 2100 __bool__ = __nonzero__
2091 2101
2092 2102 def linkrev(self):
2093 2103 # linked to self._changectx no matter if file is modified or not
2094 2104 return self.rev()
2095 2105
2096 2106 def renamed(self):
2097 2107 path = self.copysource()
2098 2108 if not path:
2099 2109 return None
2100 2110 return (
2101 2111 path,
2102 2112 self._changectx._parents[0]._manifest.get(
2103 2113 path, self._repo.nodeconstants.nullid
2104 2114 ),
2105 2115 )
2106 2116
2107 2117 def parents(self):
2108 2118 '''return parent filectxs, following copies if necessary'''
2109 2119
2110 2120 def filenode(ctx, path):
2111 2121 return ctx._manifest.get(path, self._repo.nodeconstants.nullid)
2112 2122
2113 2123 path = self._path
2114 2124 fl = self._filelog
2115 2125 pcl = self._changectx._parents
2116 2126 renamed = self.renamed()
2117 2127
2118 2128 if renamed:
2119 2129 pl = [renamed + (None,)]
2120 2130 else:
2121 2131 pl = [(path, filenode(pcl[0], path), fl)]
2122 2132
2123 2133 for pc in pcl[1:]:
2124 2134 pl.append((path, filenode(pc, path), fl))
2125 2135
2126 2136 return [
2127 2137 self._parentfilectx(p, fileid=n, filelog=l)
2128 2138 for p, n, l in pl
2129 2139 if n != self._repo.nodeconstants.nullid
2130 2140 ]
2131 2141
2132 2142 def children(self):
2133 2143 return []
2134 2144
2135 2145
2136 2146 class workingfilectx(committablefilectx):
2137 2147 """A workingfilectx object makes access to data related to a particular
2138 2148 file in the working directory convenient."""
2139 2149
2140 2150 def __init__(self, repo, path, filelog=None, workingctx=None):
2141 2151 super(workingfilectx, self).__init__(repo, path, filelog, workingctx)
2142 2152
2143 2153 @propertycache
2144 2154 def _changectx(self):
2145 2155 return workingctx(self._repo)
2146 2156
2147 2157 def data(self):
2148 2158 return self._repo.wread(self._path)
2149 2159
2150 2160 def copysource(self):
2151 2161 return self._repo.dirstate.copied(self._path)
2152 2162
2153 2163 def size(self):
2154 2164 return self._repo.wvfs.lstat(self._path).st_size
2155 2165
2156 2166 def lstat(self):
2157 2167 return self._repo.wvfs.lstat(self._path)
2158 2168
2159 2169 def date(self):
2160 2170 t, tz = self._changectx.date()
2161 2171 try:
2162 2172 return (self._repo.wvfs.lstat(self._path)[stat.ST_MTIME], tz)
2163 2173 except OSError as err:
2164 2174 if err.errno != errno.ENOENT:
2165 2175 raise
2166 2176 return (t, tz)
2167 2177
2168 2178 def exists(self):
2169 2179 return self._repo.wvfs.exists(self._path)
2170 2180
2171 2181 def lexists(self):
2172 2182 return self._repo.wvfs.lexists(self._path)
2173 2183
2174 2184 def audit(self):
2175 2185 return self._repo.wvfs.audit(self._path)
2176 2186
2177 2187 def cmp(self, fctx):
2178 2188 """compare with other file context
2179 2189
2180 2190 returns True if different than fctx.
2181 2191 """
2182 2192 # fctx should be a filectx (not a workingfilectx)
2183 2193 # invert comparison to reuse the same code path
2184 2194 return fctx.cmp(self)
2185 2195
2186 2196 def remove(self, ignoremissing=False):
2187 2197 """wraps unlink for a repo's working directory"""
2188 2198 rmdir = self._repo.ui.configbool(b'experimental', b'removeemptydirs')
2189 2199 self._repo.wvfs.unlinkpath(
2190 2200 self._path, ignoremissing=ignoremissing, rmdir=rmdir
2191 2201 )
2192 2202
2193 2203 def write(self, data, flags, backgroundclose=False, **kwargs):
2194 2204 """wraps repo.wwrite"""
2195 2205 return self._repo.wwrite(
2196 2206 self._path, data, flags, backgroundclose=backgroundclose, **kwargs
2197 2207 )
2198 2208
2199 2209 def markcopied(self, src):
2200 2210 """marks this file a copy of `src`"""
2201 2211 self._repo.dirstate.copy(src, self._path)
2202 2212
2203 2213 def clearunknown(self):
2204 2214 """Removes conflicting items in the working directory so that
2205 2215 ``write()`` can be called successfully.
2206 2216 """
2207 2217 wvfs = self._repo.wvfs
2208 2218 f = self._path
2209 2219 wvfs.audit(f)
2210 2220 if self._repo.ui.configbool(
2211 2221 b'experimental', b'merge.checkpathconflicts'
2212 2222 ):
2213 2223 # remove files under the directory as they should already be
2214 2224 # warned and backed up
2215 2225 if wvfs.isdir(f) and not wvfs.islink(f):
2216 2226 wvfs.rmtree(f, forcibly=True)
2217 2227 for p in reversed(list(pathutil.finddirs(f))):
2218 2228 if wvfs.isfileorlink(p):
2219 2229 wvfs.unlink(p)
2220 2230 break
2221 2231 else:
2222 2232 # don't remove files if path conflicts are not processed
2223 2233 if wvfs.isdir(f) and not wvfs.islink(f):
2224 2234 wvfs.removedirs(f)
2225 2235
2226 2236 def setflags(self, l, x):
2227 2237 self._repo.wvfs.setflags(self._path, l, x)
2228 2238
2229 2239
2230 2240 class overlayworkingctx(committablectx):
2231 2241 """Wraps another mutable context with a write-back cache that can be
2232 2242 converted into a commit context.
2233 2243
2234 2244 self._cache[path] maps to a dict with keys: {
2235 2245 'exists': bool?
2236 2246 'date': date?
2237 2247 'data': str?
2238 2248 'flags': str?
2239 2249 'copied': str? (path or None)
2240 2250 }
2241 2251 If `exists` is True, `flags` must be non-None and 'date' is non-None. If it
2242 2252 is `False`, the file was deleted.
2243 2253 """
2244 2254
2245 2255 def __init__(self, repo):
2246 2256 super(overlayworkingctx, self).__init__(repo)
2247 2257 self.clean()
2248 2258
2249 2259 def setbase(self, wrappedctx):
2250 2260 self._wrappedctx = wrappedctx
2251 2261 self._parents = [wrappedctx]
2252 2262 # Drop old manifest cache as it is now out of date.
2253 2263 # This is necessary when, e.g., rebasing several nodes with one
2254 2264 # ``overlayworkingctx`` (e.g. with --collapse).
2255 2265 util.clearcachedproperty(self, b'_manifest')
2256 2266
2257 2267 def setparents(self, p1node, p2node=None):
2258 2268 if p2node is None:
2259 2269 p2node = self._repo.nodeconstants.nullid
2260 2270 assert p1node == self._wrappedctx.node()
2261 2271 self._parents = [self._wrappedctx, self._repo.unfiltered()[p2node]]
2262 2272
2263 2273 def data(self, path):
2264 2274 if self.isdirty(path):
2265 2275 if self._cache[path][b'exists']:
2266 2276 if self._cache[path][b'data'] is not None:
2267 2277 return self._cache[path][b'data']
2268 2278 else:
2269 2279 # Must fallback here, too, because we only set flags.
2270 2280 return self._wrappedctx[path].data()
2271 2281 else:
2272 2282 raise error.ProgrammingError(
2273 2283 b"No such file or directory: %s" % path
2274 2284 )
2275 2285 else:
2276 2286 return self._wrappedctx[path].data()
2277 2287
2278 2288 @propertycache
2279 2289 def _manifest(self):
2280 2290 parents = self.parents()
2281 2291 man = parents[0].manifest().copy()
2282 2292
2283 2293 flag = self._flagfunc
2284 2294 for path in self.added():
2285 2295 man[path] = self._repo.nodeconstants.addednodeid
2286 2296 man.setflag(path, flag(path))
2287 2297 for path in self.modified():
2288 2298 man[path] = self._repo.nodeconstants.modifiednodeid
2289 2299 man.setflag(path, flag(path))
2290 2300 for path in self.removed():
2291 2301 del man[path]
2292 2302 return man
2293 2303
2294 2304 @propertycache
2295 2305 def _flagfunc(self):
2296 2306 def f(path):
2297 2307 return self._cache[path][b'flags']
2298 2308
2299 2309 return f
2300 2310
2301 2311 def files(self):
2302 2312 return sorted(self.added() + self.modified() + self.removed())
2303 2313
2304 2314 def modified(self):
2305 2315 return [
2306 2316 f
2307 2317 for f in self._cache.keys()
2308 2318 if self._cache[f][b'exists'] and self._existsinparent(f)
2309 2319 ]
2310 2320
2311 2321 def added(self):
2312 2322 return [
2313 2323 f
2314 2324 for f in self._cache.keys()
2315 2325 if self._cache[f][b'exists'] and not self._existsinparent(f)
2316 2326 ]
2317 2327
2318 2328 def removed(self):
2319 2329 return [
2320 2330 f
2321 2331 for f in self._cache.keys()
2322 2332 if not self._cache[f][b'exists'] and self._existsinparent(f)
2323 2333 ]
2324 2334
2325 2335 def p1copies(self):
2326 2336 copies = {}
2327 2337 narrowmatch = self._repo.narrowmatch()
2328 2338 for f in self._cache.keys():
2329 2339 if not narrowmatch(f):
2330 2340 continue
2331 2341 copies.pop(f, None) # delete if it exists
2332 2342 source = self._cache[f][b'copied']
2333 2343 if source:
2334 2344 copies[f] = source
2335 2345 return copies
2336 2346
2337 2347 def p2copies(self):
2338 2348 copies = {}
2339 2349 narrowmatch = self._repo.narrowmatch()
2340 2350 for f in self._cache.keys():
2341 2351 if not narrowmatch(f):
2342 2352 continue
2343 2353 copies.pop(f, None) # delete if it exists
2344 2354 source = self._cache[f][b'copied']
2345 2355 if source:
2346 2356 copies[f] = source
2347 2357 return copies
2348 2358
2349 2359 def isinmemory(self):
2350 2360 return True
2351 2361
2352 2362 def filedate(self, path):
2353 2363 if self.isdirty(path):
2354 2364 return self._cache[path][b'date']
2355 2365 else:
2356 2366 return self._wrappedctx[path].date()
2357 2367
2358 2368 def markcopied(self, path, origin):
2359 2369 self._markdirty(
2360 2370 path,
2361 2371 exists=True,
2362 2372 date=self.filedate(path),
2363 2373 flags=self.flags(path),
2364 2374 copied=origin,
2365 2375 )
2366 2376
2367 2377 def copydata(self, path):
2368 2378 if self.isdirty(path):
2369 2379 return self._cache[path][b'copied']
2370 2380 else:
2371 2381 return None
2372 2382
2373 2383 def flags(self, path):
2374 2384 if self.isdirty(path):
2375 2385 if self._cache[path][b'exists']:
2376 2386 return self._cache[path][b'flags']
2377 2387 else:
2378 2388 raise error.ProgrammingError(
2379 2389 b"No such file or directory: %s" % path
2380 2390 )
2381 2391 else:
2382 2392 return self._wrappedctx[path].flags()
2383 2393
2384 2394 def __contains__(self, key):
2385 2395 if key in self._cache:
2386 2396 return self._cache[key][b'exists']
2387 2397 return key in self.p1()
2388 2398
2389 2399 def _existsinparent(self, path):
2390 2400 try:
2391 2401 # ``commitctx` raises a ``ManifestLookupError`` if a path does not
2392 2402 # exist, unlike ``workingctx``, which returns a ``workingfilectx``
2393 2403 # with an ``exists()`` function.
2394 2404 self._wrappedctx[path]
2395 2405 return True
2396 2406 except error.ManifestLookupError:
2397 2407 return False
2398 2408
2399 2409 def _auditconflicts(self, path):
2400 2410 """Replicates conflict checks done by wvfs.write().
2401 2411
2402 2412 Since we never write to the filesystem and never call `applyupdates` in
2403 2413 IMM, we'll never check that a path is actually writable -- e.g., because
2404 2414 it adds `a/foo`, but `a` is actually a file in the other commit.
2405 2415 """
2406 2416
2407 2417 def fail(path, component):
2408 2418 # p1() is the base and we're receiving "writes" for p2()'s
2409 2419 # files.
2410 2420 if b'l' in self.p1()[component].flags():
2411 2421 raise error.Abort(
2412 2422 b"error: %s conflicts with symlink %s "
2413 2423 b"in %d." % (path, component, self.p1().rev())
2414 2424 )
2415 2425 else:
2416 2426 raise error.Abort(
2417 2427 b"error: '%s' conflicts with file '%s' in "
2418 2428 b"%d." % (path, component, self.p1().rev())
2419 2429 )
2420 2430
2421 2431 # Test that each new directory to be created to write this path from p2
2422 2432 # is not a file in p1.
2423 2433 components = path.split(b'/')
2424 2434 for i in pycompat.xrange(len(components)):
2425 2435 component = b"/".join(components[0:i])
2426 2436 if component in self:
2427 2437 fail(path, component)
2428 2438
2429 2439 # Test the other direction -- that this path from p2 isn't a directory
2430 2440 # in p1 (test that p1 doesn't have any paths matching `path/*`).
2431 2441 match = self.match([path], default=b'path')
2432 2442 mfiles = list(self.p1().manifest().walk(match))
2433 2443 if len(mfiles) > 0:
2434 2444 if len(mfiles) == 1 and mfiles[0] == path:
2435 2445 return
2436 2446 # omit the files which are deleted in current IMM wctx
2437 2447 mfiles = [m for m in mfiles if m in self]
2438 2448 if not mfiles:
2439 2449 return
2440 2450 raise error.Abort(
2441 2451 b"error: file '%s' cannot be written because "
2442 2452 b" '%s/' is a directory in %s (containing %d "
2443 2453 b"entries: %s)"
2444 2454 % (path, path, self.p1(), len(mfiles), b', '.join(mfiles))
2445 2455 )
2446 2456
2447 2457 def write(self, path, data, flags=b'', **kwargs):
2448 2458 if data is None:
2449 2459 raise error.ProgrammingError(b"data must be non-None")
2450 2460 self._auditconflicts(path)
2451 2461 self._markdirty(
2452 2462 path, exists=True, data=data, date=dateutil.makedate(), flags=flags
2453 2463 )
2454 2464
2455 2465 def setflags(self, path, l, x):
2456 2466 flag = b''
2457 2467 if l:
2458 2468 flag = b'l'
2459 2469 elif x:
2460 2470 flag = b'x'
2461 2471 self._markdirty(path, exists=True, date=dateutil.makedate(), flags=flag)
2462 2472
2463 2473 def remove(self, path):
2464 2474 self._markdirty(path, exists=False)
2465 2475
2466 2476 def exists(self, path):
2467 2477 """exists behaves like `lexists`, but needs to follow symlinks and
2468 2478 return False if they are broken.
2469 2479 """
2470 2480 if self.isdirty(path):
2471 2481 # If this path exists and is a symlink, "follow" it by calling
2472 2482 # exists on the destination path.
2473 2483 if (
2474 2484 self._cache[path][b'exists']
2475 2485 and b'l' in self._cache[path][b'flags']
2476 2486 ):
2477 2487 return self.exists(self._cache[path][b'data'].strip())
2478 2488 else:
2479 2489 return self._cache[path][b'exists']
2480 2490
2481 2491 return self._existsinparent(path)
2482 2492
2483 2493 def lexists(self, path):
2484 2494 """lexists returns True if the path exists"""
2485 2495 if self.isdirty(path):
2486 2496 return self._cache[path][b'exists']
2487 2497
2488 2498 return self._existsinparent(path)
2489 2499
2490 2500 def size(self, path):
2491 2501 if self.isdirty(path):
2492 2502 if self._cache[path][b'exists']:
2493 2503 return len(self._cache[path][b'data'])
2494 2504 else:
2495 2505 raise error.ProgrammingError(
2496 2506 b"No such file or directory: %s" % path
2497 2507 )
2498 2508 return self._wrappedctx[path].size()
2499 2509
2500 2510 def tomemctx(
2501 2511 self,
2502 2512 text,
2503 2513 branch=None,
2504 2514 extra=None,
2505 2515 date=None,
2506 2516 parents=None,
2507 2517 user=None,
2508 2518 editor=None,
2509 2519 ):
2510 2520 """Converts this ``overlayworkingctx`` into a ``memctx`` ready to be
2511 2521 committed.
2512 2522
2513 2523 ``text`` is the commit message.
2514 2524 ``parents`` (optional) are rev numbers.
2515 2525 """
2516 2526 # Default parents to the wrapped context if not passed.
2517 2527 if parents is None:
2518 2528 parents = self.parents()
2519 2529 if len(parents) == 1:
2520 2530 parents = (parents[0], None)
2521 2531
2522 2532 # ``parents`` is passed as rev numbers; convert to ``commitctxs``.
2523 2533 if parents[1] is None:
2524 2534 parents = (self._repo[parents[0]], None)
2525 2535 else:
2526 2536 parents = (self._repo[parents[0]], self._repo[parents[1]])
2527 2537
2528 2538 files = self.files()
2529 2539
2530 2540 def getfile(repo, memctx, path):
2531 2541 if self._cache[path][b'exists']:
2532 2542 return memfilectx(
2533 2543 repo,
2534 2544 memctx,
2535 2545 path,
2536 2546 self._cache[path][b'data'],
2537 2547 b'l' in self._cache[path][b'flags'],
2538 2548 b'x' in self._cache[path][b'flags'],
2539 2549 self._cache[path][b'copied'],
2540 2550 )
2541 2551 else:
2542 2552 # Returning None, but including the path in `files`, is
2543 2553 # necessary for memctx to register a deletion.
2544 2554 return None
2545 2555
2546 2556 if branch is None:
2547 2557 branch = self._wrappedctx.branch()
2548 2558
2549 2559 return memctx(
2550 2560 self._repo,
2551 2561 parents,
2552 2562 text,
2553 2563 files,
2554 2564 getfile,
2555 2565 date=date,
2556 2566 extra=extra,
2557 2567 user=user,
2558 2568 branch=branch,
2559 2569 editor=editor,
2560 2570 )
2561 2571
2562 2572 def tomemctx_for_amend(self, precursor):
2563 2573 extra = precursor.extra().copy()
2564 2574 extra[b'amend_source'] = precursor.hex()
2565 2575 return self.tomemctx(
2566 2576 text=precursor.description(),
2567 2577 branch=precursor.branch(),
2568 2578 extra=extra,
2569 2579 date=precursor.date(),
2570 2580 user=precursor.user(),
2571 2581 )
2572 2582
2573 2583 def isdirty(self, path):
2574 2584 return path in self._cache
2575 2585
2576 2586 def clean(self):
2577 2587 self._mergestate = None
2578 2588 self._cache = {}
2579 2589
2580 2590 def _compact(self):
2581 2591 """Removes keys from the cache that are actually clean, by comparing
2582 2592 them with the underlying context.
2583 2593
2584 2594 This can occur during the merge process, e.g. by passing --tool :local
2585 2595 to resolve a conflict.
2586 2596 """
2587 2597 keys = []
2588 2598 # This won't be perfect, but can help performance significantly when
2589 2599 # using things like remotefilelog.
2590 2600 scmutil.prefetchfiles(
2591 2601 self.repo(),
2592 2602 [
2593 2603 (
2594 2604 self.p1().rev(),
2595 2605 scmutil.matchfiles(self.repo(), self._cache.keys()),
2596 2606 )
2597 2607 ],
2598 2608 )
2599 2609
2600 2610 for path in self._cache.keys():
2601 2611 cache = self._cache[path]
2602 2612 try:
2603 2613 underlying = self._wrappedctx[path]
2604 2614 if (
2605 2615 underlying.data() == cache[b'data']
2606 2616 and underlying.flags() == cache[b'flags']
2607 2617 ):
2608 2618 keys.append(path)
2609 2619 except error.ManifestLookupError:
2610 2620 # Path not in the underlying manifest (created).
2611 2621 continue
2612 2622
2613 2623 for path in keys:
2614 2624 del self._cache[path]
2615 2625 return keys
2616 2626
2617 2627 def _markdirty(
2618 2628 self, path, exists, data=None, date=None, flags=b'', copied=None
2619 2629 ):
2620 2630 # data not provided, let's see if we already have some; if not, let's
2621 2631 # grab it from our underlying context, so that we always have data if
2622 2632 # the file is marked as existing.
2623 2633 if exists and data is None:
2624 2634 oldentry = self._cache.get(path) or {}
2625 2635 data = oldentry.get(b'data')
2626 2636 if data is None:
2627 2637 data = self._wrappedctx[path].data()
2628 2638
2629 2639 self._cache[path] = {
2630 2640 b'exists': exists,
2631 2641 b'data': data,
2632 2642 b'date': date,
2633 2643 b'flags': flags,
2634 2644 b'copied': copied,
2635 2645 }
2636 2646 util.clearcachedproperty(self, b'_manifest')
2637 2647
2638 2648 def filectx(self, path, filelog=None):
2639 2649 return overlayworkingfilectx(
2640 2650 self._repo, path, parent=self, filelog=filelog
2641 2651 )
2642 2652
2643 2653 def mergestate(self, clean=False):
2644 2654 if clean or self._mergestate is None:
2645 2655 self._mergestate = mergestatemod.memmergestate(self._repo)
2646 2656 return self._mergestate
2647 2657
2648 2658
2649 2659 class overlayworkingfilectx(committablefilectx):
2650 2660 """Wrap a ``workingfilectx`` but intercepts all writes into an in-memory
2651 2661 cache, which can be flushed through later by calling ``flush()``."""
2652 2662
2653 2663 def __init__(self, repo, path, filelog=None, parent=None):
2654 2664 super(overlayworkingfilectx, self).__init__(repo, path, filelog, parent)
2655 2665 self._repo = repo
2656 2666 self._parent = parent
2657 2667 self._path = path
2658 2668
2659 2669 def cmp(self, fctx):
2660 2670 return self.data() != fctx.data()
2661 2671
2662 2672 def changectx(self):
2663 2673 return self._parent
2664 2674
2665 2675 def data(self):
2666 2676 return self._parent.data(self._path)
2667 2677
2668 2678 def date(self):
2669 2679 return self._parent.filedate(self._path)
2670 2680
2671 2681 def exists(self):
2672 2682 return self.lexists()
2673 2683
2674 2684 def lexists(self):
2675 2685 return self._parent.exists(self._path)
2676 2686
2677 2687 def copysource(self):
2678 2688 return self._parent.copydata(self._path)
2679 2689
2680 2690 def size(self):
2681 2691 return self._parent.size(self._path)
2682 2692
2683 2693 def markcopied(self, origin):
2684 2694 self._parent.markcopied(self._path, origin)
2685 2695
2686 2696 def audit(self):
2687 2697 pass
2688 2698
2689 2699 def flags(self):
2690 2700 return self._parent.flags(self._path)
2691 2701
2692 2702 def setflags(self, islink, isexec):
2693 2703 return self._parent.setflags(self._path, islink, isexec)
2694 2704
2695 2705 def write(self, data, flags, backgroundclose=False, **kwargs):
2696 2706 return self._parent.write(self._path, data, flags, **kwargs)
2697 2707
2698 2708 def remove(self, ignoremissing=False):
2699 2709 return self._parent.remove(self._path)
2700 2710
2701 2711 def clearunknown(self):
2702 2712 pass
2703 2713
2704 2714
2705 2715 class workingcommitctx(workingctx):
2706 2716 """A workingcommitctx object makes access to data related to
2707 2717 the revision being committed convenient.
2708 2718
2709 2719 This hides changes in the working directory, if they aren't
2710 2720 committed in this context.
2711 2721 """
2712 2722
2713 2723 def __init__(
2714 2724 self, repo, changes, text=b"", user=None, date=None, extra=None
2715 2725 ):
2716 2726 super(workingcommitctx, self).__init__(
2717 2727 repo, text, user, date, extra, changes
2718 2728 )
2719 2729
2720 2730 def _dirstatestatus(self, match, ignored=False, clean=False, unknown=False):
2721 2731 """Return matched files only in ``self._status``
2722 2732
2723 2733 Uncommitted files appear "clean" via this context, even if
2724 2734 they aren't actually so in the working directory.
2725 2735 """
2726 2736 if clean:
2727 2737 clean = [f for f in self._manifest if f not in self._changedset]
2728 2738 else:
2729 2739 clean = []
2730 2740 return scmutil.status(
2731 2741 [f for f in self._status.modified if match(f)],
2732 2742 [f for f in self._status.added if match(f)],
2733 2743 [f for f in self._status.removed if match(f)],
2734 2744 [],
2735 2745 [],
2736 2746 [],
2737 2747 clean,
2738 2748 )
2739 2749
2740 2750 @propertycache
2741 2751 def _changedset(self):
2742 2752 """Return the set of files changed in this context"""
2743 2753 changed = set(self._status.modified)
2744 2754 changed.update(self._status.added)
2745 2755 changed.update(self._status.removed)
2746 2756 return changed
2747 2757
2748 2758
2749 2759 def makecachingfilectxfn(func):
2750 2760 """Create a filectxfn that caches based on the path.
2751 2761
2752 2762 We can't use util.cachefunc because it uses all arguments as the cache
2753 2763 key and this creates a cycle since the arguments include the repo and
2754 2764 memctx.
2755 2765 """
2756 2766 cache = {}
2757 2767
2758 2768 def getfilectx(repo, memctx, path):
2759 2769 if path not in cache:
2760 2770 cache[path] = func(repo, memctx, path)
2761 2771 return cache[path]
2762 2772
2763 2773 return getfilectx
2764 2774
2765 2775
2766 2776 def memfilefromctx(ctx):
2767 2777 """Given a context return a memfilectx for ctx[path]
2768 2778
2769 2779 This is a convenience method for building a memctx based on another
2770 2780 context.
2771 2781 """
2772 2782
2773 2783 def getfilectx(repo, memctx, path):
2774 2784 fctx = ctx[path]
2775 2785 copysource = fctx.copysource()
2776 2786 return memfilectx(
2777 2787 repo,
2778 2788 memctx,
2779 2789 path,
2780 2790 fctx.data(),
2781 2791 islink=fctx.islink(),
2782 2792 isexec=fctx.isexec(),
2783 2793 copysource=copysource,
2784 2794 )
2785 2795
2786 2796 return getfilectx
2787 2797
2788 2798
2789 2799 def memfilefrompatch(patchstore):
2790 2800 """Given a patch (e.g. patchstore object) return a memfilectx
2791 2801
2792 2802 This is a convenience method for building a memctx based on a patchstore.
2793 2803 """
2794 2804
2795 2805 def getfilectx(repo, memctx, path):
2796 2806 data, mode, copysource = patchstore.getfile(path)
2797 2807 if data is None:
2798 2808 return None
2799 2809 islink, isexec = mode
2800 2810 return memfilectx(
2801 2811 repo,
2802 2812 memctx,
2803 2813 path,
2804 2814 data,
2805 2815 islink=islink,
2806 2816 isexec=isexec,
2807 2817 copysource=copysource,
2808 2818 )
2809 2819
2810 2820 return getfilectx
2811 2821
2812 2822
2813 2823 class memctx(committablectx):
2814 2824 """Use memctx to perform in-memory commits via localrepo.commitctx().
2815 2825
2816 2826 Revision information is supplied at initialization time while
2817 2827 related files data and is made available through a callback
2818 2828 mechanism. 'repo' is the current localrepo, 'parents' is a
2819 2829 sequence of two parent revisions identifiers (pass None for every
2820 2830 missing parent), 'text' is the commit message and 'files' lists
2821 2831 names of files touched by the revision (normalized and relative to
2822 2832 repository root).
2823 2833
2824 2834 filectxfn(repo, memctx, path) is a callable receiving the
2825 2835 repository, the current memctx object and the normalized path of
2826 2836 requested file, relative to repository root. It is fired by the
2827 2837 commit function for every file in 'files', but calls order is
2828 2838 undefined. If the file is available in the revision being
2829 2839 committed (updated or added), filectxfn returns a memfilectx
2830 2840 object. If the file was removed, filectxfn return None for recent
2831 2841 Mercurial. Moved files are represented by marking the source file
2832 2842 removed and the new file added with copy information (see
2833 2843 memfilectx).
2834 2844
2835 2845 user receives the committer name and defaults to current
2836 2846 repository username, date is the commit date in any format
2837 2847 supported by dateutil.parsedate() and defaults to current date, extra
2838 2848 is a dictionary of metadata or is left empty.
2839 2849 """
2840 2850
2841 2851 # Mercurial <= 3.1 expects the filectxfn to raise IOError for missing files.
2842 2852 # Extensions that need to retain compatibility across Mercurial 3.1 can use
2843 2853 # this field to determine what to do in filectxfn.
2844 2854 _returnnoneformissingfiles = True
2845 2855
2846 2856 def __init__(
2847 2857 self,
2848 2858 repo,
2849 2859 parents,
2850 2860 text,
2851 2861 files,
2852 2862 filectxfn,
2853 2863 user=None,
2854 2864 date=None,
2855 2865 extra=None,
2856 2866 branch=None,
2857 2867 editor=None,
2858 2868 ):
2859 2869 super(memctx, self).__init__(
2860 2870 repo, text, user, date, extra, branch=branch
2861 2871 )
2862 2872 self._rev = None
2863 2873 self._node = None
2864 2874 parents = [(p or self._repo.nodeconstants.nullid) for p in parents]
2865 2875 p1, p2 = parents
2866 2876 self._parents = [self._repo[p] for p in (p1, p2)]
2867 2877 files = sorted(set(files))
2868 2878 self._files = files
2869 2879 self.substate = {}
2870 2880
2871 2881 if isinstance(filectxfn, patch.filestore):
2872 2882 filectxfn = memfilefrompatch(filectxfn)
2873 2883 elif not callable(filectxfn):
2874 2884 # if store is not callable, wrap it in a function
2875 2885 filectxfn = memfilefromctx(filectxfn)
2876 2886
2877 2887 # memoizing increases performance for e.g. vcs convert scenarios.
2878 2888 self._filectxfn = makecachingfilectxfn(filectxfn)
2879 2889
2880 2890 if editor:
2881 2891 self._text = editor(self._repo, self, [])
2882 2892 self._repo.savecommitmessage(self._text)
2883 2893
2884 2894 def filectx(self, path, filelog=None):
2885 2895 """get a file context from the working directory
2886 2896
2887 2897 Returns None if file doesn't exist and should be removed."""
2888 2898 return self._filectxfn(self._repo, self, path)
2889 2899
2890 2900 def commit(self):
2891 2901 """commit context to the repo"""
2892 2902 return self._repo.commitctx(self)
2893 2903
2894 2904 @propertycache
2895 2905 def _manifest(self):
2896 2906 """generate a manifest based on the return values of filectxfn"""
2897 2907
2898 2908 # keep this simple for now; just worry about p1
2899 2909 pctx = self._parents[0]
2900 2910 man = pctx.manifest().copy()
2901 2911
2902 2912 for f in self._status.modified:
2903 2913 man[f] = self._repo.nodeconstants.modifiednodeid
2904 2914
2905 2915 for f in self._status.added:
2906 2916 man[f] = self._repo.nodeconstants.addednodeid
2907 2917
2908 2918 for f in self._status.removed:
2909 2919 if f in man:
2910 2920 del man[f]
2911 2921
2912 2922 return man
2913 2923
2914 2924 @propertycache
2915 2925 def _status(self):
2916 2926 """Calculate exact status from ``files`` specified at construction"""
2917 2927 man1 = self.p1().manifest()
2918 2928 p2 = self._parents[1]
2919 2929 # "1 < len(self._parents)" can't be used for checking
2920 2930 # existence of the 2nd parent, because "memctx._parents" is
2921 2931 # explicitly initialized by the list, of which length is 2.
2922 2932 if p2.rev() != nullrev:
2923 2933 man2 = p2.manifest()
2924 2934 managing = lambda f: f in man1 or f in man2
2925 2935 else:
2926 2936 managing = lambda f: f in man1
2927 2937
2928 2938 modified, added, removed = [], [], []
2929 2939 for f in self._files:
2930 2940 if not managing(f):
2931 2941 added.append(f)
2932 2942 elif self[f]:
2933 2943 modified.append(f)
2934 2944 else:
2935 2945 removed.append(f)
2936 2946
2937 2947 return scmutil.status(modified, added, removed, [], [], [], [])
2938 2948
2939 2949 def parents(self):
2940 2950 if self._parents[1].rev() == nullrev:
2941 2951 return [self._parents[0]]
2942 2952 return self._parents
2943 2953
2944 2954
2945 2955 class memfilectx(committablefilectx):
2946 2956 """memfilectx represents an in-memory file to commit.
2947 2957
2948 2958 See memctx and committablefilectx for more details.
2949 2959 """
2950 2960
2951 2961 def __init__(
2952 2962 self,
2953 2963 repo,
2954 2964 changectx,
2955 2965 path,
2956 2966 data,
2957 2967 islink=False,
2958 2968 isexec=False,
2959 2969 copysource=None,
2960 2970 ):
2961 2971 """
2962 2972 path is the normalized file path relative to repository root.
2963 2973 data is the file content as a string.
2964 2974 islink is True if the file is a symbolic link.
2965 2975 isexec is True if the file is executable.
2966 2976 copied is the source file path if current file was copied in the
2967 2977 revision being committed, or None."""
2968 2978 super(memfilectx, self).__init__(repo, path, None, changectx)
2969 2979 self._data = data
2970 2980 if islink:
2971 2981 self._flags = b'l'
2972 2982 elif isexec:
2973 2983 self._flags = b'x'
2974 2984 else:
2975 2985 self._flags = b''
2976 2986 self._copysource = copysource
2977 2987
2978 2988 def copysource(self):
2979 2989 return self._copysource
2980 2990
2981 2991 def cmp(self, fctx):
2982 2992 return self.data() != fctx.data()
2983 2993
2984 2994 def data(self):
2985 2995 return self._data
2986 2996
2987 2997 def remove(self, ignoremissing=False):
2988 2998 """wraps unlink for a repo's working directory"""
2989 2999 # need to figure out what to do here
2990 3000 del self._changectx[self._path]
2991 3001
2992 3002 def write(self, data, flags, **kwargs):
2993 3003 """wraps repo.wwrite"""
2994 3004 self._data = data
2995 3005
2996 3006
2997 3007 class metadataonlyctx(committablectx):
2998 3008 """Like memctx but it's reusing the manifest of different commit.
2999 3009 Intended to be used by lightweight operations that are creating
3000 3010 metadata-only changes.
3001 3011
3002 3012 Revision information is supplied at initialization time. 'repo' is the
3003 3013 current localrepo, 'ctx' is original revision which manifest we're reuisng
3004 3014 'parents' is a sequence of two parent revisions identifiers (pass None for
3005 3015 every missing parent), 'text' is the commit.
3006 3016
3007 3017 user receives the committer name and defaults to current repository
3008 3018 username, date is the commit date in any format supported by
3009 3019 dateutil.parsedate() and defaults to current date, extra is a dictionary of
3010 3020 metadata or is left empty.
3011 3021 """
3012 3022
3013 3023 def __init__(
3014 3024 self,
3015 3025 repo,
3016 3026 originalctx,
3017 3027 parents=None,
3018 3028 text=None,
3019 3029 user=None,
3020 3030 date=None,
3021 3031 extra=None,
3022 3032 editor=None,
3023 3033 ):
3024 3034 if text is None:
3025 3035 text = originalctx.description()
3026 3036 super(metadataonlyctx, self).__init__(repo, text, user, date, extra)
3027 3037 self._rev = None
3028 3038 self._node = None
3029 3039 self._originalctx = originalctx
3030 3040 self._manifestnode = originalctx.manifestnode()
3031 3041 if parents is None:
3032 3042 parents = originalctx.parents()
3033 3043 else:
3034 3044 parents = [repo[p] for p in parents if p is not None]
3035 3045 parents = parents[:]
3036 3046 while len(parents) < 2:
3037 3047 parents.append(repo[nullrev])
3038 3048 p1, p2 = self._parents = parents
3039 3049
3040 3050 # sanity check to ensure that the reused manifest parents are
3041 3051 # manifests of our commit parents
3042 3052 mp1, mp2 = self.manifestctx().parents
3043 3053 if p1 != self._repo.nodeconstants.nullid and p1.manifestnode() != mp1:
3044 3054 raise RuntimeError(
3045 3055 r"can't reuse the manifest: its p1 "
3046 3056 r"doesn't match the new ctx p1"
3047 3057 )
3048 3058 if p2 != self._repo.nodeconstants.nullid and p2.manifestnode() != mp2:
3049 3059 raise RuntimeError(
3050 3060 r"can't reuse the manifest: "
3051 3061 r"its p2 doesn't match the new ctx p2"
3052 3062 )
3053 3063
3054 3064 self._files = originalctx.files()
3055 3065 self.substate = {}
3056 3066
3057 3067 if editor:
3058 3068 self._text = editor(self._repo, self, [])
3059 3069 self._repo.savecommitmessage(self._text)
3060 3070
3061 3071 def manifestnode(self):
3062 3072 return self._manifestnode
3063 3073
3064 3074 @property
3065 3075 def _manifestctx(self):
3066 3076 return self._repo.manifestlog[self._manifestnode]
3067 3077
3068 3078 def filectx(self, path, filelog=None):
3069 3079 return self._originalctx.filectx(path, filelog=filelog)
3070 3080
3071 3081 def commit(self):
3072 3082 """commit context to the repo"""
3073 3083 return self._repo.commitctx(self)
3074 3084
3075 3085 @property
3076 3086 def _manifest(self):
3077 3087 return self._originalctx.manifest()
3078 3088
3079 3089 @propertycache
3080 3090 def _status(self):
3081 3091 """Calculate exact status from ``files`` specified in the ``origctx``
3082 3092 and parents manifests.
3083 3093 """
3084 3094 man1 = self.p1().manifest()
3085 3095 p2 = self._parents[1]
3086 3096 # "1 < len(self._parents)" can't be used for checking
3087 3097 # existence of the 2nd parent, because "metadataonlyctx._parents" is
3088 3098 # explicitly initialized by the list, of which length is 2.
3089 3099 if p2.rev() != nullrev:
3090 3100 man2 = p2.manifest()
3091 3101 managing = lambda f: f in man1 or f in man2
3092 3102 else:
3093 3103 managing = lambda f: f in man1
3094 3104
3095 3105 modified, added, removed = [], [], []
3096 3106 for f in self._files:
3097 3107 if not managing(f):
3098 3108 added.append(f)
3099 3109 elif f in self:
3100 3110 modified.append(f)
3101 3111 else:
3102 3112 removed.append(f)
3103 3113
3104 3114 return scmutil.status(modified, added, removed, [], [], [], [])
3105 3115
3106 3116
3107 3117 class arbitraryfilectx:
3108 3118 """Allows you to use filectx-like functions on a file in an arbitrary
3109 3119 location on disk, possibly not in the working directory.
3110 3120 """
3111 3121
3112 3122 def __init__(self, path, repo=None):
3113 3123 # Repo is optional because contrib/simplemerge uses this class.
3114 3124 self._repo = repo
3115 3125 self._path = path
3116 3126
3117 3127 def cmp(self, fctx):
3118 3128 # filecmp follows symlinks whereas `cmp` should not, so skip the fast
3119 3129 # path if either side is a symlink.
3120 3130 symlinks = b'l' in self.flags() or b'l' in fctx.flags()
3121 3131 if not symlinks and isinstance(fctx, workingfilectx) and self._repo:
3122 3132 # Add a fast-path for merge if both sides are disk-backed.
3123 3133 # Note that filecmp uses the opposite return values (True if same)
3124 3134 # from our cmp functions (True if different).
3125 3135 return not filecmp.cmp(self.path(), self._repo.wjoin(fctx.path()))
3126 3136 return self.data() != fctx.data()
3127 3137
3128 3138 def path(self):
3129 3139 return self._path
3130 3140
3131 3141 def flags(self):
3132 3142 return b''
3133 3143
3134 3144 def data(self):
3135 3145 return util.readfile(self._path)
3136 3146
3137 3147 def decodeddata(self):
3138 3148 return util.readfile(self._path)
3139 3149
3140 3150 def remove(self):
3141 3151 util.unlink(self._path)
3142 3152
3143 3153 def write(self, data, flags, **kwargs):
3144 3154 assert not flags
3145 3155 util.writefile(self._path, data)
@@ -1,293 +1,295 b''
1 1 # filelog.py - file history class for mercurial
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8
9 9 from .i18n import _
10 10 from .node import nullrev
11 11 from . import (
12 12 error,
13 13 revlog,
14 14 )
15 15 from .interfaces import (
16 16 repository,
17 17 util as interfaceutil,
18 18 )
19 19 from .utils import storageutil
20 20 from .revlogutils import (
21 21 constants as revlog_constants,
22 22 rewrite,
23 23 )
24 24
25 25
26 26 @interfaceutil.implementer(repository.ifilestorage)
27 27 class filelog:
28 28 def __init__(self, opener, path):
29 29 self._revlog = revlog.revlog(
30 30 opener,
31 31 # XXX should use the unencoded path
32 32 target=(revlog_constants.KIND_FILELOG, path),
33 33 radix=b'/'.join((b'data', path)),
34 34 censorable=True,
35 canonical_parent_order=False, # see comment in revlog.py
35 36 )
36 37 # Full name of the user visible file, relative to the repository root.
37 38 # Used by LFS.
38 39 self._revlog.filename = path
39 40 self.nullid = self._revlog.nullid
40 41 opts = opener.options
41 42 self._fix_issue6528 = opts.get(b'issue6528.fix-incoming', True)
42 43
43 44 def __len__(self):
44 45 return len(self._revlog)
45 46
46 47 def __iter__(self):
47 48 return self._revlog.__iter__()
48 49
49 50 def hasnode(self, node):
50 51 if node in (self.nullid, nullrev):
51 52 return False
52 53
53 54 try:
54 55 self._revlog.rev(node)
55 56 return True
56 57 except (TypeError, ValueError, IndexError, error.LookupError):
57 58 return False
58 59
59 60 def revs(self, start=0, stop=None):
60 61 return self._revlog.revs(start=start, stop=stop)
61 62
62 63 def parents(self, node):
63 64 return self._revlog.parents(node)
64 65
65 66 def parentrevs(self, rev):
66 67 return self._revlog.parentrevs(rev)
67 68
68 69 def rev(self, node):
69 70 return self._revlog.rev(node)
70 71
71 72 def node(self, rev):
72 73 return self._revlog.node(rev)
73 74
74 75 def lookup(self, node):
75 76 return storageutil.fileidlookup(
76 77 self._revlog, node, self._revlog.display_id
77 78 )
78 79
79 80 def linkrev(self, rev):
80 81 return self._revlog.linkrev(rev)
81 82
82 83 def commonancestorsheads(self, node1, node2):
83 84 return self._revlog.commonancestorsheads(node1, node2)
84 85
85 86 # Used by dagop.blockdescendants().
86 87 def descendants(self, revs):
87 88 return self._revlog.descendants(revs)
88 89
89 90 def heads(self, start=None, stop=None):
90 91 return self._revlog.heads(start, stop)
91 92
92 93 # Used by hgweb, children extension.
93 94 def children(self, node):
94 95 return self._revlog.children(node)
95 96
96 97 def iscensored(self, rev):
97 98 return self._revlog.iscensored(rev)
98 99
99 100 def revision(self, node, _df=None):
100 101 return self._revlog.revision(node, _df=_df)
101 102
102 103 def rawdata(self, node, _df=None):
103 104 return self._revlog.rawdata(node, _df=_df)
104 105
105 106 def emitrevisions(
106 107 self,
107 108 nodes,
108 109 nodesorder=None,
109 110 revisiondata=False,
110 111 assumehaveparentrevisions=False,
111 112 deltamode=repository.CG_DELTAMODE_STD,
112 113 sidedata_helpers=None,
113 114 ):
114 115 return self._revlog.emitrevisions(
115 116 nodes,
116 117 nodesorder=nodesorder,
117 118 revisiondata=revisiondata,
118 119 assumehaveparentrevisions=assumehaveparentrevisions,
119 120 deltamode=deltamode,
120 121 sidedata_helpers=sidedata_helpers,
121 122 )
122 123
123 124 def addrevision(
124 125 self,
125 126 revisiondata,
126 127 transaction,
127 128 linkrev,
128 129 p1,
129 130 p2,
130 131 node=None,
131 132 flags=revlog.REVIDX_DEFAULT_FLAGS,
132 133 cachedelta=None,
133 134 ):
134 135 return self._revlog.addrevision(
135 136 revisiondata,
136 137 transaction,
137 138 linkrev,
138 139 p1,
139 140 p2,
140 141 node=node,
141 142 flags=flags,
142 143 cachedelta=cachedelta,
143 144 )
144 145
145 146 def addgroup(
146 147 self,
147 148 deltas,
148 149 linkmapper,
149 150 transaction,
150 151 addrevisioncb=None,
151 152 duplicaterevisioncb=None,
152 153 maybemissingparents=False,
153 154 ):
154 155 if maybemissingparents:
155 156 raise error.Abort(
156 157 _(
157 158 b'revlog storage does not support missing '
158 159 b'parents write mode'
159 160 )
160 161 )
161 162
162 163 with self._revlog._writing(transaction):
163 164
164 165 if self._fix_issue6528:
165 166 deltas = rewrite.filter_delta_issue6528(self._revlog, deltas)
166 167
167 168 return self._revlog.addgroup(
168 169 deltas,
169 170 linkmapper,
170 171 transaction,
171 172 addrevisioncb=addrevisioncb,
172 173 duplicaterevisioncb=duplicaterevisioncb,
173 174 )
174 175
175 176 def getstrippoint(self, minlink):
176 177 return self._revlog.getstrippoint(minlink)
177 178
178 179 def strip(self, minlink, transaction):
179 180 return self._revlog.strip(minlink, transaction)
180 181
181 182 def censorrevision(self, tr, node, tombstone=b''):
182 183 return self._revlog.censorrevision(tr, node, tombstone=tombstone)
183 184
184 185 def files(self):
185 186 return self._revlog.files()
186 187
187 188 def read(self, node):
188 189 return storageutil.filtermetadata(self.revision(node))
189 190
190 191 def add(self, text, meta, transaction, link, p1=None, p2=None):
191 192 if meta or text.startswith(b'\1\n'):
192 193 text = storageutil.packmeta(meta, text)
193 194 rev = self.addrevision(text, transaction, link, p1, p2)
194 195 return self.node(rev)
195 196
196 197 def renamed(self, node):
197 198 return storageutil.filerevisioncopied(self, node)
198 199
199 200 def size(self, rev):
200 201 """return the size of a given revision"""
201 202
202 203 # for revisions with renames, we have to go the slow way
203 204 node = self.node(rev)
204 205 if self.renamed(node):
205 206 return len(self.read(node))
206 207 if self.iscensored(rev):
207 208 return 0
208 209
209 210 # XXX if self.read(node).startswith("\1\n"), this returns (size+4)
211 # XXX See also basefilectx.cmp.
210 212 return self._revlog.size(rev)
211 213
212 214 def cmp(self, node, text):
213 215 """compare text with a given file revision
214 216
215 217 returns True if text is different than what is stored.
216 218 """
217 219 return not storageutil.filedataequivalent(self, node, text)
218 220
219 221 def verifyintegrity(self, state):
220 222 return self._revlog.verifyintegrity(state)
221 223
222 224 def storageinfo(
223 225 self,
224 226 exclusivefiles=False,
225 227 sharedfiles=False,
226 228 revisionscount=False,
227 229 trackedsize=False,
228 230 storedsize=False,
229 231 ):
230 232 return self._revlog.storageinfo(
231 233 exclusivefiles=exclusivefiles,
232 234 sharedfiles=sharedfiles,
233 235 revisionscount=revisionscount,
234 236 trackedsize=trackedsize,
235 237 storedsize=storedsize,
236 238 )
237 239
238 240 # Used by repo upgrade.
239 241 def clone(self, tr, destrevlog, **kwargs):
240 242 if not isinstance(destrevlog, filelog):
241 243 raise error.ProgrammingError(b'expected filelog to clone()')
242 244
243 245 return self._revlog.clone(tr, destrevlog._revlog, **kwargs)
244 246
245 247
246 248 class narrowfilelog(filelog):
247 249 """Filelog variation to be used with narrow stores."""
248 250
249 251 def __init__(self, opener, path, narrowmatch):
250 252 super(narrowfilelog, self).__init__(opener, path)
251 253 self._narrowmatch = narrowmatch
252 254
253 255 def renamed(self, node):
254 256 res = super(narrowfilelog, self).renamed(node)
255 257
256 258 # Renames that come from outside the narrowspec are problematic
257 259 # because we may lack the base text for the rename. This can result
258 260 # in code attempting to walk the ancestry or compute a diff
259 261 # encountering a missing revision. We address this by silently
260 262 # removing rename metadata if the source file is outside the
261 263 # narrow spec.
262 264 #
263 265 # A better solution would be to see if the base revision is available,
264 266 # rather than assuming it isn't.
265 267 #
266 268 # An even better solution would be to teach all consumers of rename
267 269 # metadata that the base revision may not be available.
268 270 #
269 271 # TODO consider better ways of doing this.
270 272 if res and not self._narrowmatch(res[0]):
271 273 return None
272 274
273 275 return res
274 276
275 277 def size(self, rev):
276 278 # Because we have a custom renamed() that may lie, we need to call
277 279 # the base renamed() to report accurate results.
278 280 node = self.node(rev)
279 281 if super(narrowfilelog, self).renamed(node):
280 282 return len(self.read(node))
281 283 else:
282 284 return super(narrowfilelog, self).size(rev)
283 285
284 286 def cmp(self, node, text):
285 287 # We don't call `super` because narrow parents can be buggy in case of a
286 288 # ambiguous dirstate. Always take the slow path until there is a better
287 289 # fix, see issue6150.
288 290
289 291 # Censored files compare against the empty file.
290 292 if self.iscensored(self.rev(node)):
291 293 return text != b''
292 294
293 295 return self.read(node) != text
@@ -1,3307 +1,3322 b''
1 1 # revlog.py - storage back-end for mercurial
2 2 # coding: utf8
3 3 #
4 4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 """Storage back-end for Mercurial.
10 10
11 11 This provides efficient delta storage with O(1) retrieve and append
12 12 and O(changes) merge between branches.
13 13 """
14 14
15 15
16 16 import binascii
17 17 import collections
18 18 import contextlib
19 19 import errno
20 20 import io
21 21 import os
22 22 import struct
23 23 import zlib
24 24
25 25 # import stuff from node for others to import from revlog
26 26 from .node import (
27 27 bin,
28 28 hex,
29 29 nullrev,
30 30 sha1nodeconstants,
31 31 short,
32 32 wdirrev,
33 33 )
34 34 from .i18n import _
35 35 from .pycompat import getattr
36 36 from .revlogutils.constants import (
37 37 ALL_KINDS,
38 38 CHANGELOGV2,
39 39 COMP_MODE_DEFAULT,
40 40 COMP_MODE_INLINE,
41 41 COMP_MODE_PLAIN,
42 42 ENTRY_RANK,
43 43 FEATURES_BY_VERSION,
44 44 FLAG_GENERALDELTA,
45 45 FLAG_INLINE_DATA,
46 46 INDEX_HEADER,
47 47 KIND_CHANGELOG,
48 48 RANK_UNKNOWN,
49 49 REVLOGV0,
50 50 REVLOGV1,
51 51 REVLOGV1_FLAGS,
52 52 REVLOGV2,
53 53 REVLOGV2_FLAGS,
54 54 REVLOG_DEFAULT_FLAGS,
55 55 REVLOG_DEFAULT_FORMAT,
56 56 REVLOG_DEFAULT_VERSION,
57 57 SUPPORTED_FLAGS,
58 58 )
59 59 from .revlogutils.flagutil import (
60 60 REVIDX_DEFAULT_FLAGS,
61 61 REVIDX_ELLIPSIS,
62 62 REVIDX_EXTSTORED,
63 63 REVIDX_FLAGS_ORDER,
64 64 REVIDX_HASCOPIESINFO,
65 65 REVIDX_ISCENSORED,
66 66 REVIDX_RAWTEXT_CHANGING_FLAGS,
67 67 )
68 68 from .thirdparty import attr
69 69 from . import (
70 70 ancestor,
71 71 dagop,
72 72 error,
73 73 mdiff,
74 74 policy,
75 75 pycompat,
76 76 revlogutils,
77 77 templatefilters,
78 78 util,
79 79 )
80 80 from .interfaces import (
81 81 repository,
82 82 util as interfaceutil,
83 83 )
84 84 from .revlogutils import (
85 85 deltas as deltautil,
86 86 docket as docketutil,
87 87 flagutil,
88 88 nodemap as nodemaputil,
89 89 randomaccessfile,
90 90 revlogv0,
91 91 rewrite,
92 92 sidedata as sidedatautil,
93 93 )
94 94 from .utils import (
95 95 storageutil,
96 96 stringutil,
97 97 )
98 98
99 99 # blanked usage of all the name to prevent pyflakes constraints
100 100 # We need these name available in the module for extensions.
101 101
102 102 REVLOGV0
103 103 REVLOGV1
104 104 REVLOGV2
105 105 CHANGELOGV2
106 106 FLAG_INLINE_DATA
107 107 FLAG_GENERALDELTA
108 108 REVLOG_DEFAULT_FLAGS
109 109 REVLOG_DEFAULT_FORMAT
110 110 REVLOG_DEFAULT_VERSION
111 111 REVLOGV1_FLAGS
112 112 REVLOGV2_FLAGS
113 113 REVIDX_ISCENSORED
114 114 REVIDX_ELLIPSIS
115 115 REVIDX_HASCOPIESINFO
116 116 REVIDX_EXTSTORED
117 117 REVIDX_DEFAULT_FLAGS
118 118 REVIDX_FLAGS_ORDER
119 119 REVIDX_RAWTEXT_CHANGING_FLAGS
120 120
121 121 parsers = policy.importmod('parsers')
122 122 rustancestor = policy.importrust('ancestor')
123 123 rustdagop = policy.importrust('dagop')
124 124 rustrevlog = policy.importrust('revlog')
125 125
126 126 # Aliased for performance.
127 127 _zlibdecompress = zlib.decompress
128 128
129 129 # max size of revlog with inline data
130 130 _maxinline = 131072
131 131
132 132 # Flag processors for REVIDX_ELLIPSIS.
133 133 def ellipsisreadprocessor(rl, text):
134 134 return text, False
135 135
136 136
137 137 def ellipsiswriteprocessor(rl, text):
138 138 return text, False
139 139
140 140
141 141 def ellipsisrawprocessor(rl, text):
142 142 return False
143 143
144 144
145 145 ellipsisprocessor = (
146 146 ellipsisreadprocessor,
147 147 ellipsiswriteprocessor,
148 148 ellipsisrawprocessor,
149 149 )
150 150
151 151
152 152 def _verify_revision(rl, skipflags, state, node):
153 153 """Verify the integrity of the given revlog ``node`` while providing a hook
154 154 point for extensions to influence the operation."""
155 155 if skipflags:
156 156 state[b'skipread'].add(node)
157 157 else:
158 158 # Side-effect: read content and verify hash.
159 159 rl.revision(node)
160 160
161 161
162 162 # True if a fast implementation for persistent-nodemap is available
163 163 #
164 164 # We also consider we have a "fast" implementation in "pure" python because
165 165 # people using pure don't really have performance consideration (and a
166 166 # wheelbarrow of other slowness source)
167 167 HAS_FAST_PERSISTENT_NODEMAP = rustrevlog is not None or util.safehasattr(
168 168 parsers, 'BaseIndexObject'
169 169 )
170 170
171 171
172 172 @interfaceutil.implementer(repository.irevisiondelta)
173 173 @attr.s(slots=True)
174 174 class revlogrevisiondelta:
175 175 node = attr.ib()
176 176 p1node = attr.ib()
177 177 p2node = attr.ib()
178 178 basenode = attr.ib()
179 179 flags = attr.ib()
180 180 baserevisionsize = attr.ib()
181 181 revision = attr.ib()
182 182 delta = attr.ib()
183 183 sidedata = attr.ib()
184 184 protocol_flags = attr.ib()
185 185 linknode = attr.ib(default=None)
186 186
187 187
188 188 @interfaceutil.implementer(repository.iverifyproblem)
189 189 @attr.s(frozen=True)
190 190 class revlogproblem:
191 191 warning = attr.ib(default=None)
192 192 error = attr.ib(default=None)
193 193 node = attr.ib(default=None)
194 194
195 195
196 196 def parse_index_v1(data, inline):
197 197 # call the C implementation to parse the index data
198 198 index, cache = parsers.parse_index2(data, inline)
199 199 return index, cache
200 200
201 201
202 202 def parse_index_v2(data, inline):
203 203 # call the C implementation to parse the index data
204 204 index, cache = parsers.parse_index2(data, inline, format=REVLOGV2)
205 205 return index, cache
206 206
207 207
208 208 def parse_index_cl_v2(data, inline):
209 209 # call the C implementation to parse the index data
210 210 index, cache = parsers.parse_index2(data, inline, format=CHANGELOGV2)
211 211 return index, cache
212 212
213 213
214 214 if util.safehasattr(parsers, 'parse_index_devel_nodemap'):
215 215
216 216 def parse_index_v1_nodemap(data, inline):
217 217 index, cache = parsers.parse_index_devel_nodemap(data, inline)
218 218 return index, cache
219 219
220 220
221 221 else:
222 222 parse_index_v1_nodemap = None
223 223
224 224
225 225 def parse_index_v1_mixed(data, inline):
226 226 index, cache = parse_index_v1(data, inline)
227 227 return rustrevlog.MixedIndex(index), cache
228 228
229 229
230 230 # corresponds to uncompressed length of indexformatng (2 gigs, 4-byte
231 231 # signed integer)
232 232 _maxentrysize = 0x7FFFFFFF
233 233
234 234 FILE_TOO_SHORT_MSG = _(
235 235 b'cannot read from revlog %s;'
236 236 b' expected %d bytes from offset %d, data size is %d'
237 237 )
238 238
239 239
240 240 class revlog:
241 241 """
242 242 the underlying revision storage object
243 243
244 244 A revlog consists of two parts, an index and the revision data.
245 245
246 246 The index is a file with a fixed record size containing
247 247 information on each revision, including its nodeid (hash), the
248 248 nodeids of its parents, the position and offset of its data within
249 249 the data file, and the revision it's based on. Finally, each entry
250 250 contains a linkrev entry that can serve as a pointer to external
251 251 data.
252 252
253 253 The revision data itself is a linear collection of data chunks.
254 254 Each chunk represents a revision and is usually represented as a
255 255 delta against the previous chunk. To bound lookup time, runs of
256 256 deltas are limited to about 2 times the length of the original
257 257 version data. This makes retrieval of a version proportional to
258 258 its size, or O(1) relative to the number of revisions.
259 259
260 260 Both pieces of the revlog are written to in an append-only
261 261 fashion, which means we never need to rewrite a file to insert or
262 262 remove data, and can use some simple techniques to avoid the need
263 263 for locking while reading.
264 264
265 265 If checkambig, indexfile is opened with checkambig=True at
266 266 writing, to avoid file stat ambiguity.
267 267
268 268 If mmaplargeindex is True, and an mmapindexthreshold is set, the
269 269 index will be mmapped rather than read if it is larger than the
270 270 configured threshold.
271 271
272 272 If censorable is True, the revlog can have censored revisions.
273 273
274 274 If `upperboundcomp` is not None, this is the expected maximal gain from
275 275 compression for the data content.
276 276
277 277 `concurrencychecker` is an optional function that receives 3 arguments: a
278 278 file handle, a filename, and an expected position. It should check whether
279 279 the current position in the file handle is valid, and log/warn/fail (by
280 280 raising).
281 281
282 282 See mercurial/revlogutils/contants.py for details about the content of an
283 283 index entry.
284 284 """
285 285
286 286 _flagserrorclass = error.RevlogError
287 287
288 288 def __init__(
289 289 self,
290 290 opener,
291 291 target,
292 292 radix,
293 293 postfix=None, # only exist for `tmpcensored` now
294 294 checkambig=False,
295 295 mmaplargeindex=False,
296 296 censorable=False,
297 297 upperboundcomp=None,
298 298 persistentnodemap=False,
299 299 concurrencychecker=None,
300 300 trypending=False,
301 canonical_parent_order=True,
301 302 ):
302 303 """
303 304 create a revlog object
304 305
305 306 opener is a function that abstracts the file opening operation
306 307 and can be used to implement COW semantics or the like.
307 308
308 309 `target`: a (KIND, ID) tuple that identify the content stored in
309 310 this revlog. It help the rest of the code to understand what the revlog
310 311 is about without having to resort to heuristic and index filename
311 312 analysis. Note: that this must be reliably be set by normal code, but
312 313 that test, debug, or performance measurement code might not set this to
313 314 accurate value.
314 315 """
315 316 self.upperboundcomp = upperboundcomp
316 317
317 318 self.radix = radix
318 319
319 320 self._docket_file = None
320 321 self._indexfile = None
321 322 self._datafile = None
322 323 self._sidedatafile = None
323 324 self._nodemap_file = None
324 325 self.postfix = postfix
325 326 self._trypending = trypending
326 327 self.opener = opener
327 328 if persistentnodemap:
328 329 self._nodemap_file = nodemaputil.get_nodemap_file(self)
329 330
330 331 assert target[0] in ALL_KINDS
331 332 assert len(target) == 2
332 333 self.target = target
333 334 # When True, indexfile is opened with checkambig=True at writing, to
334 335 # avoid file stat ambiguity.
335 336 self._checkambig = checkambig
336 337 self._mmaplargeindex = mmaplargeindex
337 338 self._censorable = censorable
338 339 # 3-tuple of (node, rev, text) for a raw revision.
339 340 self._revisioncache = None
340 341 # Maps rev to chain base rev.
341 342 self._chainbasecache = util.lrucachedict(100)
342 343 # 2-tuple of (offset, data) of raw data from the revlog at an offset.
343 344 self._chunkcache = (0, b'')
344 345 # How much data to read and cache into the raw revlog data cache.
345 346 self._chunkcachesize = 65536
346 347 self._maxchainlen = None
347 348 self._deltabothparents = True
348 349 self.index = None
349 350 self._docket = None
350 351 self._nodemap_docket = None
351 352 # Mapping of partial identifiers to full nodes.
352 353 self._pcache = {}
353 354 # Mapping of revision integer to full node.
354 355 self._compengine = b'zlib'
355 356 self._compengineopts = {}
356 357 self._maxdeltachainspan = -1
357 358 self._withsparseread = False
358 359 self._sparserevlog = False
359 360 self.hassidedata = False
360 361 self._srdensitythreshold = 0.50
361 362 self._srmingapsize = 262144
362 363
363 364 # Make copy of flag processors so each revlog instance can support
364 365 # custom flags.
365 366 self._flagprocessors = dict(flagutil.flagprocessors)
366 367
367 368 # 3-tuple of file handles being used for active writing.
368 369 self._writinghandles = None
369 370 # prevent nesting of addgroup
370 371 self._adding_group = None
371 372
372 373 self._loadindex()
373 374
374 375 self._concurrencychecker = concurrencychecker
375 376
377 # parent order is supposed to be semantically irrelevant, so we
378 # normally resort parents to ensure that the first parent is non-null,
379 # if there is a non-null parent at all.
380 # filelog abuses the parent order as flag to mark some instances of
381 # meta-encoded files, so allow it to disable this behavior.
382 self.canonical_parent_order = canonical_parent_order
383
376 384 def _init_opts(self):
377 385 """process options (from above/config) to setup associated default revlog mode
378 386
379 387 These values might be affected when actually reading on disk information.
380 388
381 389 The relevant values are returned for use in _loadindex().
382 390
383 391 * newversionflags:
384 392 version header to use if we need to create a new revlog
385 393
386 394 * mmapindexthreshold:
387 395 minimal index size for start to use mmap
388 396
389 397 * force_nodemap:
390 398 force the usage of a "development" version of the nodemap code
391 399 """
392 400 mmapindexthreshold = None
393 401 opts = self.opener.options
394 402
395 403 if b'changelogv2' in opts and self.revlog_kind == KIND_CHANGELOG:
396 404 new_header = CHANGELOGV2
397 405 elif b'revlogv2' in opts:
398 406 new_header = REVLOGV2
399 407 elif b'revlogv1' in opts:
400 408 new_header = REVLOGV1 | FLAG_INLINE_DATA
401 409 if b'generaldelta' in opts:
402 410 new_header |= FLAG_GENERALDELTA
403 411 elif b'revlogv0' in self.opener.options:
404 412 new_header = REVLOGV0
405 413 else:
406 414 new_header = REVLOG_DEFAULT_VERSION
407 415
408 416 if b'chunkcachesize' in opts:
409 417 self._chunkcachesize = opts[b'chunkcachesize']
410 418 if b'maxchainlen' in opts:
411 419 self._maxchainlen = opts[b'maxchainlen']
412 420 if b'deltabothparents' in opts:
413 421 self._deltabothparents = opts[b'deltabothparents']
414 422 self._lazydelta = bool(opts.get(b'lazydelta', True))
415 423 self._lazydeltabase = False
416 424 if self._lazydelta:
417 425 self._lazydeltabase = bool(opts.get(b'lazydeltabase', False))
418 426 if b'compengine' in opts:
419 427 self._compengine = opts[b'compengine']
420 428 if b'zlib.level' in opts:
421 429 self._compengineopts[b'zlib.level'] = opts[b'zlib.level']
422 430 if b'zstd.level' in opts:
423 431 self._compengineopts[b'zstd.level'] = opts[b'zstd.level']
424 432 if b'maxdeltachainspan' in opts:
425 433 self._maxdeltachainspan = opts[b'maxdeltachainspan']
426 434 if self._mmaplargeindex and b'mmapindexthreshold' in opts:
427 435 mmapindexthreshold = opts[b'mmapindexthreshold']
428 436 self._sparserevlog = bool(opts.get(b'sparse-revlog', False))
429 437 withsparseread = bool(opts.get(b'with-sparse-read', False))
430 438 # sparse-revlog forces sparse-read
431 439 self._withsparseread = self._sparserevlog or withsparseread
432 440 if b'sparse-read-density-threshold' in opts:
433 441 self._srdensitythreshold = opts[b'sparse-read-density-threshold']
434 442 if b'sparse-read-min-gap-size' in opts:
435 443 self._srmingapsize = opts[b'sparse-read-min-gap-size']
436 444 if opts.get(b'enableellipsis'):
437 445 self._flagprocessors[REVIDX_ELLIPSIS] = ellipsisprocessor
438 446
439 447 # revlog v0 doesn't have flag processors
440 448 for flag, processor in opts.get(b'flagprocessors', {}).items():
441 449 flagutil.insertflagprocessor(flag, processor, self._flagprocessors)
442 450
443 451 if self._chunkcachesize <= 0:
444 452 raise error.RevlogError(
445 453 _(b'revlog chunk cache size %r is not greater than 0')
446 454 % self._chunkcachesize
447 455 )
448 456 elif self._chunkcachesize & (self._chunkcachesize - 1):
449 457 raise error.RevlogError(
450 458 _(b'revlog chunk cache size %r is not a power of 2')
451 459 % self._chunkcachesize
452 460 )
453 461 force_nodemap = opts.get(b'devel-force-nodemap', False)
454 462 return new_header, mmapindexthreshold, force_nodemap
455 463
456 464 def _get_data(self, filepath, mmap_threshold, size=None):
457 465 """return a file content with or without mmap
458 466
459 467 If the file is missing return the empty string"""
460 468 try:
461 469 with self.opener(filepath) as fp:
462 470 if mmap_threshold is not None:
463 471 file_size = self.opener.fstat(fp).st_size
464 472 if file_size >= mmap_threshold:
465 473 if size is not None:
466 474 # avoid potentiel mmap crash
467 475 size = min(file_size, size)
468 476 # TODO: should .close() to release resources without
469 477 # relying on Python GC
470 478 if size is None:
471 479 return util.buffer(util.mmapread(fp))
472 480 else:
473 481 return util.buffer(util.mmapread(fp, size))
474 482 if size is None:
475 483 return fp.read()
476 484 else:
477 485 return fp.read(size)
478 486 except IOError as inst:
479 487 if inst.errno != errno.ENOENT:
480 488 raise
481 489 return b''
482 490
483 491 def _loadindex(self, docket=None):
484 492
485 493 new_header, mmapindexthreshold, force_nodemap = self._init_opts()
486 494
487 495 if self.postfix is not None:
488 496 entry_point = b'%s.i.%s' % (self.radix, self.postfix)
489 497 elif self._trypending and self.opener.exists(b'%s.i.a' % self.radix):
490 498 entry_point = b'%s.i.a' % self.radix
491 499 else:
492 500 entry_point = b'%s.i' % self.radix
493 501
494 502 if docket is not None:
495 503 self._docket = docket
496 504 self._docket_file = entry_point
497 505 else:
498 506 entry_data = b''
499 507 self._initempty = True
500 508 entry_data = self._get_data(entry_point, mmapindexthreshold)
501 509 if len(entry_data) > 0:
502 510 header = INDEX_HEADER.unpack(entry_data[:4])[0]
503 511 self._initempty = False
504 512 else:
505 513 header = new_header
506 514
507 515 self._format_flags = header & ~0xFFFF
508 516 self._format_version = header & 0xFFFF
509 517
510 518 supported_flags = SUPPORTED_FLAGS.get(self._format_version)
511 519 if supported_flags is None:
512 520 msg = _(b'unknown version (%d) in revlog %s')
513 521 msg %= (self._format_version, self.display_id)
514 522 raise error.RevlogError(msg)
515 523 elif self._format_flags & ~supported_flags:
516 524 msg = _(b'unknown flags (%#04x) in version %d revlog %s')
517 525 display_flag = self._format_flags >> 16
518 526 msg %= (display_flag, self._format_version, self.display_id)
519 527 raise error.RevlogError(msg)
520 528
521 529 features = FEATURES_BY_VERSION[self._format_version]
522 530 self._inline = features[b'inline'](self._format_flags)
523 531 self._generaldelta = features[b'generaldelta'](self._format_flags)
524 532 self.hassidedata = features[b'sidedata']
525 533
526 534 if not features[b'docket']:
527 535 self._indexfile = entry_point
528 536 index_data = entry_data
529 537 else:
530 538 self._docket_file = entry_point
531 539 if self._initempty:
532 540 self._docket = docketutil.default_docket(self, header)
533 541 else:
534 542 self._docket = docketutil.parse_docket(
535 543 self, entry_data, use_pending=self._trypending
536 544 )
537 545
538 546 if self._docket is not None:
539 547 self._indexfile = self._docket.index_filepath()
540 548 index_data = b''
541 549 index_size = self._docket.index_end
542 550 if index_size > 0:
543 551 index_data = self._get_data(
544 552 self._indexfile, mmapindexthreshold, size=index_size
545 553 )
546 554 if len(index_data) < index_size:
547 555 msg = _(b'too few index data for %s: got %d, expected %d')
548 556 msg %= (self.display_id, len(index_data), index_size)
549 557 raise error.RevlogError(msg)
550 558
551 559 self._inline = False
552 560 # generaldelta implied by version 2 revlogs.
553 561 self._generaldelta = True
554 562 # the logic for persistent nodemap will be dealt with within the
555 563 # main docket, so disable it for now.
556 564 self._nodemap_file = None
557 565
558 566 if self._docket is not None:
559 567 self._datafile = self._docket.data_filepath()
560 568 self._sidedatafile = self._docket.sidedata_filepath()
561 569 elif self.postfix is None:
562 570 self._datafile = b'%s.d' % self.radix
563 571 else:
564 572 self._datafile = b'%s.d.%s' % (self.radix, self.postfix)
565 573
566 574 self.nodeconstants = sha1nodeconstants
567 575 self.nullid = self.nodeconstants.nullid
568 576
569 577 # sparse-revlog can't be on without general-delta (issue6056)
570 578 if not self._generaldelta:
571 579 self._sparserevlog = False
572 580
573 581 self._storedeltachains = True
574 582
575 583 devel_nodemap = (
576 584 self._nodemap_file
577 585 and force_nodemap
578 586 and parse_index_v1_nodemap is not None
579 587 )
580 588
581 589 use_rust_index = False
582 590 if rustrevlog is not None:
583 591 if self._nodemap_file is not None:
584 592 use_rust_index = True
585 593 else:
586 594 use_rust_index = self.opener.options.get(b'rust.index')
587 595
588 596 self._parse_index = parse_index_v1
589 597 if self._format_version == REVLOGV0:
590 598 self._parse_index = revlogv0.parse_index_v0
591 599 elif self._format_version == REVLOGV2:
592 600 self._parse_index = parse_index_v2
593 601 elif self._format_version == CHANGELOGV2:
594 602 self._parse_index = parse_index_cl_v2
595 603 elif devel_nodemap:
596 604 self._parse_index = parse_index_v1_nodemap
597 605 elif use_rust_index:
598 606 self._parse_index = parse_index_v1_mixed
599 607 try:
600 608 d = self._parse_index(index_data, self._inline)
601 609 index, chunkcache = d
602 610 use_nodemap = (
603 611 not self._inline
604 612 and self._nodemap_file is not None
605 613 and util.safehasattr(index, 'update_nodemap_data')
606 614 )
607 615 if use_nodemap:
608 616 nodemap_data = nodemaputil.persisted_data(self)
609 617 if nodemap_data is not None:
610 618 docket = nodemap_data[0]
611 619 if (
612 620 len(d[0]) > docket.tip_rev
613 621 and d[0][docket.tip_rev][7] == docket.tip_node
614 622 ):
615 623 # no changelog tampering
616 624 self._nodemap_docket = docket
617 625 index.update_nodemap_data(*nodemap_data)
618 626 except (ValueError, IndexError):
619 627 raise error.RevlogError(
620 628 _(b"index %s is corrupted") % self.display_id
621 629 )
622 630 self.index = index
623 631 self._segmentfile = randomaccessfile.randomaccessfile(
624 632 self.opener,
625 633 (self._indexfile if self._inline else self._datafile),
626 634 self._chunkcachesize,
627 635 chunkcache,
628 636 )
629 637 self._segmentfile_sidedata = randomaccessfile.randomaccessfile(
630 638 self.opener,
631 639 self._sidedatafile,
632 640 self._chunkcachesize,
633 641 )
634 642 # revnum -> (chain-length, sum-delta-length)
635 643 self._chaininfocache = util.lrucachedict(500)
636 644 # revlog header -> revlog compressor
637 645 self._decompressors = {}
638 646
639 647 @util.propertycache
640 648 def revlog_kind(self):
641 649 return self.target[0]
642 650
643 651 @util.propertycache
644 652 def display_id(self):
645 653 """The public facing "ID" of the revlog that we use in message"""
646 654 # Maybe we should build a user facing representation of
647 655 # revlog.target instead of using `self.radix`
648 656 return self.radix
649 657
650 658 def _get_decompressor(self, t):
651 659 try:
652 660 compressor = self._decompressors[t]
653 661 except KeyError:
654 662 try:
655 663 engine = util.compengines.forrevlogheader(t)
656 664 compressor = engine.revlogcompressor(self._compengineopts)
657 665 self._decompressors[t] = compressor
658 666 except KeyError:
659 667 raise error.RevlogError(
660 668 _(b'unknown compression type %s') % binascii.hexlify(t)
661 669 )
662 670 return compressor
663 671
664 672 @util.propertycache
665 673 def _compressor(self):
666 674 engine = util.compengines[self._compengine]
667 675 return engine.revlogcompressor(self._compengineopts)
668 676
669 677 @util.propertycache
670 678 def _decompressor(self):
671 679 """the default decompressor"""
672 680 if self._docket is None:
673 681 return None
674 682 t = self._docket.default_compression_header
675 683 c = self._get_decompressor(t)
676 684 return c.decompress
677 685
678 686 def _indexfp(self):
679 687 """file object for the revlog's index file"""
680 688 return self.opener(self._indexfile, mode=b"r")
681 689
682 690 def __index_write_fp(self):
683 691 # You should not use this directly and use `_writing` instead
684 692 try:
685 693 f = self.opener(
686 694 self._indexfile, mode=b"r+", checkambig=self._checkambig
687 695 )
688 696 if self._docket is None:
689 697 f.seek(0, os.SEEK_END)
690 698 else:
691 699 f.seek(self._docket.index_end, os.SEEK_SET)
692 700 return f
693 701 except IOError as inst:
694 702 if inst.errno != errno.ENOENT:
695 703 raise
696 704 return self.opener(
697 705 self._indexfile, mode=b"w+", checkambig=self._checkambig
698 706 )
699 707
700 708 def __index_new_fp(self):
701 709 # You should not use this unless you are upgrading from inline revlog
702 710 return self.opener(
703 711 self._indexfile,
704 712 mode=b"w",
705 713 checkambig=self._checkambig,
706 714 atomictemp=True,
707 715 )
708 716
709 717 def _datafp(self, mode=b'r'):
710 718 """file object for the revlog's data file"""
711 719 return self.opener(self._datafile, mode=mode)
712 720
713 721 @contextlib.contextmanager
714 722 def _sidedatareadfp(self):
715 723 """file object suitable to read sidedata"""
716 724 if self._writinghandles:
717 725 yield self._writinghandles[2]
718 726 else:
719 727 with self.opener(self._sidedatafile) as fp:
720 728 yield fp
721 729
722 730 def tiprev(self):
723 731 return len(self.index) - 1
724 732
725 733 def tip(self):
726 734 return self.node(self.tiprev())
727 735
728 736 def __contains__(self, rev):
729 737 return 0 <= rev < len(self)
730 738
731 739 def __len__(self):
732 740 return len(self.index)
733 741
734 742 def __iter__(self):
735 743 return iter(pycompat.xrange(len(self)))
736 744
737 745 def revs(self, start=0, stop=None):
738 746 """iterate over all rev in this revlog (from start to stop)"""
739 747 return storageutil.iterrevs(len(self), start=start, stop=stop)
740 748
741 749 def hasnode(self, node):
742 750 try:
743 751 self.rev(node)
744 752 return True
745 753 except KeyError:
746 754 return False
747 755
748 756 def candelta(self, baserev, rev):
749 757 """whether two revisions (baserev, rev) can be delta-ed or not"""
750 758 # Disable delta if either rev requires a content-changing flag
751 759 # processor (ex. LFS). This is because such flag processor can alter
752 760 # the rawtext content that the delta will be based on, and two clients
753 761 # could have a same revlog node with different flags (i.e. different
754 762 # rawtext contents) and the delta could be incompatible.
755 763 if (self.flags(baserev) & REVIDX_RAWTEXT_CHANGING_FLAGS) or (
756 764 self.flags(rev) & REVIDX_RAWTEXT_CHANGING_FLAGS
757 765 ):
758 766 return False
759 767 return True
760 768
761 769 def update_caches(self, transaction):
762 770 if self._nodemap_file is not None:
763 771 if transaction is None:
764 772 nodemaputil.update_persistent_nodemap(self)
765 773 else:
766 774 nodemaputil.setup_persistent_nodemap(transaction, self)
767 775
768 776 def clearcaches(self):
769 777 self._revisioncache = None
770 778 self._chainbasecache.clear()
771 779 self._segmentfile.clear_cache()
772 780 self._segmentfile_sidedata.clear_cache()
773 781 self._pcache = {}
774 782 self._nodemap_docket = None
775 783 self.index.clearcaches()
776 784 # The python code is the one responsible for validating the docket, we
777 785 # end up having to refresh it here.
778 786 use_nodemap = (
779 787 not self._inline
780 788 and self._nodemap_file is not None
781 789 and util.safehasattr(self.index, 'update_nodemap_data')
782 790 )
783 791 if use_nodemap:
784 792 nodemap_data = nodemaputil.persisted_data(self)
785 793 if nodemap_data is not None:
786 794 self._nodemap_docket = nodemap_data[0]
787 795 self.index.update_nodemap_data(*nodemap_data)
788 796
789 797 def rev(self, node):
790 798 try:
791 799 return self.index.rev(node)
792 800 except TypeError:
793 801 raise
794 802 except error.RevlogError:
795 803 # parsers.c radix tree lookup failed
796 804 if (
797 805 node == self.nodeconstants.wdirid
798 806 or node in self.nodeconstants.wdirfilenodeids
799 807 ):
800 808 raise error.WdirUnsupported
801 809 raise error.LookupError(node, self.display_id, _(b'no node'))
802 810
803 811 # Accessors for index entries.
804 812
805 813 # First tuple entry is 8 bytes. First 6 bytes are offset. Last 2 bytes
806 814 # are flags.
807 815 def start(self, rev):
808 816 return int(self.index[rev][0] >> 16)
809 817
810 818 def sidedata_cut_off(self, rev):
811 819 sd_cut_off = self.index[rev][8]
812 820 if sd_cut_off != 0:
813 821 return sd_cut_off
814 822 # This is some annoying dance, because entries without sidedata
815 823 # currently use 0 as their ofsset. (instead of previous-offset +
816 824 # previous-size)
817 825 #
818 826 # We should reconsider this sidedata β†’ 0 sidata_offset policy.
819 827 # In the meantime, we need this.
820 828 while 0 <= rev:
821 829 e = self.index[rev]
822 830 if e[9] != 0:
823 831 return e[8] + e[9]
824 832 rev -= 1
825 833 return 0
826 834
827 835 def flags(self, rev):
828 836 return self.index[rev][0] & 0xFFFF
829 837
830 838 def length(self, rev):
831 839 return self.index[rev][1]
832 840
833 841 def sidedata_length(self, rev):
834 842 if not self.hassidedata:
835 843 return 0
836 844 return self.index[rev][9]
837 845
838 846 def rawsize(self, rev):
839 847 """return the length of the uncompressed text for a given revision"""
840 848 l = self.index[rev][2]
841 849 if l >= 0:
842 850 return l
843 851
844 852 t = self.rawdata(rev)
845 853 return len(t)
846 854
847 855 def size(self, rev):
848 856 """length of non-raw text (processed by a "read" flag processor)"""
849 857 # fast path: if no "read" flag processor could change the content,
850 858 # size is rawsize. note: ELLIPSIS is known to not change the content.
851 859 flags = self.flags(rev)
852 860 if flags & (flagutil.REVIDX_KNOWN_FLAGS ^ REVIDX_ELLIPSIS) == 0:
853 861 return self.rawsize(rev)
854 862
855 863 return len(self.revision(rev))
856 864
857 865 def fast_rank(self, rev):
858 866 """Return the rank of a revision if already known, or None otherwise.
859 867
860 868 The rank of a revision is the size of the sub-graph it defines as a
861 869 head. Equivalently, the rank of a revision `r` is the size of the set
862 870 `ancestors(r)`, `r` included.
863 871
864 872 This method returns the rank retrieved from the revlog in constant
865 873 time. It makes no attempt at computing unknown values for versions of
866 874 the revlog which do not persist the rank.
867 875 """
868 876 rank = self.index[rev][ENTRY_RANK]
869 877 if self._format_version != CHANGELOGV2 or rank == RANK_UNKNOWN:
870 878 return None
871 879 if rev == nullrev:
872 880 return 0 # convention
873 881 return rank
874 882
875 883 def chainbase(self, rev):
876 884 base = self._chainbasecache.get(rev)
877 885 if base is not None:
878 886 return base
879 887
880 888 index = self.index
881 889 iterrev = rev
882 890 base = index[iterrev][3]
883 891 while base != iterrev:
884 892 iterrev = base
885 893 base = index[iterrev][3]
886 894
887 895 self._chainbasecache[rev] = base
888 896 return base
889 897
890 898 def linkrev(self, rev):
891 899 return self.index[rev][4]
892 900
893 901 def parentrevs(self, rev):
894 902 try:
895 903 entry = self.index[rev]
896 904 except IndexError:
897 905 if rev == wdirrev:
898 906 raise error.WdirUnsupported
899 907 raise
900 908
901 return entry[5], entry[6]
909 if self.canonical_parent_order and entry[5] == nullrev:
910 return entry[6], entry[5]
911 else:
912 return entry[5], entry[6]
902 913
903 914 # fast parentrevs(rev) where rev isn't filtered
904 915 _uncheckedparentrevs = parentrevs
905 916
906 917 def node(self, rev):
907 918 try:
908 919 return self.index[rev][7]
909 920 except IndexError:
910 921 if rev == wdirrev:
911 922 raise error.WdirUnsupported
912 923 raise
913 924
914 925 # Derived from index values.
915 926
916 927 def end(self, rev):
917 928 return self.start(rev) + self.length(rev)
918 929
919 930 def parents(self, node):
920 931 i = self.index
921 932 d = i[self.rev(node)]
922 return i[d[5]][7], i[d[6]][7] # map revisions to nodes inline
933 # inline node() to avoid function call overhead
934 if self.canonical_parent_order and d[5] == self.nullid:
935 return i[d[6]][7], i[d[5]][7]
936 else:
937 return i[d[5]][7], i[d[6]][7]
923 938
924 939 def chainlen(self, rev):
925 940 return self._chaininfo(rev)[0]
926 941
927 942 def _chaininfo(self, rev):
928 943 chaininfocache = self._chaininfocache
929 944 if rev in chaininfocache:
930 945 return chaininfocache[rev]
931 946 index = self.index
932 947 generaldelta = self._generaldelta
933 948 iterrev = rev
934 949 e = index[iterrev]
935 950 clen = 0
936 951 compresseddeltalen = 0
937 952 while iterrev != e[3]:
938 953 clen += 1
939 954 compresseddeltalen += e[1]
940 955 if generaldelta:
941 956 iterrev = e[3]
942 957 else:
943 958 iterrev -= 1
944 959 if iterrev in chaininfocache:
945 960 t = chaininfocache[iterrev]
946 961 clen += t[0]
947 962 compresseddeltalen += t[1]
948 963 break
949 964 e = index[iterrev]
950 965 else:
951 966 # Add text length of base since decompressing that also takes
952 967 # work. For cache hits the length is already included.
953 968 compresseddeltalen += e[1]
954 969 r = (clen, compresseddeltalen)
955 970 chaininfocache[rev] = r
956 971 return r
957 972
958 973 def _deltachain(self, rev, stoprev=None):
959 974 """Obtain the delta chain for a revision.
960 975
961 976 ``stoprev`` specifies a revision to stop at. If not specified, we
962 977 stop at the base of the chain.
963 978
964 979 Returns a 2-tuple of (chain, stopped) where ``chain`` is a list of
965 980 revs in ascending order and ``stopped`` is a bool indicating whether
966 981 ``stoprev`` was hit.
967 982 """
968 983 # Try C implementation.
969 984 try:
970 985 return self.index.deltachain(rev, stoprev, self._generaldelta)
971 986 except AttributeError:
972 987 pass
973 988
974 989 chain = []
975 990
976 991 # Alias to prevent attribute lookup in tight loop.
977 992 index = self.index
978 993 generaldelta = self._generaldelta
979 994
980 995 iterrev = rev
981 996 e = index[iterrev]
982 997 while iterrev != e[3] and iterrev != stoprev:
983 998 chain.append(iterrev)
984 999 if generaldelta:
985 1000 iterrev = e[3]
986 1001 else:
987 1002 iterrev -= 1
988 1003 e = index[iterrev]
989 1004
990 1005 if iterrev == stoprev:
991 1006 stopped = True
992 1007 else:
993 1008 chain.append(iterrev)
994 1009 stopped = False
995 1010
996 1011 chain.reverse()
997 1012 return chain, stopped
998 1013
999 1014 def ancestors(self, revs, stoprev=0, inclusive=False):
1000 1015 """Generate the ancestors of 'revs' in reverse revision order.
1001 1016 Does not generate revs lower than stoprev.
1002 1017
1003 1018 See the documentation for ancestor.lazyancestors for more details."""
1004 1019
1005 1020 # first, make sure start revisions aren't filtered
1006 1021 revs = list(revs)
1007 1022 checkrev = self.node
1008 1023 for r in revs:
1009 1024 checkrev(r)
1010 1025 # and we're sure ancestors aren't filtered as well
1011 1026
1012 1027 if rustancestor is not None and self.index.rust_ext_compat:
1013 1028 lazyancestors = rustancestor.LazyAncestors
1014 1029 arg = self.index
1015 1030 else:
1016 1031 lazyancestors = ancestor.lazyancestors
1017 1032 arg = self._uncheckedparentrevs
1018 1033 return lazyancestors(arg, revs, stoprev=stoprev, inclusive=inclusive)
1019 1034
1020 1035 def descendants(self, revs):
1021 1036 return dagop.descendantrevs(revs, self.revs, self.parentrevs)
1022 1037
1023 1038 def findcommonmissing(self, common=None, heads=None):
1024 1039 """Return a tuple of the ancestors of common and the ancestors of heads
1025 1040 that are not ancestors of common. In revset terminology, we return the
1026 1041 tuple:
1027 1042
1028 1043 ::common, (::heads) - (::common)
1029 1044
1030 1045 The list is sorted by revision number, meaning it is
1031 1046 topologically sorted.
1032 1047
1033 1048 'heads' and 'common' are both lists of node IDs. If heads is
1034 1049 not supplied, uses all of the revlog's heads. If common is not
1035 1050 supplied, uses nullid."""
1036 1051 if common is None:
1037 1052 common = [self.nullid]
1038 1053 if heads is None:
1039 1054 heads = self.heads()
1040 1055
1041 1056 common = [self.rev(n) for n in common]
1042 1057 heads = [self.rev(n) for n in heads]
1043 1058
1044 1059 # we want the ancestors, but inclusive
1045 1060 class lazyset:
1046 1061 def __init__(self, lazyvalues):
1047 1062 self.addedvalues = set()
1048 1063 self.lazyvalues = lazyvalues
1049 1064
1050 1065 def __contains__(self, value):
1051 1066 return value in self.addedvalues or value in self.lazyvalues
1052 1067
1053 1068 def __iter__(self):
1054 1069 added = self.addedvalues
1055 1070 for r in added:
1056 1071 yield r
1057 1072 for r in self.lazyvalues:
1058 1073 if not r in added:
1059 1074 yield r
1060 1075
1061 1076 def add(self, value):
1062 1077 self.addedvalues.add(value)
1063 1078
1064 1079 def update(self, values):
1065 1080 self.addedvalues.update(values)
1066 1081
1067 1082 has = lazyset(self.ancestors(common))
1068 1083 has.add(nullrev)
1069 1084 has.update(common)
1070 1085
1071 1086 # take all ancestors from heads that aren't in has
1072 1087 missing = set()
1073 1088 visit = collections.deque(r for r in heads if r not in has)
1074 1089 while visit:
1075 1090 r = visit.popleft()
1076 1091 if r in missing:
1077 1092 continue
1078 1093 else:
1079 1094 missing.add(r)
1080 1095 for p in self.parentrevs(r):
1081 1096 if p not in has:
1082 1097 visit.append(p)
1083 1098 missing = list(missing)
1084 1099 missing.sort()
1085 1100 return has, [self.node(miss) for miss in missing]
1086 1101
1087 1102 def incrementalmissingrevs(self, common=None):
1088 1103 """Return an object that can be used to incrementally compute the
1089 1104 revision numbers of the ancestors of arbitrary sets that are not
1090 1105 ancestors of common. This is an ancestor.incrementalmissingancestors
1091 1106 object.
1092 1107
1093 1108 'common' is a list of revision numbers. If common is not supplied, uses
1094 1109 nullrev.
1095 1110 """
1096 1111 if common is None:
1097 1112 common = [nullrev]
1098 1113
1099 1114 if rustancestor is not None and self.index.rust_ext_compat:
1100 1115 return rustancestor.MissingAncestors(self.index, common)
1101 1116 return ancestor.incrementalmissingancestors(self.parentrevs, common)
1102 1117
1103 1118 def findmissingrevs(self, common=None, heads=None):
1104 1119 """Return the revision numbers of the ancestors of heads that
1105 1120 are not ancestors of common.
1106 1121
1107 1122 More specifically, return a list of revision numbers corresponding to
1108 1123 nodes N such that every N satisfies the following constraints:
1109 1124
1110 1125 1. N is an ancestor of some node in 'heads'
1111 1126 2. N is not an ancestor of any node in 'common'
1112 1127
1113 1128 The list is sorted by revision number, meaning it is
1114 1129 topologically sorted.
1115 1130
1116 1131 'heads' and 'common' are both lists of revision numbers. If heads is
1117 1132 not supplied, uses all of the revlog's heads. If common is not
1118 1133 supplied, uses nullid."""
1119 1134 if common is None:
1120 1135 common = [nullrev]
1121 1136 if heads is None:
1122 1137 heads = self.headrevs()
1123 1138
1124 1139 inc = self.incrementalmissingrevs(common=common)
1125 1140 return inc.missingancestors(heads)
1126 1141
1127 1142 def findmissing(self, common=None, heads=None):
1128 1143 """Return the ancestors of heads that are not ancestors of common.
1129 1144
1130 1145 More specifically, return a list of nodes N such that every N
1131 1146 satisfies the following constraints:
1132 1147
1133 1148 1. N is an ancestor of some node in 'heads'
1134 1149 2. N is not an ancestor of any node in 'common'
1135 1150
1136 1151 The list is sorted by revision number, meaning it is
1137 1152 topologically sorted.
1138 1153
1139 1154 'heads' and 'common' are both lists of node IDs. If heads is
1140 1155 not supplied, uses all of the revlog's heads. If common is not
1141 1156 supplied, uses nullid."""
1142 1157 if common is None:
1143 1158 common = [self.nullid]
1144 1159 if heads is None:
1145 1160 heads = self.heads()
1146 1161
1147 1162 common = [self.rev(n) for n in common]
1148 1163 heads = [self.rev(n) for n in heads]
1149 1164
1150 1165 inc = self.incrementalmissingrevs(common=common)
1151 1166 return [self.node(r) for r in inc.missingancestors(heads)]
1152 1167
1153 1168 def nodesbetween(self, roots=None, heads=None):
1154 1169 """Return a topological path from 'roots' to 'heads'.
1155 1170
1156 1171 Return a tuple (nodes, outroots, outheads) where 'nodes' is a
1157 1172 topologically sorted list of all nodes N that satisfy both of
1158 1173 these constraints:
1159 1174
1160 1175 1. N is a descendant of some node in 'roots'
1161 1176 2. N is an ancestor of some node in 'heads'
1162 1177
1163 1178 Every node is considered to be both a descendant and an ancestor
1164 1179 of itself, so every reachable node in 'roots' and 'heads' will be
1165 1180 included in 'nodes'.
1166 1181
1167 1182 'outroots' is the list of reachable nodes in 'roots', i.e., the
1168 1183 subset of 'roots' that is returned in 'nodes'. Likewise,
1169 1184 'outheads' is the subset of 'heads' that is also in 'nodes'.
1170 1185
1171 1186 'roots' and 'heads' are both lists of node IDs. If 'roots' is
1172 1187 unspecified, uses nullid as the only root. If 'heads' is
1173 1188 unspecified, uses list of all of the revlog's heads."""
1174 1189 nonodes = ([], [], [])
1175 1190 if roots is not None:
1176 1191 roots = list(roots)
1177 1192 if not roots:
1178 1193 return nonodes
1179 1194 lowestrev = min([self.rev(n) for n in roots])
1180 1195 else:
1181 1196 roots = [self.nullid] # Everybody's a descendant of nullid
1182 1197 lowestrev = nullrev
1183 1198 if (lowestrev == nullrev) and (heads is None):
1184 1199 # We want _all_ the nodes!
1185 1200 return (
1186 1201 [self.node(r) for r in self],
1187 1202 [self.nullid],
1188 1203 list(self.heads()),
1189 1204 )
1190 1205 if heads is None:
1191 1206 # All nodes are ancestors, so the latest ancestor is the last
1192 1207 # node.
1193 1208 highestrev = len(self) - 1
1194 1209 # Set ancestors to None to signal that every node is an ancestor.
1195 1210 ancestors = None
1196 1211 # Set heads to an empty dictionary for later discovery of heads
1197 1212 heads = {}
1198 1213 else:
1199 1214 heads = list(heads)
1200 1215 if not heads:
1201 1216 return nonodes
1202 1217 ancestors = set()
1203 1218 # Turn heads into a dictionary so we can remove 'fake' heads.
1204 1219 # Also, later we will be using it to filter out the heads we can't
1205 1220 # find from roots.
1206 1221 heads = dict.fromkeys(heads, False)
1207 1222 # Start at the top and keep marking parents until we're done.
1208 1223 nodestotag = set(heads)
1209 1224 # Remember where the top was so we can use it as a limit later.
1210 1225 highestrev = max([self.rev(n) for n in nodestotag])
1211 1226 while nodestotag:
1212 1227 # grab a node to tag
1213 1228 n = nodestotag.pop()
1214 1229 # Never tag nullid
1215 1230 if n == self.nullid:
1216 1231 continue
1217 1232 # A node's revision number represents its place in a
1218 1233 # topologically sorted list of nodes.
1219 1234 r = self.rev(n)
1220 1235 if r >= lowestrev:
1221 1236 if n not in ancestors:
1222 1237 # If we are possibly a descendant of one of the roots
1223 1238 # and we haven't already been marked as an ancestor
1224 1239 ancestors.add(n) # Mark as ancestor
1225 1240 # Add non-nullid parents to list of nodes to tag.
1226 1241 nodestotag.update(
1227 1242 [p for p in self.parents(n) if p != self.nullid]
1228 1243 )
1229 1244 elif n in heads: # We've seen it before, is it a fake head?
1230 1245 # So it is, real heads should not be the ancestors of
1231 1246 # any other heads.
1232 1247 heads.pop(n)
1233 1248 if not ancestors:
1234 1249 return nonodes
1235 1250 # Now that we have our set of ancestors, we want to remove any
1236 1251 # roots that are not ancestors.
1237 1252
1238 1253 # If one of the roots was nullid, everything is included anyway.
1239 1254 if lowestrev > nullrev:
1240 1255 # But, since we weren't, let's recompute the lowest rev to not
1241 1256 # include roots that aren't ancestors.
1242 1257
1243 1258 # Filter out roots that aren't ancestors of heads
1244 1259 roots = [root for root in roots if root in ancestors]
1245 1260 # Recompute the lowest revision
1246 1261 if roots:
1247 1262 lowestrev = min([self.rev(root) for root in roots])
1248 1263 else:
1249 1264 # No more roots? Return empty list
1250 1265 return nonodes
1251 1266 else:
1252 1267 # We are descending from nullid, and don't need to care about
1253 1268 # any other roots.
1254 1269 lowestrev = nullrev
1255 1270 roots = [self.nullid]
1256 1271 # Transform our roots list into a set.
1257 1272 descendants = set(roots)
1258 1273 # Also, keep the original roots so we can filter out roots that aren't
1259 1274 # 'real' roots (i.e. are descended from other roots).
1260 1275 roots = descendants.copy()
1261 1276 # Our topologically sorted list of output nodes.
1262 1277 orderedout = []
1263 1278 # Don't start at nullid since we don't want nullid in our output list,
1264 1279 # and if nullid shows up in descendants, empty parents will look like
1265 1280 # they're descendants.
1266 1281 for r in self.revs(start=max(lowestrev, 0), stop=highestrev + 1):
1267 1282 n = self.node(r)
1268 1283 isdescendant = False
1269 1284 if lowestrev == nullrev: # Everybody is a descendant of nullid
1270 1285 isdescendant = True
1271 1286 elif n in descendants:
1272 1287 # n is already a descendant
1273 1288 isdescendant = True
1274 1289 # This check only needs to be done here because all the roots
1275 1290 # will start being marked is descendants before the loop.
1276 1291 if n in roots:
1277 1292 # If n was a root, check if it's a 'real' root.
1278 1293 p = tuple(self.parents(n))
1279 1294 # If any of its parents are descendants, it's not a root.
1280 1295 if (p[0] in descendants) or (p[1] in descendants):
1281 1296 roots.remove(n)
1282 1297 else:
1283 1298 p = tuple(self.parents(n))
1284 1299 # A node is a descendant if either of its parents are
1285 1300 # descendants. (We seeded the dependents list with the roots
1286 1301 # up there, remember?)
1287 1302 if (p[0] in descendants) or (p[1] in descendants):
1288 1303 descendants.add(n)
1289 1304 isdescendant = True
1290 1305 if isdescendant and ((ancestors is None) or (n in ancestors)):
1291 1306 # Only include nodes that are both descendants and ancestors.
1292 1307 orderedout.append(n)
1293 1308 if (ancestors is not None) and (n in heads):
1294 1309 # We're trying to figure out which heads are reachable
1295 1310 # from roots.
1296 1311 # Mark this head as having been reached
1297 1312 heads[n] = True
1298 1313 elif ancestors is None:
1299 1314 # Otherwise, we're trying to discover the heads.
1300 1315 # Assume this is a head because if it isn't, the next step
1301 1316 # will eventually remove it.
1302 1317 heads[n] = True
1303 1318 # But, obviously its parents aren't.
1304 1319 for p in self.parents(n):
1305 1320 heads.pop(p, None)
1306 1321 heads = [head for head, flag in heads.items() if flag]
1307 1322 roots = list(roots)
1308 1323 assert orderedout
1309 1324 assert roots
1310 1325 assert heads
1311 1326 return (orderedout, roots, heads)
1312 1327
1313 1328 def headrevs(self, revs=None):
1314 1329 if revs is None:
1315 1330 try:
1316 1331 return self.index.headrevs()
1317 1332 except AttributeError:
1318 1333 return self._headrevs()
1319 1334 if rustdagop is not None and self.index.rust_ext_compat:
1320 1335 return rustdagop.headrevs(self.index, revs)
1321 1336 return dagop.headrevs(revs, self._uncheckedparentrevs)
1322 1337
1323 1338 def computephases(self, roots):
1324 1339 return self.index.computephasesmapsets(roots)
1325 1340
1326 1341 def _headrevs(self):
1327 1342 count = len(self)
1328 1343 if not count:
1329 1344 return [nullrev]
1330 1345 # we won't iter over filtered rev so nobody is a head at start
1331 1346 ishead = [0] * (count + 1)
1332 1347 index = self.index
1333 1348 for r in self:
1334 1349 ishead[r] = 1 # I may be an head
1335 1350 e = index[r]
1336 1351 ishead[e[5]] = ishead[e[6]] = 0 # my parent are not
1337 1352 return [r for r, val in enumerate(ishead) if val]
1338 1353
1339 1354 def heads(self, start=None, stop=None):
1340 1355 """return the list of all nodes that have no children
1341 1356
1342 1357 if start is specified, only heads that are descendants of
1343 1358 start will be returned
1344 1359 if stop is specified, it will consider all the revs from stop
1345 1360 as if they had no children
1346 1361 """
1347 1362 if start is None and stop is None:
1348 1363 if not len(self):
1349 1364 return [self.nullid]
1350 1365 return [self.node(r) for r in self.headrevs()]
1351 1366
1352 1367 if start is None:
1353 1368 start = nullrev
1354 1369 else:
1355 1370 start = self.rev(start)
1356 1371
1357 1372 stoprevs = {self.rev(n) for n in stop or []}
1358 1373
1359 1374 revs = dagop.headrevssubset(
1360 1375 self.revs, self.parentrevs, startrev=start, stoprevs=stoprevs
1361 1376 )
1362 1377
1363 1378 return [self.node(rev) for rev in revs]
1364 1379
1365 1380 def children(self, node):
1366 1381 """find the children of a given node"""
1367 1382 c = []
1368 1383 p = self.rev(node)
1369 1384 for r in self.revs(start=p + 1):
1370 1385 prevs = [pr for pr in self.parentrevs(r) if pr != nullrev]
1371 1386 if prevs:
1372 1387 for pr in prevs:
1373 1388 if pr == p:
1374 1389 c.append(self.node(r))
1375 1390 elif p == nullrev:
1376 1391 c.append(self.node(r))
1377 1392 return c
1378 1393
1379 1394 def commonancestorsheads(self, a, b):
1380 1395 """calculate all the heads of the common ancestors of nodes a and b"""
1381 1396 a, b = self.rev(a), self.rev(b)
1382 1397 ancs = self._commonancestorsheads(a, b)
1383 1398 return pycompat.maplist(self.node, ancs)
1384 1399
1385 1400 def _commonancestorsheads(self, *revs):
1386 1401 """calculate all the heads of the common ancestors of revs"""
1387 1402 try:
1388 1403 ancs = self.index.commonancestorsheads(*revs)
1389 1404 except (AttributeError, OverflowError): # C implementation failed
1390 1405 ancs = ancestor.commonancestorsheads(self.parentrevs, *revs)
1391 1406 return ancs
1392 1407
1393 1408 def isancestor(self, a, b):
1394 1409 """return True if node a is an ancestor of node b
1395 1410
1396 1411 A revision is considered an ancestor of itself."""
1397 1412 a, b = self.rev(a), self.rev(b)
1398 1413 return self.isancestorrev(a, b)
1399 1414
1400 1415 def isancestorrev(self, a, b):
1401 1416 """return True if revision a is an ancestor of revision b
1402 1417
1403 1418 A revision is considered an ancestor of itself.
1404 1419
1405 1420 The implementation of this is trivial but the use of
1406 1421 reachableroots is not."""
1407 1422 if a == nullrev:
1408 1423 return True
1409 1424 elif a == b:
1410 1425 return True
1411 1426 elif a > b:
1412 1427 return False
1413 1428 return bool(self.reachableroots(a, [b], [a], includepath=False))
1414 1429
1415 1430 def reachableroots(self, minroot, heads, roots, includepath=False):
1416 1431 """return (heads(::(<roots> and <roots>::<heads>)))
1417 1432
1418 1433 If includepath is True, return (<roots>::<heads>)."""
1419 1434 try:
1420 1435 return self.index.reachableroots2(
1421 1436 minroot, heads, roots, includepath
1422 1437 )
1423 1438 except AttributeError:
1424 1439 return dagop._reachablerootspure(
1425 1440 self.parentrevs, minroot, roots, heads, includepath
1426 1441 )
1427 1442
1428 1443 def ancestor(self, a, b):
1429 1444 """calculate the "best" common ancestor of nodes a and b"""
1430 1445
1431 1446 a, b = self.rev(a), self.rev(b)
1432 1447 try:
1433 1448 ancs = self.index.ancestors(a, b)
1434 1449 except (AttributeError, OverflowError):
1435 1450 ancs = ancestor.ancestors(self.parentrevs, a, b)
1436 1451 if ancs:
1437 1452 # choose a consistent winner when there's a tie
1438 1453 return min(map(self.node, ancs))
1439 1454 return self.nullid
1440 1455
1441 1456 def _match(self, id):
1442 1457 if isinstance(id, int):
1443 1458 # rev
1444 1459 return self.node(id)
1445 1460 if len(id) == self.nodeconstants.nodelen:
1446 1461 # possibly a binary node
1447 1462 # odds of a binary node being all hex in ASCII are 1 in 10**25
1448 1463 try:
1449 1464 node = id
1450 1465 self.rev(node) # quick search the index
1451 1466 return node
1452 1467 except error.LookupError:
1453 1468 pass # may be partial hex id
1454 1469 try:
1455 1470 # str(rev)
1456 1471 rev = int(id)
1457 1472 if b"%d" % rev != id:
1458 1473 raise ValueError
1459 1474 if rev < 0:
1460 1475 rev = len(self) + rev
1461 1476 if rev < 0 or rev >= len(self):
1462 1477 raise ValueError
1463 1478 return self.node(rev)
1464 1479 except (ValueError, OverflowError):
1465 1480 pass
1466 1481 if len(id) == 2 * self.nodeconstants.nodelen:
1467 1482 try:
1468 1483 # a full hex nodeid?
1469 1484 node = bin(id)
1470 1485 self.rev(node)
1471 1486 return node
1472 1487 except (TypeError, error.LookupError):
1473 1488 pass
1474 1489
1475 1490 def _partialmatch(self, id):
1476 1491 # we don't care wdirfilenodeids as they should be always full hash
1477 1492 maybewdir = self.nodeconstants.wdirhex.startswith(id)
1478 1493 ambiguous = False
1479 1494 try:
1480 1495 partial = self.index.partialmatch(id)
1481 1496 if partial and self.hasnode(partial):
1482 1497 if maybewdir:
1483 1498 # single 'ff...' match in radix tree, ambiguous with wdir
1484 1499 ambiguous = True
1485 1500 else:
1486 1501 return partial
1487 1502 elif maybewdir:
1488 1503 # no 'ff...' match in radix tree, wdir identified
1489 1504 raise error.WdirUnsupported
1490 1505 else:
1491 1506 return None
1492 1507 except error.RevlogError:
1493 1508 # parsers.c radix tree lookup gave multiple matches
1494 1509 # fast path: for unfiltered changelog, radix tree is accurate
1495 1510 if not getattr(self, 'filteredrevs', None):
1496 1511 ambiguous = True
1497 1512 # fall through to slow path that filters hidden revisions
1498 1513 except (AttributeError, ValueError):
1499 1514 # we are pure python, or key was too short to search radix tree
1500 1515 pass
1501 1516 if ambiguous:
1502 1517 raise error.AmbiguousPrefixLookupError(
1503 1518 id, self.display_id, _(b'ambiguous identifier')
1504 1519 )
1505 1520
1506 1521 if id in self._pcache:
1507 1522 return self._pcache[id]
1508 1523
1509 1524 if len(id) <= 40:
1510 1525 try:
1511 1526 # hex(node)[:...]
1512 1527 l = len(id) // 2 # grab an even number of digits
1513 1528 prefix = bin(id[: l * 2])
1514 1529 nl = [e[7] for e in self.index if e[7].startswith(prefix)]
1515 1530 nl = [
1516 1531 n for n in nl if hex(n).startswith(id) and self.hasnode(n)
1517 1532 ]
1518 1533 if self.nodeconstants.nullhex.startswith(id):
1519 1534 nl.append(self.nullid)
1520 1535 if len(nl) > 0:
1521 1536 if len(nl) == 1 and not maybewdir:
1522 1537 self._pcache[id] = nl[0]
1523 1538 return nl[0]
1524 1539 raise error.AmbiguousPrefixLookupError(
1525 1540 id, self.display_id, _(b'ambiguous identifier')
1526 1541 )
1527 1542 if maybewdir:
1528 1543 raise error.WdirUnsupported
1529 1544 return None
1530 1545 except TypeError:
1531 1546 pass
1532 1547
1533 1548 def lookup(self, id):
1534 1549 """locate a node based on:
1535 1550 - revision number or str(revision number)
1536 1551 - nodeid or subset of hex nodeid
1537 1552 """
1538 1553 n = self._match(id)
1539 1554 if n is not None:
1540 1555 return n
1541 1556 n = self._partialmatch(id)
1542 1557 if n:
1543 1558 return n
1544 1559
1545 1560 raise error.LookupError(id, self.display_id, _(b'no match found'))
1546 1561
1547 1562 def shortest(self, node, minlength=1):
1548 1563 """Find the shortest unambiguous prefix that matches node."""
1549 1564
1550 1565 def isvalid(prefix):
1551 1566 try:
1552 1567 matchednode = self._partialmatch(prefix)
1553 1568 except error.AmbiguousPrefixLookupError:
1554 1569 return False
1555 1570 except error.WdirUnsupported:
1556 1571 # single 'ff...' match
1557 1572 return True
1558 1573 if matchednode is None:
1559 1574 raise error.LookupError(node, self.display_id, _(b'no node'))
1560 1575 return True
1561 1576
1562 1577 def maybewdir(prefix):
1563 1578 return all(c == b'f' for c in pycompat.iterbytestr(prefix))
1564 1579
1565 1580 hexnode = hex(node)
1566 1581
1567 1582 def disambiguate(hexnode, minlength):
1568 1583 """Disambiguate against wdirid."""
1569 1584 for length in range(minlength, len(hexnode) + 1):
1570 1585 prefix = hexnode[:length]
1571 1586 if not maybewdir(prefix):
1572 1587 return prefix
1573 1588
1574 1589 if not getattr(self, 'filteredrevs', None):
1575 1590 try:
1576 1591 length = max(self.index.shortest(node), minlength)
1577 1592 return disambiguate(hexnode, length)
1578 1593 except error.RevlogError:
1579 1594 if node != self.nodeconstants.wdirid:
1580 1595 raise error.LookupError(
1581 1596 node, self.display_id, _(b'no node')
1582 1597 )
1583 1598 except AttributeError:
1584 1599 # Fall through to pure code
1585 1600 pass
1586 1601
1587 1602 if node == self.nodeconstants.wdirid:
1588 1603 for length in range(minlength, len(hexnode) + 1):
1589 1604 prefix = hexnode[:length]
1590 1605 if isvalid(prefix):
1591 1606 return prefix
1592 1607
1593 1608 for length in range(minlength, len(hexnode) + 1):
1594 1609 prefix = hexnode[:length]
1595 1610 if isvalid(prefix):
1596 1611 return disambiguate(hexnode, length)
1597 1612
1598 1613 def cmp(self, node, text):
1599 1614 """compare text with a given file revision
1600 1615
1601 1616 returns True if text is different than what is stored.
1602 1617 """
1603 1618 p1, p2 = self.parents(node)
1604 1619 return storageutil.hashrevisionsha1(text, p1, p2) != node
1605 1620
1606 1621 def _getsegmentforrevs(self, startrev, endrev, df=None):
1607 1622 """Obtain a segment of raw data corresponding to a range of revisions.
1608 1623
1609 1624 Accepts the start and end revisions and an optional already-open
1610 1625 file handle to be used for reading. If the file handle is read, its
1611 1626 seek position will not be preserved.
1612 1627
1613 1628 Requests for data may be satisfied by a cache.
1614 1629
1615 1630 Returns a 2-tuple of (offset, data) for the requested range of
1616 1631 revisions. Offset is the integer offset from the beginning of the
1617 1632 revlog and data is a str or buffer of the raw byte data.
1618 1633
1619 1634 Callers will need to call ``self.start(rev)`` and ``self.length(rev)``
1620 1635 to determine where each revision's data begins and ends.
1621 1636 """
1622 1637 # Inlined self.start(startrev) & self.end(endrev) for perf reasons
1623 1638 # (functions are expensive).
1624 1639 index = self.index
1625 1640 istart = index[startrev]
1626 1641 start = int(istart[0] >> 16)
1627 1642 if startrev == endrev:
1628 1643 end = start + istart[1]
1629 1644 else:
1630 1645 iend = index[endrev]
1631 1646 end = int(iend[0] >> 16) + iend[1]
1632 1647
1633 1648 if self._inline:
1634 1649 start += (startrev + 1) * self.index.entry_size
1635 1650 end += (endrev + 1) * self.index.entry_size
1636 1651 length = end - start
1637 1652
1638 1653 return start, self._segmentfile.read_chunk(start, length, df)
1639 1654
1640 1655 def _chunk(self, rev, df=None):
1641 1656 """Obtain a single decompressed chunk for a revision.
1642 1657
1643 1658 Accepts an integer revision and an optional already-open file handle
1644 1659 to be used for reading. If used, the seek position of the file will not
1645 1660 be preserved.
1646 1661
1647 1662 Returns a str holding uncompressed data for the requested revision.
1648 1663 """
1649 1664 compression_mode = self.index[rev][10]
1650 1665 data = self._getsegmentforrevs(rev, rev, df=df)[1]
1651 1666 if compression_mode == COMP_MODE_PLAIN:
1652 1667 return data
1653 1668 elif compression_mode == COMP_MODE_DEFAULT:
1654 1669 return self._decompressor(data)
1655 1670 elif compression_mode == COMP_MODE_INLINE:
1656 1671 return self.decompress(data)
1657 1672 else:
1658 1673 msg = b'unknown compression mode %d'
1659 1674 msg %= compression_mode
1660 1675 raise error.RevlogError(msg)
1661 1676
1662 1677 def _chunks(self, revs, df=None, targetsize=None):
1663 1678 """Obtain decompressed chunks for the specified revisions.
1664 1679
1665 1680 Accepts an iterable of numeric revisions that are assumed to be in
1666 1681 ascending order. Also accepts an optional already-open file handle
1667 1682 to be used for reading. If used, the seek position of the file will
1668 1683 not be preserved.
1669 1684
1670 1685 This function is similar to calling ``self._chunk()`` multiple times,
1671 1686 but is faster.
1672 1687
1673 1688 Returns a list with decompressed data for each requested revision.
1674 1689 """
1675 1690 if not revs:
1676 1691 return []
1677 1692 start = self.start
1678 1693 length = self.length
1679 1694 inline = self._inline
1680 1695 iosize = self.index.entry_size
1681 1696 buffer = util.buffer
1682 1697
1683 1698 l = []
1684 1699 ladd = l.append
1685 1700
1686 1701 if not self._withsparseread:
1687 1702 slicedchunks = (revs,)
1688 1703 else:
1689 1704 slicedchunks = deltautil.slicechunk(
1690 1705 self, revs, targetsize=targetsize
1691 1706 )
1692 1707
1693 1708 for revschunk in slicedchunks:
1694 1709 firstrev = revschunk[0]
1695 1710 # Skip trailing revisions with empty diff
1696 1711 for lastrev in revschunk[::-1]:
1697 1712 if length(lastrev) != 0:
1698 1713 break
1699 1714
1700 1715 try:
1701 1716 offset, data = self._getsegmentforrevs(firstrev, lastrev, df=df)
1702 1717 except OverflowError:
1703 1718 # issue4215 - we can't cache a run of chunks greater than
1704 1719 # 2G on Windows
1705 1720 return [self._chunk(rev, df=df) for rev in revschunk]
1706 1721
1707 1722 decomp = self.decompress
1708 1723 # self._decompressor might be None, but will not be used in that case
1709 1724 def_decomp = self._decompressor
1710 1725 for rev in revschunk:
1711 1726 chunkstart = start(rev)
1712 1727 if inline:
1713 1728 chunkstart += (rev + 1) * iosize
1714 1729 chunklength = length(rev)
1715 1730 comp_mode = self.index[rev][10]
1716 1731 c = buffer(data, chunkstart - offset, chunklength)
1717 1732 if comp_mode == COMP_MODE_PLAIN:
1718 1733 ladd(c)
1719 1734 elif comp_mode == COMP_MODE_INLINE:
1720 1735 ladd(decomp(c))
1721 1736 elif comp_mode == COMP_MODE_DEFAULT:
1722 1737 ladd(def_decomp(c))
1723 1738 else:
1724 1739 msg = b'unknown compression mode %d'
1725 1740 msg %= comp_mode
1726 1741 raise error.RevlogError(msg)
1727 1742
1728 1743 return l
1729 1744
1730 1745 def deltaparent(self, rev):
1731 1746 """return deltaparent of the given revision"""
1732 1747 base = self.index[rev][3]
1733 1748 if base == rev:
1734 1749 return nullrev
1735 1750 elif self._generaldelta:
1736 1751 return base
1737 1752 else:
1738 1753 return rev - 1
1739 1754
1740 1755 def issnapshot(self, rev):
1741 1756 """tells whether rev is a snapshot"""
1742 1757 if not self._sparserevlog:
1743 1758 return self.deltaparent(rev) == nullrev
1744 1759 elif util.safehasattr(self.index, b'issnapshot'):
1745 1760 # directly assign the method to cache the testing and access
1746 1761 self.issnapshot = self.index.issnapshot
1747 1762 return self.issnapshot(rev)
1748 1763 if rev == nullrev:
1749 1764 return True
1750 1765 entry = self.index[rev]
1751 1766 base = entry[3]
1752 1767 if base == rev:
1753 1768 return True
1754 1769 if base == nullrev:
1755 1770 return True
1756 1771 p1 = entry[5]
1757 1772 p2 = entry[6]
1758 1773 if base == p1 or base == p2:
1759 1774 return False
1760 1775 return self.issnapshot(base)
1761 1776
1762 1777 def snapshotdepth(self, rev):
1763 1778 """number of snapshot in the chain before this one"""
1764 1779 if not self.issnapshot(rev):
1765 1780 raise error.ProgrammingError(b'revision %d not a snapshot')
1766 1781 return len(self._deltachain(rev)[0]) - 1
1767 1782
1768 1783 def revdiff(self, rev1, rev2):
1769 1784 """return or calculate a delta between two revisions
1770 1785
1771 1786 The delta calculated is in binary form and is intended to be written to
1772 1787 revlog data directly. So this function needs raw revision data.
1773 1788 """
1774 1789 if rev1 != nullrev and self.deltaparent(rev2) == rev1:
1775 1790 return bytes(self._chunk(rev2))
1776 1791
1777 1792 return mdiff.textdiff(self.rawdata(rev1), self.rawdata(rev2))
1778 1793
1779 1794 def revision(self, nodeorrev, _df=None):
1780 1795 """return an uncompressed revision of a given node or revision
1781 1796 number.
1782 1797
1783 1798 _df - an existing file handle to read from. (internal-only)
1784 1799 """
1785 1800 return self._revisiondata(nodeorrev, _df)
1786 1801
1787 1802 def sidedata(self, nodeorrev, _df=None):
1788 1803 """a map of extra data related to the changeset but not part of the hash
1789 1804
1790 1805 This function currently return a dictionary. However, more advanced
1791 1806 mapping object will likely be used in the future for a more
1792 1807 efficient/lazy code.
1793 1808 """
1794 1809 # deal with <nodeorrev> argument type
1795 1810 if isinstance(nodeorrev, int):
1796 1811 rev = nodeorrev
1797 1812 else:
1798 1813 rev = self.rev(nodeorrev)
1799 1814 return self._sidedata(rev)
1800 1815
1801 1816 def _revisiondata(self, nodeorrev, _df=None, raw=False):
1802 1817 # deal with <nodeorrev> argument type
1803 1818 if isinstance(nodeorrev, int):
1804 1819 rev = nodeorrev
1805 1820 node = self.node(rev)
1806 1821 else:
1807 1822 node = nodeorrev
1808 1823 rev = None
1809 1824
1810 1825 # fast path the special `nullid` rev
1811 1826 if node == self.nullid:
1812 1827 return b""
1813 1828
1814 1829 # ``rawtext`` is the text as stored inside the revlog. Might be the
1815 1830 # revision or might need to be processed to retrieve the revision.
1816 1831 rev, rawtext, validated = self._rawtext(node, rev, _df=_df)
1817 1832
1818 1833 if raw and validated:
1819 1834 # if we don't want to process the raw text and that raw
1820 1835 # text is cached, we can exit early.
1821 1836 return rawtext
1822 1837 if rev is None:
1823 1838 rev = self.rev(node)
1824 1839 # the revlog's flag for this revision
1825 1840 # (usually alter its state or content)
1826 1841 flags = self.flags(rev)
1827 1842
1828 1843 if validated and flags == REVIDX_DEFAULT_FLAGS:
1829 1844 # no extra flags set, no flag processor runs, text = rawtext
1830 1845 return rawtext
1831 1846
1832 1847 if raw:
1833 1848 validatehash = flagutil.processflagsraw(self, rawtext, flags)
1834 1849 text = rawtext
1835 1850 else:
1836 1851 r = flagutil.processflagsread(self, rawtext, flags)
1837 1852 text, validatehash = r
1838 1853 if validatehash:
1839 1854 self.checkhash(text, node, rev=rev)
1840 1855 if not validated:
1841 1856 self._revisioncache = (node, rev, rawtext)
1842 1857
1843 1858 return text
1844 1859
1845 1860 def _rawtext(self, node, rev, _df=None):
1846 1861 """return the possibly unvalidated rawtext for a revision
1847 1862
1848 1863 returns (rev, rawtext, validated)
1849 1864 """
1850 1865
1851 1866 # revision in the cache (could be useful to apply delta)
1852 1867 cachedrev = None
1853 1868 # An intermediate text to apply deltas to
1854 1869 basetext = None
1855 1870
1856 1871 # Check if we have the entry in cache
1857 1872 # The cache entry looks like (node, rev, rawtext)
1858 1873 if self._revisioncache:
1859 1874 if self._revisioncache[0] == node:
1860 1875 return (rev, self._revisioncache[2], True)
1861 1876 cachedrev = self._revisioncache[1]
1862 1877
1863 1878 if rev is None:
1864 1879 rev = self.rev(node)
1865 1880
1866 1881 chain, stopped = self._deltachain(rev, stoprev=cachedrev)
1867 1882 if stopped:
1868 1883 basetext = self._revisioncache[2]
1869 1884
1870 1885 # drop cache to save memory, the caller is expected to
1871 1886 # update self._revisioncache after validating the text
1872 1887 self._revisioncache = None
1873 1888
1874 1889 targetsize = None
1875 1890 rawsize = self.index[rev][2]
1876 1891 if 0 <= rawsize:
1877 1892 targetsize = 4 * rawsize
1878 1893
1879 1894 bins = self._chunks(chain, df=_df, targetsize=targetsize)
1880 1895 if basetext is None:
1881 1896 basetext = bytes(bins[0])
1882 1897 bins = bins[1:]
1883 1898
1884 1899 rawtext = mdiff.patches(basetext, bins)
1885 1900 del basetext # let us have a chance to free memory early
1886 1901 return (rev, rawtext, False)
1887 1902
1888 1903 def _sidedata(self, rev):
1889 1904 """Return the sidedata for a given revision number."""
1890 1905 index_entry = self.index[rev]
1891 1906 sidedata_offset = index_entry[8]
1892 1907 sidedata_size = index_entry[9]
1893 1908
1894 1909 if self._inline:
1895 1910 sidedata_offset += self.index.entry_size * (1 + rev)
1896 1911 if sidedata_size == 0:
1897 1912 return {}
1898 1913
1899 1914 if self._docket.sidedata_end < sidedata_offset + sidedata_size:
1900 1915 filename = self._sidedatafile
1901 1916 end = self._docket.sidedata_end
1902 1917 offset = sidedata_offset
1903 1918 length = sidedata_size
1904 1919 m = FILE_TOO_SHORT_MSG % (filename, length, offset, end)
1905 1920 raise error.RevlogError(m)
1906 1921
1907 1922 comp_segment = self._segmentfile_sidedata.read_chunk(
1908 1923 sidedata_offset, sidedata_size
1909 1924 )
1910 1925
1911 1926 comp = self.index[rev][11]
1912 1927 if comp == COMP_MODE_PLAIN:
1913 1928 segment = comp_segment
1914 1929 elif comp == COMP_MODE_DEFAULT:
1915 1930 segment = self._decompressor(comp_segment)
1916 1931 elif comp == COMP_MODE_INLINE:
1917 1932 segment = self.decompress(comp_segment)
1918 1933 else:
1919 1934 msg = b'unknown compression mode %d'
1920 1935 msg %= comp
1921 1936 raise error.RevlogError(msg)
1922 1937
1923 1938 sidedata = sidedatautil.deserialize_sidedata(segment)
1924 1939 return sidedata
1925 1940
1926 1941 def rawdata(self, nodeorrev, _df=None):
1927 1942 """return an uncompressed raw data of a given node or revision number.
1928 1943
1929 1944 _df - an existing file handle to read from. (internal-only)
1930 1945 """
1931 1946 return self._revisiondata(nodeorrev, _df, raw=True)
1932 1947
1933 1948 def hash(self, text, p1, p2):
1934 1949 """Compute a node hash.
1935 1950
1936 1951 Available as a function so that subclasses can replace the hash
1937 1952 as needed.
1938 1953 """
1939 1954 return storageutil.hashrevisionsha1(text, p1, p2)
1940 1955
1941 1956 def checkhash(self, text, node, p1=None, p2=None, rev=None):
1942 1957 """Check node hash integrity.
1943 1958
1944 1959 Available as a function so that subclasses can extend hash mismatch
1945 1960 behaviors as needed.
1946 1961 """
1947 1962 try:
1948 1963 if p1 is None and p2 is None:
1949 1964 p1, p2 = self.parents(node)
1950 1965 if node != self.hash(text, p1, p2):
1951 1966 # Clear the revision cache on hash failure. The revision cache
1952 1967 # only stores the raw revision and clearing the cache does have
1953 1968 # the side-effect that we won't have a cache hit when the raw
1954 1969 # revision data is accessed. But this case should be rare and
1955 1970 # it is extra work to teach the cache about the hash
1956 1971 # verification state.
1957 1972 if self._revisioncache and self._revisioncache[0] == node:
1958 1973 self._revisioncache = None
1959 1974
1960 1975 revornode = rev
1961 1976 if revornode is None:
1962 1977 revornode = templatefilters.short(hex(node))
1963 1978 raise error.RevlogError(
1964 1979 _(b"integrity check failed on %s:%s")
1965 1980 % (self.display_id, pycompat.bytestr(revornode))
1966 1981 )
1967 1982 except error.RevlogError:
1968 1983 if self._censorable and storageutil.iscensoredtext(text):
1969 1984 raise error.CensoredNodeError(self.display_id, node, text)
1970 1985 raise
1971 1986
1972 1987 def _enforceinlinesize(self, tr):
1973 1988 """Check if the revlog is too big for inline and convert if so.
1974 1989
1975 1990 This should be called after revisions are added to the revlog. If the
1976 1991 revlog has grown too large to be an inline revlog, it will convert it
1977 1992 to use multiple index and data files.
1978 1993 """
1979 1994 tiprev = len(self) - 1
1980 1995 total_size = self.start(tiprev) + self.length(tiprev)
1981 1996 if not self._inline or total_size < _maxinline:
1982 1997 return
1983 1998
1984 1999 troffset = tr.findoffset(self._indexfile)
1985 2000 if troffset is None:
1986 2001 raise error.RevlogError(
1987 2002 _(b"%s not found in the transaction") % self._indexfile
1988 2003 )
1989 2004 trindex = None
1990 2005 tr.add(self._datafile, 0)
1991 2006
1992 2007 existing_handles = False
1993 2008 if self._writinghandles is not None:
1994 2009 existing_handles = True
1995 2010 fp = self._writinghandles[0]
1996 2011 fp.flush()
1997 2012 fp.close()
1998 2013 # We can't use the cached file handle after close(). So prevent
1999 2014 # its usage.
2000 2015 self._writinghandles = None
2001 2016 self._segmentfile.writing_handle = None
2002 2017 # No need to deal with sidedata writing handle as it is only
2003 2018 # relevant with revlog-v2 which is never inline, not reaching
2004 2019 # this code
2005 2020
2006 2021 new_dfh = self._datafp(b'w+')
2007 2022 new_dfh.truncate(0) # drop any potentially existing data
2008 2023 try:
2009 2024 with self._indexfp() as read_ifh:
2010 2025 for r in self:
2011 2026 new_dfh.write(self._getsegmentforrevs(r, r, df=read_ifh)[1])
2012 2027 if (
2013 2028 trindex is None
2014 2029 and troffset
2015 2030 <= self.start(r) + r * self.index.entry_size
2016 2031 ):
2017 2032 trindex = r
2018 2033 new_dfh.flush()
2019 2034
2020 2035 if trindex is None:
2021 2036 trindex = 0
2022 2037
2023 2038 with self.__index_new_fp() as fp:
2024 2039 self._format_flags &= ~FLAG_INLINE_DATA
2025 2040 self._inline = False
2026 2041 for i in self:
2027 2042 e = self.index.entry_binary(i)
2028 2043 if i == 0 and self._docket is None:
2029 2044 header = self._format_flags | self._format_version
2030 2045 header = self.index.pack_header(header)
2031 2046 e = header + e
2032 2047 fp.write(e)
2033 2048 if self._docket is not None:
2034 2049 self._docket.index_end = fp.tell()
2035 2050
2036 2051 # There is a small transactional race here. If the rename of
2037 2052 # the index fails, we should remove the datafile. It is more
2038 2053 # important to ensure that the data file is not truncated
2039 2054 # when the index is replaced as otherwise data is lost.
2040 2055 tr.replace(self._datafile, self.start(trindex))
2041 2056
2042 2057 # the temp file replace the real index when we exit the context
2043 2058 # manager
2044 2059
2045 2060 tr.replace(self._indexfile, trindex * self.index.entry_size)
2046 2061 nodemaputil.setup_persistent_nodemap(tr, self)
2047 2062 self._segmentfile = randomaccessfile.randomaccessfile(
2048 2063 self.opener,
2049 2064 self._datafile,
2050 2065 self._chunkcachesize,
2051 2066 )
2052 2067
2053 2068 if existing_handles:
2054 2069 # switched from inline to conventional reopen the index
2055 2070 ifh = self.__index_write_fp()
2056 2071 self._writinghandles = (ifh, new_dfh, None)
2057 2072 self._segmentfile.writing_handle = new_dfh
2058 2073 new_dfh = None
2059 2074 # No need to deal with sidedata writing handle as it is only
2060 2075 # relevant with revlog-v2 which is never inline, not reaching
2061 2076 # this code
2062 2077 finally:
2063 2078 if new_dfh is not None:
2064 2079 new_dfh.close()
2065 2080
2066 2081 def _nodeduplicatecallback(self, transaction, node):
2067 2082 """called when trying to add a node already stored."""
2068 2083
2069 2084 @contextlib.contextmanager
2070 2085 def reading(self):
2071 2086 """Context manager that keeps data and sidedata files open for reading"""
2072 2087 with self._segmentfile.reading():
2073 2088 with self._segmentfile_sidedata.reading():
2074 2089 yield
2075 2090
2076 2091 @contextlib.contextmanager
2077 2092 def _writing(self, transaction):
2078 2093 if self._trypending:
2079 2094 msg = b'try to write in a `trypending` revlog: %s'
2080 2095 msg %= self.display_id
2081 2096 raise error.ProgrammingError(msg)
2082 2097 if self._writinghandles is not None:
2083 2098 yield
2084 2099 else:
2085 2100 ifh = dfh = sdfh = None
2086 2101 try:
2087 2102 r = len(self)
2088 2103 # opening the data file.
2089 2104 dsize = 0
2090 2105 if r:
2091 2106 dsize = self.end(r - 1)
2092 2107 dfh = None
2093 2108 if not self._inline:
2094 2109 try:
2095 2110 dfh = self._datafp(b"r+")
2096 2111 if self._docket is None:
2097 2112 dfh.seek(0, os.SEEK_END)
2098 2113 else:
2099 2114 dfh.seek(self._docket.data_end, os.SEEK_SET)
2100 2115 except IOError as inst:
2101 2116 if inst.errno != errno.ENOENT:
2102 2117 raise
2103 2118 dfh = self._datafp(b"w+")
2104 2119 transaction.add(self._datafile, dsize)
2105 2120 if self._sidedatafile is not None:
2106 2121 # revlog-v2 does not inline, help Pytype
2107 2122 assert dfh is not None
2108 2123 try:
2109 2124 sdfh = self.opener(self._sidedatafile, mode=b"r+")
2110 2125 dfh.seek(self._docket.sidedata_end, os.SEEK_SET)
2111 2126 except IOError as inst:
2112 2127 if inst.errno != errno.ENOENT:
2113 2128 raise
2114 2129 sdfh = self.opener(self._sidedatafile, mode=b"w+")
2115 2130 transaction.add(
2116 2131 self._sidedatafile, self._docket.sidedata_end
2117 2132 )
2118 2133
2119 2134 # opening the index file.
2120 2135 isize = r * self.index.entry_size
2121 2136 ifh = self.__index_write_fp()
2122 2137 if self._inline:
2123 2138 transaction.add(self._indexfile, dsize + isize)
2124 2139 else:
2125 2140 transaction.add(self._indexfile, isize)
2126 2141 # exposing all file handle for writing.
2127 2142 self._writinghandles = (ifh, dfh, sdfh)
2128 2143 self._segmentfile.writing_handle = ifh if self._inline else dfh
2129 2144 self._segmentfile_sidedata.writing_handle = sdfh
2130 2145 yield
2131 2146 if self._docket is not None:
2132 2147 self._write_docket(transaction)
2133 2148 finally:
2134 2149 self._writinghandles = None
2135 2150 self._segmentfile.writing_handle = None
2136 2151 self._segmentfile_sidedata.writing_handle = None
2137 2152 if dfh is not None:
2138 2153 dfh.close()
2139 2154 if sdfh is not None:
2140 2155 sdfh.close()
2141 2156 # closing the index file last to avoid exposing referent to
2142 2157 # potential unflushed data content.
2143 2158 if ifh is not None:
2144 2159 ifh.close()
2145 2160
2146 2161 def _write_docket(self, transaction):
2147 2162 """write the current docket on disk
2148 2163
2149 2164 Exist as a method to help changelog to implement transaction logic
2150 2165
2151 2166 We could also imagine using the same transaction logic for all revlog
2152 2167 since docket are cheap."""
2153 2168 self._docket.write(transaction)
2154 2169
2155 2170 def addrevision(
2156 2171 self,
2157 2172 text,
2158 2173 transaction,
2159 2174 link,
2160 2175 p1,
2161 2176 p2,
2162 2177 cachedelta=None,
2163 2178 node=None,
2164 2179 flags=REVIDX_DEFAULT_FLAGS,
2165 2180 deltacomputer=None,
2166 2181 sidedata=None,
2167 2182 ):
2168 2183 """add a revision to the log
2169 2184
2170 2185 text - the revision data to add
2171 2186 transaction - the transaction object used for rollback
2172 2187 link - the linkrev data to add
2173 2188 p1, p2 - the parent nodeids of the revision
2174 2189 cachedelta - an optional precomputed delta
2175 2190 node - nodeid of revision; typically node is not specified, and it is
2176 2191 computed by default as hash(text, p1, p2), however subclasses might
2177 2192 use different hashing method (and override checkhash() in such case)
2178 2193 flags - the known flags to set on the revision
2179 2194 deltacomputer - an optional deltacomputer instance shared between
2180 2195 multiple calls
2181 2196 """
2182 2197 if link == nullrev:
2183 2198 raise error.RevlogError(
2184 2199 _(b"attempted to add linkrev -1 to %s") % self.display_id
2185 2200 )
2186 2201
2187 2202 if sidedata is None:
2188 2203 sidedata = {}
2189 2204 elif sidedata and not self.hassidedata:
2190 2205 raise error.ProgrammingError(
2191 2206 _(b"trying to add sidedata to a revlog who don't support them")
2192 2207 )
2193 2208
2194 2209 if flags:
2195 2210 node = node or self.hash(text, p1, p2)
2196 2211
2197 2212 rawtext, validatehash = flagutil.processflagswrite(self, text, flags)
2198 2213
2199 2214 # If the flag processor modifies the revision data, ignore any provided
2200 2215 # cachedelta.
2201 2216 if rawtext != text:
2202 2217 cachedelta = None
2203 2218
2204 2219 if len(rawtext) > _maxentrysize:
2205 2220 raise error.RevlogError(
2206 2221 _(
2207 2222 b"%s: size of %d bytes exceeds maximum revlog storage of 2GiB"
2208 2223 )
2209 2224 % (self.display_id, len(rawtext))
2210 2225 )
2211 2226
2212 2227 node = node or self.hash(rawtext, p1, p2)
2213 2228 rev = self.index.get_rev(node)
2214 2229 if rev is not None:
2215 2230 return rev
2216 2231
2217 2232 if validatehash:
2218 2233 self.checkhash(rawtext, node, p1=p1, p2=p2)
2219 2234
2220 2235 return self.addrawrevision(
2221 2236 rawtext,
2222 2237 transaction,
2223 2238 link,
2224 2239 p1,
2225 2240 p2,
2226 2241 node,
2227 2242 flags,
2228 2243 cachedelta=cachedelta,
2229 2244 deltacomputer=deltacomputer,
2230 2245 sidedata=sidedata,
2231 2246 )
2232 2247
2233 2248 def addrawrevision(
2234 2249 self,
2235 2250 rawtext,
2236 2251 transaction,
2237 2252 link,
2238 2253 p1,
2239 2254 p2,
2240 2255 node,
2241 2256 flags,
2242 2257 cachedelta=None,
2243 2258 deltacomputer=None,
2244 2259 sidedata=None,
2245 2260 ):
2246 2261 """add a raw revision with known flags, node and parents
2247 2262 useful when reusing a revision not stored in this revlog (ex: received
2248 2263 over wire, or read from an external bundle).
2249 2264 """
2250 2265 with self._writing(transaction):
2251 2266 return self._addrevision(
2252 2267 node,
2253 2268 rawtext,
2254 2269 transaction,
2255 2270 link,
2256 2271 p1,
2257 2272 p2,
2258 2273 flags,
2259 2274 cachedelta,
2260 2275 deltacomputer=deltacomputer,
2261 2276 sidedata=sidedata,
2262 2277 )
2263 2278
2264 2279 def compress(self, data):
2265 2280 """Generate a possibly-compressed representation of data."""
2266 2281 if not data:
2267 2282 return b'', data
2268 2283
2269 2284 compressed = self._compressor.compress(data)
2270 2285
2271 2286 if compressed:
2272 2287 # The revlog compressor added the header in the returned data.
2273 2288 return b'', compressed
2274 2289
2275 2290 if data[0:1] == b'\0':
2276 2291 return b'', data
2277 2292 return b'u', data
2278 2293
2279 2294 def decompress(self, data):
2280 2295 """Decompress a revlog chunk.
2281 2296
2282 2297 The chunk is expected to begin with a header identifying the
2283 2298 format type so it can be routed to an appropriate decompressor.
2284 2299 """
2285 2300 if not data:
2286 2301 return data
2287 2302
2288 2303 # Revlogs are read much more frequently than they are written and many
2289 2304 # chunks only take microseconds to decompress, so performance is
2290 2305 # important here.
2291 2306 #
2292 2307 # We can make a few assumptions about revlogs:
2293 2308 #
2294 2309 # 1) the majority of chunks will be compressed (as opposed to inline
2295 2310 # raw data).
2296 2311 # 2) decompressing *any* data will likely by at least 10x slower than
2297 2312 # returning raw inline data.
2298 2313 # 3) we want to prioritize common and officially supported compression
2299 2314 # engines
2300 2315 #
2301 2316 # It follows that we want to optimize for "decompress compressed data
2302 2317 # when encoded with common and officially supported compression engines"
2303 2318 # case over "raw data" and "data encoded by less common or non-official
2304 2319 # compression engines." That is why we have the inline lookup first
2305 2320 # followed by the compengines lookup.
2306 2321 #
2307 2322 # According to `hg perfrevlogchunks`, this is ~0.5% faster for zlib
2308 2323 # compressed chunks. And this matters for changelog and manifest reads.
2309 2324 t = data[0:1]
2310 2325
2311 2326 if t == b'x':
2312 2327 try:
2313 2328 return _zlibdecompress(data)
2314 2329 except zlib.error as e:
2315 2330 raise error.RevlogError(
2316 2331 _(b'revlog decompress error: %s')
2317 2332 % stringutil.forcebytestr(e)
2318 2333 )
2319 2334 # '\0' is more common than 'u' so it goes first.
2320 2335 elif t == b'\0':
2321 2336 return data
2322 2337 elif t == b'u':
2323 2338 return util.buffer(data, 1)
2324 2339
2325 2340 compressor = self._get_decompressor(t)
2326 2341
2327 2342 return compressor.decompress(data)
2328 2343
2329 2344 def _addrevision(
2330 2345 self,
2331 2346 node,
2332 2347 rawtext,
2333 2348 transaction,
2334 2349 link,
2335 2350 p1,
2336 2351 p2,
2337 2352 flags,
2338 2353 cachedelta,
2339 2354 alwayscache=False,
2340 2355 deltacomputer=None,
2341 2356 sidedata=None,
2342 2357 ):
2343 2358 """internal function to add revisions to the log
2344 2359
2345 2360 see addrevision for argument descriptions.
2346 2361
2347 2362 note: "addrevision" takes non-raw text, "_addrevision" takes raw text.
2348 2363
2349 2364 if "deltacomputer" is not provided or None, a defaultdeltacomputer will
2350 2365 be used.
2351 2366
2352 2367 invariants:
2353 2368 - rawtext is optional (can be None); if not set, cachedelta must be set.
2354 2369 if both are set, they must correspond to each other.
2355 2370 """
2356 2371 if node == self.nullid:
2357 2372 raise error.RevlogError(
2358 2373 _(b"%s: attempt to add null revision") % self.display_id
2359 2374 )
2360 2375 if (
2361 2376 node == self.nodeconstants.wdirid
2362 2377 or node in self.nodeconstants.wdirfilenodeids
2363 2378 ):
2364 2379 raise error.RevlogError(
2365 2380 _(b"%s: attempt to add wdir revision") % self.display_id
2366 2381 )
2367 2382 if self._writinghandles is None:
2368 2383 msg = b'adding revision outside `revlog._writing` context'
2369 2384 raise error.ProgrammingError(msg)
2370 2385
2371 2386 if self._inline:
2372 2387 fh = self._writinghandles[0]
2373 2388 else:
2374 2389 fh = self._writinghandles[1]
2375 2390
2376 2391 btext = [rawtext]
2377 2392
2378 2393 curr = len(self)
2379 2394 prev = curr - 1
2380 2395
2381 2396 offset = self._get_data_offset(prev)
2382 2397
2383 2398 if self._concurrencychecker:
2384 2399 ifh, dfh, sdfh = self._writinghandles
2385 2400 # XXX no checking for the sidedata file
2386 2401 if self._inline:
2387 2402 # offset is "as if" it were in the .d file, so we need to add on
2388 2403 # the size of the entry metadata.
2389 2404 self._concurrencychecker(
2390 2405 ifh, self._indexfile, offset + curr * self.index.entry_size
2391 2406 )
2392 2407 else:
2393 2408 # Entries in the .i are a consistent size.
2394 2409 self._concurrencychecker(
2395 2410 ifh, self._indexfile, curr * self.index.entry_size
2396 2411 )
2397 2412 self._concurrencychecker(dfh, self._datafile, offset)
2398 2413
2399 2414 p1r, p2r = self.rev(p1), self.rev(p2)
2400 2415
2401 2416 # full versions are inserted when the needed deltas
2402 2417 # become comparable to the uncompressed text
2403 2418 if rawtext is None:
2404 2419 # need rawtext size, before changed by flag processors, which is
2405 2420 # the non-raw size. use revlog explicitly to avoid filelog's extra
2406 2421 # logic that might remove metadata size.
2407 2422 textlen = mdiff.patchedsize(
2408 2423 revlog.size(self, cachedelta[0]), cachedelta[1]
2409 2424 )
2410 2425 else:
2411 2426 textlen = len(rawtext)
2412 2427
2413 2428 if deltacomputer is None:
2414 2429 deltacomputer = deltautil.deltacomputer(self)
2415 2430
2416 2431 revinfo = revlogutils.revisioninfo(
2417 2432 node,
2418 2433 p1,
2419 2434 p2,
2420 2435 btext,
2421 2436 textlen,
2422 2437 cachedelta,
2423 2438 flags,
2424 2439 )
2425 2440
2426 2441 deltainfo = deltacomputer.finddeltainfo(revinfo, fh)
2427 2442
2428 2443 compression_mode = COMP_MODE_INLINE
2429 2444 if self._docket is not None:
2430 2445 default_comp = self._docket.default_compression_header
2431 2446 r = deltautil.delta_compression(default_comp, deltainfo)
2432 2447 compression_mode, deltainfo = r
2433 2448
2434 2449 sidedata_compression_mode = COMP_MODE_INLINE
2435 2450 if sidedata and self.hassidedata:
2436 2451 sidedata_compression_mode = COMP_MODE_PLAIN
2437 2452 serialized_sidedata = sidedatautil.serialize_sidedata(sidedata)
2438 2453 sidedata_offset = self._docket.sidedata_end
2439 2454 h, comp_sidedata = self.compress(serialized_sidedata)
2440 2455 if (
2441 2456 h != b'u'
2442 2457 and comp_sidedata[0:1] != b'\0'
2443 2458 and len(comp_sidedata) < len(serialized_sidedata)
2444 2459 ):
2445 2460 assert not h
2446 2461 if (
2447 2462 comp_sidedata[0:1]
2448 2463 == self._docket.default_compression_header
2449 2464 ):
2450 2465 sidedata_compression_mode = COMP_MODE_DEFAULT
2451 2466 serialized_sidedata = comp_sidedata
2452 2467 else:
2453 2468 sidedata_compression_mode = COMP_MODE_INLINE
2454 2469 serialized_sidedata = comp_sidedata
2455 2470 else:
2456 2471 serialized_sidedata = b""
2457 2472 # Don't store the offset if the sidedata is empty, that way
2458 2473 # we can easily detect empty sidedata and they will be no different
2459 2474 # than ones we manually add.
2460 2475 sidedata_offset = 0
2461 2476
2462 2477 rank = RANK_UNKNOWN
2463 2478 if self._format_version == CHANGELOGV2:
2464 2479 if (p1r, p2r) == (nullrev, nullrev):
2465 2480 rank = 1
2466 2481 elif p1r != nullrev and p2r == nullrev:
2467 2482 rank = 1 + self.fast_rank(p1r)
2468 2483 elif p1r == nullrev and p2r != nullrev:
2469 2484 rank = 1 + self.fast_rank(p2r)
2470 2485 else: # merge node
2471 2486 if rustdagop is not None and self.index.rust_ext_compat:
2472 2487 rank = rustdagop.rank(self.index, p1r, p2r)
2473 2488 else:
2474 2489 pmin, pmax = sorted((p1r, p2r))
2475 2490 rank = 1 + self.fast_rank(pmax)
2476 2491 rank += sum(1 for _ in self.findmissingrevs([pmax], [pmin]))
2477 2492
2478 2493 e = revlogutils.entry(
2479 2494 flags=flags,
2480 2495 data_offset=offset,
2481 2496 data_compressed_length=deltainfo.deltalen,
2482 2497 data_uncompressed_length=textlen,
2483 2498 data_compression_mode=compression_mode,
2484 2499 data_delta_base=deltainfo.base,
2485 2500 link_rev=link,
2486 2501 parent_rev_1=p1r,
2487 2502 parent_rev_2=p2r,
2488 2503 node_id=node,
2489 2504 sidedata_offset=sidedata_offset,
2490 2505 sidedata_compressed_length=len(serialized_sidedata),
2491 2506 sidedata_compression_mode=sidedata_compression_mode,
2492 2507 rank=rank,
2493 2508 )
2494 2509
2495 2510 self.index.append(e)
2496 2511 entry = self.index.entry_binary(curr)
2497 2512 if curr == 0 and self._docket is None:
2498 2513 header = self._format_flags | self._format_version
2499 2514 header = self.index.pack_header(header)
2500 2515 entry = header + entry
2501 2516 self._writeentry(
2502 2517 transaction,
2503 2518 entry,
2504 2519 deltainfo.data,
2505 2520 link,
2506 2521 offset,
2507 2522 serialized_sidedata,
2508 2523 sidedata_offset,
2509 2524 )
2510 2525
2511 2526 rawtext = btext[0]
2512 2527
2513 2528 if alwayscache and rawtext is None:
2514 2529 rawtext = deltacomputer.buildtext(revinfo, fh)
2515 2530
2516 2531 if type(rawtext) == bytes: # only accept immutable objects
2517 2532 self._revisioncache = (node, curr, rawtext)
2518 2533 self._chainbasecache[curr] = deltainfo.chainbase
2519 2534 return curr
2520 2535
2521 2536 def _get_data_offset(self, prev):
2522 2537 """Returns the current offset in the (in-transaction) data file.
2523 2538 Versions < 2 of the revlog can get this 0(1), revlog v2 needs a docket
2524 2539 file to store that information: since sidedata can be rewritten to the
2525 2540 end of the data file within a transaction, you can have cases where, for
2526 2541 example, rev `n` does not have sidedata while rev `n - 1` does, leading
2527 2542 to `n - 1`'s sidedata being written after `n`'s data.
2528 2543
2529 2544 TODO cache this in a docket file before getting out of experimental."""
2530 2545 if self._docket is None:
2531 2546 return self.end(prev)
2532 2547 else:
2533 2548 return self._docket.data_end
2534 2549
2535 2550 def _writeentry(
2536 2551 self, transaction, entry, data, link, offset, sidedata, sidedata_offset
2537 2552 ):
2538 2553 # Files opened in a+ mode have inconsistent behavior on various
2539 2554 # platforms. Windows requires that a file positioning call be made
2540 2555 # when the file handle transitions between reads and writes. See
2541 2556 # 3686fa2b8eee and the mixedfilemodewrapper in windows.py. On other
2542 2557 # platforms, Python or the platform itself can be buggy. Some versions
2543 2558 # of Solaris have been observed to not append at the end of the file
2544 2559 # if the file was seeked to before the end. See issue4943 for more.
2545 2560 #
2546 2561 # We work around this issue by inserting a seek() before writing.
2547 2562 # Note: This is likely not necessary on Python 3. However, because
2548 2563 # the file handle is reused for reads and may be seeked there, we need
2549 2564 # to be careful before changing this.
2550 2565 if self._writinghandles is None:
2551 2566 msg = b'adding revision outside `revlog._writing` context'
2552 2567 raise error.ProgrammingError(msg)
2553 2568 ifh, dfh, sdfh = self._writinghandles
2554 2569 if self._docket is None:
2555 2570 ifh.seek(0, os.SEEK_END)
2556 2571 else:
2557 2572 ifh.seek(self._docket.index_end, os.SEEK_SET)
2558 2573 if dfh:
2559 2574 if self._docket is None:
2560 2575 dfh.seek(0, os.SEEK_END)
2561 2576 else:
2562 2577 dfh.seek(self._docket.data_end, os.SEEK_SET)
2563 2578 if sdfh:
2564 2579 sdfh.seek(self._docket.sidedata_end, os.SEEK_SET)
2565 2580
2566 2581 curr = len(self) - 1
2567 2582 if not self._inline:
2568 2583 transaction.add(self._datafile, offset)
2569 2584 if self._sidedatafile:
2570 2585 transaction.add(self._sidedatafile, sidedata_offset)
2571 2586 transaction.add(self._indexfile, curr * len(entry))
2572 2587 if data[0]:
2573 2588 dfh.write(data[0])
2574 2589 dfh.write(data[1])
2575 2590 if sidedata:
2576 2591 sdfh.write(sidedata)
2577 2592 ifh.write(entry)
2578 2593 else:
2579 2594 offset += curr * self.index.entry_size
2580 2595 transaction.add(self._indexfile, offset)
2581 2596 ifh.write(entry)
2582 2597 ifh.write(data[0])
2583 2598 ifh.write(data[1])
2584 2599 assert not sidedata
2585 2600 self._enforceinlinesize(transaction)
2586 2601 if self._docket is not None:
2587 2602 # revlog-v2 always has 3 writing handles, help Pytype
2588 2603 wh1 = self._writinghandles[0]
2589 2604 wh2 = self._writinghandles[1]
2590 2605 wh3 = self._writinghandles[2]
2591 2606 assert wh1 is not None
2592 2607 assert wh2 is not None
2593 2608 assert wh3 is not None
2594 2609 self._docket.index_end = wh1.tell()
2595 2610 self._docket.data_end = wh2.tell()
2596 2611 self._docket.sidedata_end = wh3.tell()
2597 2612
2598 2613 nodemaputil.setup_persistent_nodemap(transaction, self)
2599 2614
2600 2615 def addgroup(
2601 2616 self,
2602 2617 deltas,
2603 2618 linkmapper,
2604 2619 transaction,
2605 2620 alwayscache=False,
2606 2621 addrevisioncb=None,
2607 2622 duplicaterevisioncb=None,
2608 2623 ):
2609 2624 """
2610 2625 add a delta group
2611 2626
2612 2627 given a set of deltas, add them to the revision log. the
2613 2628 first delta is against its parent, which should be in our
2614 2629 log, the rest are against the previous delta.
2615 2630
2616 2631 If ``addrevisioncb`` is defined, it will be called with arguments of
2617 2632 this revlog and the node that was added.
2618 2633 """
2619 2634
2620 2635 if self._adding_group:
2621 2636 raise error.ProgrammingError(b'cannot nest addgroup() calls')
2622 2637
2623 2638 self._adding_group = True
2624 2639 empty = True
2625 2640 try:
2626 2641 with self._writing(transaction):
2627 2642 deltacomputer = deltautil.deltacomputer(self)
2628 2643 # loop through our set of deltas
2629 2644 for data in deltas:
2630 2645 (
2631 2646 node,
2632 2647 p1,
2633 2648 p2,
2634 2649 linknode,
2635 2650 deltabase,
2636 2651 delta,
2637 2652 flags,
2638 2653 sidedata,
2639 2654 ) = data
2640 2655 link = linkmapper(linknode)
2641 2656 flags = flags or REVIDX_DEFAULT_FLAGS
2642 2657
2643 2658 rev = self.index.get_rev(node)
2644 2659 if rev is not None:
2645 2660 # this can happen if two branches make the same change
2646 2661 self._nodeduplicatecallback(transaction, rev)
2647 2662 if duplicaterevisioncb:
2648 2663 duplicaterevisioncb(self, rev)
2649 2664 empty = False
2650 2665 continue
2651 2666
2652 2667 for p in (p1, p2):
2653 2668 if not self.index.has_node(p):
2654 2669 raise error.LookupError(
2655 2670 p, self.radix, _(b'unknown parent')
2656 2671 )
2657 2672
2658 2673 if not self.index.has_node(deltabase):
2659 2674 raise error.LookupError(
2660 2675 deltabase, self.display_id, _(b'unknown delta base')
2661 2676 )
2662 2677
2663 2678 baserev = self.rev(deltabase)
2664 2679
2665 2680 if baserev != nullrev and self.iscensored(baserev):
2666 2681 # if base is censored, delta must be full replacement in a
2667 2682 # single patch operation
2668 2683 hlen = struct.calcsize(b">lll")
2669 2684 oldlen = self.rawsize(baserev)
2670 2685 newlen = len(delta) - hlen
2671 2686 if delta[:hlen] != mdiff.replacediffheader(
2672 2687 oldlen, newlen
2673 2688 ):
2674 2689 raise error.CensoredBaseError(
2675 2690 self.display_id, self.node(baserev)
2676 2691 )
2677 2692
2678 2693 if not flags and self._peek_iscensored(baserev, delta):
2679 2694 flags |= REVIDX_ISCENSORED
2680 2695
2681 2696 # We assume consumers of addrevisioncb will want to retrieve
2682 2697 # the added revision, which will require a call to
2683 2698 # revision(). revision() will fast path if there is a cache
2684 2699 # hit. So, we tell _addrevision() to always cache in this case.
2685 2700 # We're only using addgroup() in the context of changegroup
2686 2701 # generation so the revision data can always be handled as raw
2687 2702 # by the flagprocessor.
2688 2703 rev = self._addrevision(
2689 2704 node,
2690 2705 None,
2691 2706 transaction,
2692 2707 link,
2693 2708 p1,
2694 2709 p2,
2695 2710 flags,
2696 2711 (baserev, delta),
2697 2712 alwayscache=alwayscache,
2698 2713 deltacomputer=deltacomputer,
2699 2714 sidedata=sidedata,
2700 2715 )
2701 2716
2702 2717 if addrevisioncb:
2703 2718 addrevisioncb(self, rev)
2704 2719 empty = False
2705 2720 finally:
2706 2721 self._adding_group = False
2707 2722 return not empty
2708 2723
2709 2724 def iscensored(self, rev):
2710 2725 """Check if a file revision is censored."""
2711 2726 if not self._censorable:
2712 2727 return False
2713 2728
2714 2729 return self.flags(rev) & REVIDX_ISCENSORED
2715 2730
2716 2731 def _peek_iscensored(self, baserev, delta):
2717 2732 """Quickly check if a delta produces a censored revision."""
2718 2733 if not self._censorable:
2719 2734 return False
2720 2735
2721 2736 return storageutil.deltaiscensored(delta, baserev, self.rawsize)
2722 2737
2723 2738 def getstrippoint(self, minlink):
2724 2739 """find the minimum rev that must be stripped to strip the linkrev
2725 2740
2726 2741 Returns a tuple containing the minimum rev and a set of all revs that
2727 2742 have linkrevs that will be broken by this strip.
2728 2743 """
2729 2744 return storageutil.resolvestripinfo(
2730 2745 minlink,
2731 2746 len(self) - 1,
2732 2747 self.headrevs(),
2733 2748 self.linkrev,
2734 2749 self.parentrevs,
2735 2750 )
2736 2751
2737 2752 def strip(self, minlink, transaction):
2738 2753 """truncate the revlog on the first revision with a linkrev >= minlink
2739 2754
2740 2755 This function is called when we're stripping revision minlink and
2741 2756 its descendants from the repository.
2742 2757
2743 2758 We have to remove all revisions with linkrev >= minlink, because
2744 2759 the equivalent changelog revisions will be renumbered after the
2745 2760 strip.
2746 2761
2747 2762 So we truncate the revlog on the first of these revisions, and
2748 2763 trust that the caller has saved the revisions that shouldn't be
2749 2764 removed and that it'll re-add them after this truncation.
2750 2765 """
2751 2766 if len(self) == 0:
2752 2767 return
2753 2768
2754 2769 rev, _ = self.getstrippoint(minlink)
2755 2770 if rev == len(self):
2756 2771 return
2757 2772
2758 2773 # first truncate the files on disk
2759 2774 data_end = self.start(rev)
2760 2775 if not self._inline:
2761 2776 transaction.add(self._datafile, data_end)
2762 2777 end = rev * self.index.entry_size
2763 2778 else:
2764 2779 end = data_end + (rev * self.index.entry_size)
2765 2780
2766 2781 if self._sidedatafile:
2767 2782 sidedata_end = self.sidedata_cut_off(rev)
2768 2783 transaction.add(self._sidedatafile, sidedata_end)
2769 2784
2770 2785 transaction.add(self._indexfile, end)
2771 2786 if self._docket is not None:
2772 2787 # XXX we could, leverage the docket while stripping. However it is
2773 2788 # not powerfull enough at the time of this comment
2774 2789 self._docket.index_end = end
2775 2790 self._docket.data_end = data_end
2776 2791 self._docket.sidedata_end = sidedata_end
2777 2792 self._docket.write(transaction, stripping=True)
2778 2793
2779 2794 # then reset internal state in memory to forget those revisions
2780 2795 self._revisioncache = None
2781 2796 self._chaininfocache = util.lrucachedict(500)
2782 2797 self._segmentfile.clear_cache()
2783 2798 self._segmentfile_sidedata.clear_cache()
2784 2799
2785 2800 del self.index[rev:-1]
2786 2801
2787 2802 def checksize(self):
2788 2803 """Check size of index and data files
2789 2804
2790 2805 return a (dd, di) tuple.
2791 2806 - dd: extra bytes for the "data" file
2792 2807 - di: extra bytes for the "index" file
2793 2808
2794 2809 A healthy revlog will return (0, 0).
2795 2810 """
2796 2811 expected = 0
2797 2812 if len(self):
2798 2813 expected = max(0, self.end(len(self) - 1))
2799 2814
2800 2815 try:
2801 2816 with self._datafp() as f:
2802 2817 f.seek(0, io.SEEK_END)
2803 2818 actual = f.tell()
2804 2819 dd = actual - expected
2805 2820 except IOError as inst:
2806 2821 if inst.errno != errno.ENOENT:
2807 2822 raise
2808 2823 dd = 0
2809 2824
2810 2825 try:
2811 2826 f = self.opener(self._indexfile)
2812 2827 f.seek(0, io.SEEK_END)
2813 2828 actual = f.tell()
2814 2829 f.close()
2815 2830 s = self.index.entry_size
2816 2831 i = max(0, actual // s)
2817 2832 di = actual - (i * s)
2818 2833 if self._inline:
2819 2834 databytes = 0
2820 2835 for r in self:
2821 2836 databytes += max(0, self.length(r))
2822 2837 dd = 0
2823 2838 di = actual - len(self) * s - databytes
2824 2839 except IOError as inst:
2825 2840 if inst.errno != errno.ENOENT:
2826 2841 raise
2827 2842 di = 0
2828 2843
2829 2844 return (dd, di)
2830 2845
2831 2846 def files(self):
2832 2847 res = [self._indexfile]
2833 2848 if self._docket_file is None:
2834 2849 if not self._inline:
2835 2850 res.append(self._datafile)
2836 2851 else:
2837 2852 res.append(self._docket_file)
2838 2853 res.extend(self._docket.old_index_filepaths(include_empty=False))
2839 2854 if self._docket.data_end:
2840 2855 res.append(self._datafile)
2841 2856 res.extend(self._docket.old_data_filepaths(include_empty=False))
2842 2857 if self._docket.sidedata_end:
2843 2858 res.append(self._sidedatafile)
2844 2859 res.extend(self._docket.old_sidedata_filepaths(include_empty=False))
2845 2860 return res
2846 2861
2847 2862 def emitrevisions(
2848 2863 self,
2849 2864 nodes,
2850 2865 nodesorder=None,
2851 2866 revisiondata=False,
2852 2867 assumehaveparentrevisions=False,
2853 2868 deltamode=repository.CG_DELTAMODE_STD,
2854 2869 sidedata_helpers=None,
2855 2870 ):
2856 2871 if nodesorder not in (b'nodes', b'storage', b'linear', None):
2857 2872 raise error.ProgrammingError(
2858 2873 b'unhandled value for nodesorder: %s' % nodesorder
2859 2874 )
2860 2875
2861 2876 if nodesorder is None and not self._generaldelta:
2862 2877 nodesorder = b'storage'
2863 2878
2864 2879 if (
2865 2880 not self._storedeltachains
2866 2881 and deltamode != repository.CG_DELTAMODE_PREV
2867 2882 ):
2868 2883 deltamode = repository.CG_DELTAMODE_FULL
2869 2884
2870 2885 return storageutil.emitrevisions(
2871 2886 self,
2872 2887 nodes,
2873 2888 nodesorder,
2874 2889 revlogrevisiondelta,
2875 2890 deltaparentfn=self.deltaparent,
2876 2891 candeltafn=self.candelta,
2877 2892 rawsizefn=self.rawsize,
2878 2893 revdifffn=self.revdiff,
2879 2894 flagsfn=self.flags,
2880 2895 deltamode=deltamode,
2881 2896 revisiondata=revisiondata,
2882 2897 assumehaveparentrevisions=assumehaveparentrevisions,
2883 2898 sidedata_helpers=sidedata_helpers,
2884 2899 )
2885 2900
2886 2901 DELTAREUSEALWAYS = b'always'
2887 2902 DELTAREUSESAMEREVS = b'samerevs'
2888 2903 DELTAREUSENEVER = b'never'
2889 2904
2890 2905 DELTAREUSEFULLADD = b'fulladd'
2891 2906
2892 2907 DELTAREUSEALL = {b'always', b'samerevs', b'never', b'fulladd'}
2893 2908
2894 2909 def clone(
2895 2910 self,
2896 2911 tr,
2897 2912 destrevlog,
2898 2913 addrevisioncb=None,
2899 2914 deltareuse=DELTAREUSESAMEREVS,
2900 2915 forcedeltabothparents=None,
2901 2916 sidedata_helpers=None,
2902 2917 ):
2903 2918 """Copy this revlog to another, possibly with format changes.
2904 2919
2905 2920 The destination revlog will contain the same revisions and nodes.
2906 2921 However, it may not be bit-for-bit identical due to e.g. delta encoding
2907 2922 differences.
2908 2923
2909 2924 The ``deltareuse`` argument control how deltas from the existing revlog
2910 2925 are preserved in the destination revlog. The argument can have the
2911 2926 following values:
2912 2927
2913 2928 DELTAREUSEALWAYS
2914 2929 Deltas will always be reused (if possible), even if the destination
2915 2930 revlog would not select the same revisions for the delta. This is the
2916 2931 fastest mode of operation.
2917 2932 DELTAREUSESAMEREVS
2918 2933 Deltas will be reused if the destination revlog would pick the same
2919 2934 revisions for the delta. This mode strikes a balance between speed
2920 2935 and optimization.
2921 2936 DELTAREUSENEVER
2922 2937 Deltas will never be reused. This is the slowest mode of execution.
2923 2938 This mode can be used to recompute deltas (e.g. if the diff/delta
2924 2939 algorithm changes).
2925 2940 DELTAREUSEFULLADD
2926 2941 Revision will be re-added as if their were new content. This is
2927 2942 slower than DELTAREUSEALWAYS but allow more mechanism to kicks in.
2928 2943 eg: large file detection and handling.
2929 2944
2930 2945 Delta computation can be slow, so the choice of delta reuse policy can
2931 2946 significantly affect run time.
2932 2947
2933 2948 The default policy (``DELTAREUSESAMEREVS``) strikes a balance between
2934 2949 two extremes. Deltas will be reused if they are appropriate. But if the
2935 2950 delta could choose a better revision, it will do so. This means if you
2936 2951 are converting a non-generaldelta revlog to a generaldelta revlog,
2937 2952 deltas will be recomputed if the delta's parent isn't a parent of the
2938 2953 revision.
2939 2954
2940 2955 In addition to the delta policy, the ``forcedeltabothparents``
2941 2956 argument controls whether to force compute deltas against both parents
2942 2957 for merges. By default, the current default is used.
2943 2958
2944 2959 See `revlogutil.sidedata.get_sidedata_helpers` for the doc on
2945 2960 `sidedata_helpers`.
2946 2961 """
2947 2962 if deltareuse not in self.DELTAREUSEALL:
2948 2963 raise ValueError(
2949 2964 _(b'value for deltareuse invalid: %s') % deltareuse
2950 2965 )
2951 2966
2952 2967 if len(destrevlog):
2953 2968 raise ValueError(_(b'destination revlog is not empty'))
2954 2969
2955 2970 if getattr(self, 'filteredrevs', None):
2956 2971 raise ValueError(_(b'source revlog has filtered revisions'))
2957 2972 if getattr(destrevlog, 'filteredrevs', None):
2958 2973 raise ValueError(_(b'destination revlog has filtered revisions'))
2959 2974
2960 2975 # lazydelta and lazydeltabase controls whether to reuse a cached delta,
2961 2976 # if possible.
2962 2977 oldlazydelta = destrevlog._lazydelta
2963 2978 oldlazydeltabase = destrevlog._lazydeltabase
2964 2979 oldamd = destrevlog._deltabothparents
2965 2980
2966 2981 try:
2967 2982 if deltareuse == self.DELTAREUSEALWAYS:
2968 2983 destrevlog._lazydeltabase = True
2969 2984 destrevlog._lazydelta = True
2970 2985 elif deltareuse == self.DELTAREUSESAMEREVS:
2971 2986 destrevlog._lazydeltabase = False
2972 2987 destrevlog._lazydelta = True
2973 2988 elif deltareuse == self.DELTAREUSENEVER:
2974 2989 destrevlog._lazydeltabase = False
2975 2990 destrevlog._lazydelta = False
2976 2991
2977 2992 destrevlog._deltabothparents = forcedeltabothparents or oldamd
2978 2993
2979 2994 self._clone(
2980 2995 tr,
2981 2996 destrevlog,
2982 2997 addrevisioncb,
2983 2998 deltareuse,
2984 2999 forcedeltabothparents,
2985 3000 sidedata_helpers,
2986 3001 )
2987 3002
2988 3003 finally:
2989 3004 destrevlog._lazydelta = oldlazydelta
2990 3005 destrevlog._lazydeltabase = oldlazydeltabase
2991 3006 destrevlog._deltabothparents = oldamd
2992 3007
2993 3008 def _clone(
2994 3009 self,
2995 3010 tr,
2996 3011 destrevlog,
2997 3012 addrevisioncb,
2998 3013 deltareuse,
2999 3014 forcedeltabothparents,
3000 3015 sidedata_helpers,
3001 3016 ):
3002 3017 """perform the core duty of `revlog.clone` after parameter processing"""
3003 3018 deltacomputer = deltautil.deltacomputer(destrevlog)
3004 3019 index = self.index
3005 3020 for rev in self:
3006 3021 entry = index[rev]
3007 3022
3008 3023 # Some classes override linkrev to take filtered revs into
3009 3024 # account. Use raw entry from index.
3010 3025 flags = entry[0] & 0xFFFF
3011 3026 linkrev = entry[4]
3012 3027 p1 = index[entry[5]][7]
3013 3028 p2 = index[entry[6]][7]
3014 3029 node = entry[7]
3015 3030
3016 3031 # (Possibly) reuse the delta from the revlog if allowed and
3017 3032 # the revlog chunk is a delta.
3018 3033 cachedelta = None
3019 3034 rawtext = None
3020 3035 if deltareuse == self.DELTAREUSEFULLADD:
3021 3036 text = self._revisiondata(rev)
3022 3037 sidedata = self.sidedata(rev)
3023 3038
3024 3039 if sidedata_helpers is not None:
3025 3040 (sidedata, new_flags) = sidedatautil.run_sidedata_helpers(
3026 3041 self, sidedata_helpers, sidedata, rev
3027 3042 )
3028 3043 flags = flags | new_flags[0] & ~new_flags[1]
3029 3044
3030 3045 destrevlog.addrevision(
3031 3046 text,
3032 3047 tr,
3033 3048 linkrev,
3034 3049 p1,
3035 3050 p2,
3036 3051 cachedelta=cachedelta,
3037 3052 node=node,
3038 3053 flags=flags,
3039 3054 deltacomputer=deltacomputer,
3040 3055 sidedata=sidedata,
3041 3056 )
3042 3057 else:
3043 3058 if destrevlog._lazydelta:
3044 3059 dp = self.deltaparent(rev)
3045 3060 if dp != nullrev:
3046 3061 cachedelta = (dp, bytes(self._chunk(rev)))
3047 3062
3048 3063 sidedata = None
3049 3064 if not cachedelta:
3050 3065 rawtext = self._revisiondata(rev)
3051 3066 sidedata = self.sidedata(rev)
3052 3067 if sidedata is None:
3053 3068 sidedata = self.sidedata(rev)
3054 3069
3055 3070 if sidedata_helpers is not None:
3056 3071 (sidedata, new_flags) = sidedatautil.run_sidedata_helpers(
3057 3072 self, sidedata_helpers, sidedata, rev
3058 3073 )
3059 3074 flags = flags | new_flags[0] & ~new_flags[1]
3060 3075
3061 3076 with destrevlog._writing(tr):
3062 3077 destrevlog._addrevision(
3063 3078 node,
3064 3079 rawtext,
3065 3080 tr,
3066 3081 linkrev,
3067 3082 p1,
3068 3083 p2,
3069 3084 flags,
3070 3085 cachedelta,
3071 3086 deltacomputer=deltacomputer,
3072 3087 sidedata=sidedata,
3073 3088 )
3074 3089
3075 3090 if addrevisioncb:
3076 3091 addrevisioncb(self, rev, node)
3077 3092
3078 3093 def censorrevision(self, tr, censornode, tombstone=b''):
3079 3094 if self._format_version == REVLOGV0:
3080 3095 raise error.RevlogError(
3081 3096 _(b'cannot censor with version %d revlogs')
3082 3097 % self._format_version
3083 3098 )
3084 3099 elif self._format_version == REVLOGV1:
3085 3100 rewrite.v1_censor(self, tr, censornode, tombstone)
3086 3101 else:
3087 3102 rewrite.v2_censor(self, tr, censornode, tombstone)
3088 3103
3089 3104 def verifyintegrity(self, state):
3090 3105 """Verifies the integrity of the revlog.
3091 3106
3092 3107 Yields ``revlogproblem`` instances describing problems that are
3093 3108 found.
3094 3109 """
3095 3110 dd, di = self.checksize()
3096 3111 if dd:
3097 3112 yield revlogproblem(error=_(b'data length off by %d bytes') % dd)
3098 3113 if di:
3099 3114 yield revlogproblem(error=_(b'index contains %d extra bytes') % di)
3100 3115
3101 3116 version = self._format_version
3102 3117
3103 3118 # The verifier tells us what version revlog we should be.
3104 3119 if version != state[b'expectedversion']:
3105 3120 yield revlogproblem(
3106 3121 warning=_(b"warning: '%s' uses revlog format %d; expected %d")
3107 3122 % (self.display_id, version, state[b'expectedversion'])
3108 3123 )
3109 3124
3110 3125 state[b'skipread'] = set()
3111 3126 state[b'safe_renamed'] = set()
3112 3127
3113 3128 for rev in self:
3114 3129 node = self.node(rev)
3115 3130
3116 3131 # Verify contents. 4 cases to care about:
3117 3132 #
3118 3133 # common: the most common case
3119 3134 # rename: with a rename
3120 3135 # meta: file content starts with b'\1\n', the metadata
3121 3136 # header defined in filelog.py, but without a rename
3122 3137 # ext: content stored externally
3123 3138 #
3124 3139 # More formally, their differences are shown below:
3125 3140 #
3126 3141 # | common | rename | meta | ext
3127 3142 # -------------------------------------------------------
3128 3143 # flags() | 0 | 0 | 0 | not 0
3129 3144 # renamed() | False | True | False | ?
3130 3145 # rawtext[0:2]=='\1\n'| False | True | True | ?
3131 3146 #
3132 3147 # "rawtext" means the raw text stored in revlog data, which
3133 3148 # could be retrieved by "rawdata(rev)". "text"
3134 3149 # mentioned below is "revision(rev)".
3135 3150 #
3136 3151 # There are 3 different lengths stored physically:
3137 3152 # 1. L1: rawsize, stored in revlog index
3138 3153 # 2. L2: len(rawtext), stored in revlog data
3139 3154 # 3. L3: len(text), stored in revlog data if flags==0, or
3140 3155 # possibly somewhere else if flags!=0
3141 3156 #
3142 3157 # L1 should be equal to L2. L3 could be different from them.
3143 3158 # "text" may or may not affect commit hash depending on flag
3144 3159 # processors (see flagutil.addflagprocessor).
3145 3160 #
3146 3161 # | common | rename | meta | ext
3147 3162 # -------------------------------------------------
3148 3163 # rawsize() | L1 | L1 | L1 | L1
3149 3164 # size() | L1 | L2-LM | L1(*) | L1 (?)
3150 3165 # len(rawtext) | L2 | L2 | L2 | L2
3151 3166 # len(text) | L2 | L2 | L2 | L3
3152 3167 # len(read()) | L2 | L2-LM | L2-LM | L3 (?)
3153 3168 #
3154 3169 # LM: length of metadata, depending on rawtext
3155 3170 # (*): not ideal, see comment in filelog.size
3156 3171 # (?): could be "- len(meta)" if the resolved content has
3157 3172 # rename metadata
3158 3173 #
3159 3174 # Checks needed to be done:
3160 3175 # 1. length check: L1 == L2, in all cases.
3161 3176 # 2. hash check: depending on flag processor, we may need to
3162 3177 # use either "text" (external), or "rawtext" (in revlog).
3163 3178
3164 3179 try:
3165 3180 skipflags = state.get(b'skipflags', 0)
3166 3181 if skipflags:
3167 3182 skipflags &= self.flags(rev)
3168 3183
3169 3184 _verify_revision(self, skipflags, state, node)
3170 3185
3171 3186 l1 = self.rawsize(rev)
3172 3187 l2 = len(self.rawdata(node))
3173 3188
3174 3189 if l1 != l2:
3175 3190 yield revlogproblem(
3176 3191 error=_(b'unpacked size is %d, %d expected') % (l2, l1),
3177 3192 node=node,
3178 3193 )
3179 3194
3180 3195 except error.CensoredNodeError:
3181 3196 if state[b'erroroncensored']:
3182 3197 yield revlogproblem(
3183 3198 error=_(b'censored file data'), node=node
3184 3199 )
3185 3200 state[b'skipread'].add(node)
3186 3201 except Exception as e:
3187 3202 yield revlogproblem(
3188 3203 error=_(b'unpacking %s: %s')
3189 3204 % (short(node), stringutil.forcebytestr(e)),
3190 3205 node=node,
3191 3206 )
3192 3207 state[b'skipread'].add(node)
3193 3208
3194 3209 def storageinfo(
3195 3210 self,
3196 3211 exclusivefiles=False,
3197 3212 sharedfiles=False,
3198 3213 revisionscount=False,
3199 3214 trackedsize=False,
3200 3215 storedsize=False,
3201 3216 ):
3202 3217 d = {}
3203 3218
3204 3219 if exclusivefiles:
3205 3220 d[b'exclusivefiles'] = [(self.opener, self._indexfile)]
3206 3221 if not self._inline:
3207 3222 d[b'exclusivefiles'].append((self.opener, self._datafile))
3208 3223
3209 3224 if sharedfiles:
3210 3225 d[b'sharedfiles'] = []
3211 3226
3212 3227 if revisionscount:
3213 3228 d[b'revisionscount'] = len(self)
3214 3229
3215 3230 if trackedsize:
3216 3231 d[b'trackedsize'] = sum(map(self.rawsize, iter(self)))
3217 3232
3218 3233 if storedsize:
3219 3234 d[b'storedsize'] = sum(
3220 3235 self.opener.stat(path).st_size for path in self.files()
3221 3236 )
3222 3237
3223 3238 return d
3224 3239
3225 3240 def rewrite_sidedata(self, transaction, helpers, startrev, endrev):
3226 3241 if not self.hassidedata:
3227 3242 return
3228 3243 # revlog formats with sidedata support does not support inline
3229 3244 assert not self._inline
3230 3245 if not helpers[1] and not helpers[2]:
3231 3246 # Nothing to generate or remove
3232 3247 return
3233 3248
3234 3249 new_entries = []
3235 3250 # append the new sidedata
3236 3251 with self._writing(transaction):
3237 3252 ifh, dfh, sdfh = self._writinghandles
3238 3253 dfh.seek(self._docket.sidedata_end, os.SEEK_SET)
3239 3254
3240 3255 current_offset = sdfh.tell()
3241 3256 for rev in range(startrev, endrev + 1):
3242 3257 entry = self.index[rev]
3243 3258 new_sidedata, flags = sidedatautil.run_sidedata_helpers(
3244 3259 store=self,
3245 3260 sidedata_helpers=helpers,
3246 3261 sidedata={},
3247 3262 rev=rev,
3248 3263 )
3249 3264
3250 3265 serialized_sidedata = sidedatautil.serialize_sidedata(
3251 3266 new_sidedata
3252 3267 )
3253 3268
3254 3269 sidedata_compression_mode = COMP_MODE_INLINE
3255 3270 if serialized_sidedata and self.hassidedata:
3256 3271 sidedata_compression_mode = COMP_MODE_PLAIN
3257 3272 h, comp_sidedata = self.compress(serialized_sidedata)
3258 3273 if (
3259 3274 h != b'u'
3260 3275 and comp_sidedata[0] != b'\0'
3261 3276 and len(comp_sidedata) < len(serialized_sidedata)
3262 3277 ):
3263 3278 assert not h
3264 3279 if (
3265 3280 comp_sidedata[0]
3266 3281 == self._docket.default_compression_header
3267 3282 ):
3268 3283 sidedata_compression_mode = COMP_MODE_DEFAULT
3269 3284 serialized_sidedata = comp_sidedata
3270 3285 else:
3271 3286 sidedata_compression_mode = COMP_MODE_INLINE
3272 3287 serialized_sidedata = comp_sidedata
3273 3288 if entry[8] != 0 or entry[9] != 0:
3274 3289 # rewriting entries that already have sidedata is not
3275 3290 # supported yet, because it introduces garbage data in the
3276 3291 # revlog.
3277 3292 msg = b"rewriting existing sidedata is not supported yet"
3278 3293 raise error.Abort(msg)
3279 3294
3280 3295 # Apply (potential) flags to add and to remove after running
3281 3296 # the sidedata helpers
3282 3297 new_offset_flags = entry[0] | flags[0] & ~flags[1]
3283 3298 entry_update = (
3284 3299 current_offset,
3285 3300 len(serialized_sidedata),
3286 3301 new_offset_flags,
3287 3302 sidedata_compression_mode,
3288 3303 )
3289 3304
3290 3305 # the sidedata computation might have move the file cursors around
3291 3306 sdfh.seek(current_offset, os.SEEK_SET)
3292 3307 sdfh.write(serialized_sidedata)
3293 3308 new_entries.append(entry_update)
3294 3309 current_offset += len(serialized_sidedata)
3295 3310 self._docket.sidedata_end = sdfh.tell()
3296 3311
3297 3312 # rewrite the new index entries
3298 3313 ifh.seek(startrev * self.index.entry_size)
3299 3314 for i, e in enumerate(new_entries):
3300 3315 rev = startrev + i
3301 3316 self.index.replace_sidedata_info(rev, *e)
3302 3317 packed = self.index.entry_binary(rev)
3303 3318 if rev == 0 and self._docket is None:
3304 3319 header = self._format_flags | self._format_version
3305 3320 header = self.index.pack_header(header)
3306 3321 packed = header + packed
3307 3322 ifh.write(packed)
@@ -1,347 +1,347 b''
1 1 #require no-reposimplestore
2 2
3 3 $ . "$TESTDIR/narrow-library.sh"
4 4
5 5 create full repo
6 6
7 7 $ hg init master
8 8 $ cd master
9 9 $ cat >> .hg/hgrc <<EOF
10 10 > [narrow]
11 11 > serveellipses=True
12 12 > EOF
13 13
14 14 $ mkdir inside
15 15 $ echo 1 > inside/f
16 16 $ hg commit -Aqm 'initial inside'
17 17
18 18 $ mkdir outside
19 19 $ echo 1 > outside/f
20 20 $ hg commit -Aqm 'initial outside'
21 21
22 22 $ echo 2a > outside/f
23 23 $ hg commit -Aqm 'outside 2a'
24 24 $ echo 3 > inside/f
25 25 $ hg commit -Aqm 'inside 3'
26 26 $ echo 4a > outside/f
27 27 $ hg commit -Aqm 'outside 4a'
28 28 $ hg update '.~3'
29 29 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
30 30
31 31 $ echo 2b > outside/f
32 32 $ hg commit -Aqm 'outside 2b'
33 33 $ echo 3 > inside/f
34 34 $ hg commit -Aqm 'inside 3'
35 35 $ echo 4b > outside/f
36 36 $ hg commit -Aqm 'outside 4b'
37 37 $ hg update '.~3'
38 38 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
39 39
40 40 $ echo 2c > outside/f
41 41 $ hg commit -Aqm 'outside 2c'
42 42 $ echo 3 > inside/f
43 43 $ hg commit -Aqm 'inside 3'
44 44 $ echo 4c > outside/f
45 45 $ hg commit -Aqm 'outside 4c'
46 46 $ hg update '.~3'
47 47 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
48 48
49 49 $ echo 2d > outside/f
50 50 $ hg commit -Aqm 'outside 2d'
51 51 $ echo 3 > inside/f
52 52 $ hg commit -Aqm 'inside 3'
53 53 $ echo 4d > outside/f
54 54 $ hg commit -Aqm 'outside 4d'
55 55
56 56 $ hg update -r 'desc("outside 4a")'
57 57 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
58 58 $ hg merge -r 'desc("outside 4b")' 2>&1 | egrep -v '(warning:|incomplete!)'
59 59 merging outside/f
60 60 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
61 61 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
62 62 $ echo 5 > outside/f
63 63 $ rm outside/f.orig
64 64 $ hg resolve --mark outside/f
65 65 (no more unresolved files)
66 66 $ hg commit -m 'merge a/b 5'
67 67 $ echo 6 > outside/f
68 68 $ hg commit -Aqm 'outside 6'
69 69
70 70 $ hg merge -r 'desc("outside 4c")' 2>&1 | egrep -v '(warning:|incomplete!)'
71 71 merging outside/f
72 72 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
73 73 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
74 74 $ echo 7 > outside/f
75 75 $ rm outside/f.orig
76 76 $ hg resolve --mark outside/f
77 77 (no more unresolved files)
78 78 $ hg commit -Aqm 'merge a/b/c 7'
79 79 $ echo 8 > outside/f
80 80 $ hg commit -Aqm 'outside 8'
81 81
82 82 $ hg merge -r 'desc("outside 4d")' 2>&1 | egrep -v '(warning:|incomplete!)'
83 83 merging outside/f
84 84 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
85 85 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
86 86 $ echo 9 > outside/f
87 87 $ rm outside/f.orig
88 88 $ hg resolve --mark outside/f
89 89 (no more unresolved files)
90 90 $ hg commit -Aqm 'merge a/b/c/d 9'
91 91 $ echo 10 > outside/f
92 92 $ hg commit -Aqm 'outside 10'
93 93
94 94 $ echo 11 > inside/f
95 95 $ hg commit -Aqm 'inside 11'
96 96 $ echo 12 > outside/f
97 97 $ hg commit -Aqm 'outside 12'
98 98
99 99 $ hg log -G -T '{rev} {node|short} {desc}\n'
100 100 @ 21 8d874d57adea outside 12
101 101 |
102 102 o 20 7ef88b4dd4fa inside 11
103 103 |
104 104 o 19 2a20009de83e outside 10
105 105 |
106 106 o 18 3ac1f5779de3 merge a/b/c/d 9
107 107 |\
108 108 | o 17 38a9c2f7e546 outside 8
109 109 | |
110 110 | o 16 094aa62fc898 merge a/b/c 7
111 111 | |\
112 112 | | o 15 f29d083d32e4 outside 6
113 113 | | |
114 114 | | o 14 2dc11382541d merge a/b 5
115 115 | | |\
116 116 o | | | 13 27d07ef97221 outside 4d
117 117 | | | |
118 118 o | | | 12 465567bdfb2d inside 3
119 119 | | | |
120 120 o | | | 11 d1c61993ec83 outside 2d
121 121 | | | |
122 122 | o | | 10 56859a8e33b9 outside 4c
123 123 | | | |
124 124 | o | | 9 bb96a08b062a inside 3
125 125 | | | |
126 126 | o | | 8 b844052e7b3b outside 2c
127 127 |/ / /
128 128 | | o 7 9db2d8fcc2a6 outside 4b
129 129 | | |
130 130 | | o 6 6418167787a6 inside 3
131 131 | | |
132 132 +---o 5 77344f344d83 outside 2b
133 133 | |
134 134 | o 4 9cadde08dc9f outside 4a
135 135 | |
136 136 | o 3 019ef06f125b inside 3
137 137 | |
138 138 | o 2 75e40c075a19 outside 2a
139 139 |/
140 140 o 1 906d6c682641 initial outside
141 141 |
142 142 o 0 9f8e82b51004 initial inside
143 143
144 144
145 145 Now narrow and shallow clone this and get a hopefully correct graph
146 146
147 147 $ cd ..
148 148 $ hg clone --narrow ssh://user@dummy/master narrow --include inside --depth 7
149 149 requesting all changes
150 150 adding changesets
151 151 adding manifests
152 152 adding file changes
153 153 added 8 changesets with 3 changes to 1 files
154 154 new changesets *:* (glob)
155 155 updating to branch default
156 156 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
157 157 $ cd narrow
158 158
159 159 To make updating the tests easier, we print the emitted nodes
160 160 sorted. This makes it easier to identify when the same node structure
161 161 has been emitted, just in a different order.
162 162
163 163 $ hg log -G -T '{rev} {node|short}{if(ellipsis,"...")} {desc}\n'
164 164 @ 7 8d874d57adea... outside 12
165 165 |
166 166 o 6 7ef88b4dd4fa inside 11
167 167 |
168 168 o 5 2a20009de83e... outside 10
169 169 |
170 170 o 4 3ac1f5779de3... merge a/b/c/d 9
171 171 |\
172 172 | o 3 465567bdfb2d inside 3
173 173 | |
174 174 | o 2 d1c61993ec83... outside 2d
175 175 |
176 176 o 1 bb96a08b062a inside 3
177 177 |
178 178 o 0 b844052e7b3b... outside 2c
179 179
180 180
181 181 $ hg log -T '{if(ellipsis,"...")}{node|short} {p1node|short} {p2node|short} {desc}\n' | sort
182 ...2a20009de83e 000000000000 3ac1f5779de3 outside 10
182 ...2a20009de83e 3ac1f5779de3 000000000000 outside 10
183 183 ...3ac1f5779de3 bb96a08b062a 465567bdfb2d merge a/b/c/d 9
184 184 ...8d874d57adea 7ef88b4dd4fa 000000000000 outside 12
185 185 ...b844052e7b3b 000000000000 000000000000 outside 2c
186 186 ...d1c61993ec83 000000000000 000000000000 outside 2d
187 187 465567bdfb2d d1c61993ec83 000000000000 inside 3
188 188 7ef88b4dd4fa 2a20009de83e 000000000000 inside 11
189 189 bb96a08b062a b844052e7b3b 000000000000 inside 3
190 190
191 191 $ cd ..
192 192
193 193 Incremental test case: show a pull can pull in a conflicted merge even if elided
194 194
195 195 $ hg init pullmaster
196 196 $ cd pullmaster
197 197 $ cat >> .hg/hgrc <<EOF
198 198 > [narrow]
199 199 > serveellipses=True
200 200 > EOF
201 201 $ mkdir inside outside
202 202 $ echo v1 > inside/f
203 203 $ echo v1 > outside/f
204 204 $ hg add inside/f outside/f
205 205 $ hg commit -m init
206 206
207 207 $ for line in a b c d
208 208 > do
209 209 > hg update -r 0
210 210 > echo v2$line > outside/f
211 211 > hg commit -m "outside 2$line"
212 212 > echo v2$line > inside/f
213 213 > hg commit -m "inside 2$line"
214 214 > echo v3$line > outside/f
215 215 > hg commit -m "outside 3$line"
216 216 > echo v4$line > outside/f
217 217 > hg commit -m "outside 4$line"
218 218 > done
219 219 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
220 220 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
221 221 created new head
222 222 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
223 223 created new head
224 224 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
225 225 created new head
226 226
227 227 $ cd ..
228 228 $ hg clone --narrow ssh://user@dummy/pullmaster pullshallow \
229 229 > --include inside --depth 3
230 230 requesting all changes
231 231 adding changesets
232 232 adding manifests
233 233 adding file changes
234 234 added 12 changesets with 5 changes to 1 files (+3 heads)
235 235 new changesets *:* (glob)
236 236 updating to branch default
237 237 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
238 238 $ cd pullshallow
239 239
240 240 $ hg log -G -T '{rev} {node|short}{if(ellipsis,"...")} {desc}\n'
241 241 @ 11 0ebbd712a0c8... outside 4d
242 242 |
243 243 o 10 0d4c867aeb23 inside 2d
244 244 |
245 245 o 9 e932969c3961... outside 2d
246 246
247 247 o 8 33d530345455... outside 4c
248 248 |
249 249 o 7 0ce6481bfe07 inside 2c
250 250 |
251 251 o 6 caa65c940632... outside 2c
252 252
253 253 o 5 3df233defecc... outside 4b
254 254 |
255 255 o 4 7162cc6d11a4 inside 2b
256 256 |
257 257 o 3 f2a632f0082d... outside 2b
258 258
259 259 o 2 b8a3da16ba49... outside 4a
260 260 |
261 261 o 1 53f543eb8e45 inside 2a
262 262 |
263 263 o 0 1be3e5221c6a... outside 2a
264 264
265 265 $ hg log -T '{if(ellipsis,"...")}{node|short} {p1node|short} {p2node|short} {desc}\n' | sort
266 266 ...0ebbd712a0c8 0d4c867aeb23 000000000000 outside 4d
267 267 ...1be3e5221c6a 000000000000 000000000000 outside 2a
268 268 ...33d530345455 0ce6481bfe07 000000000000 outside 4c
269 269 ...3df233defecc 7162cc6d11a4 000000000000 outside 4b
270 270 ...b8a3da16ba49 53f543eb8e45 000000000000 outside 4a
271 271 ...caa65c940632 000000000000 000000000000 outside 2c
272 272 ...e932969c3961 000000000000 000000000000 outside 2d
273 273 ...f2a632f0082d 000000000000 000000000000 outside 2b
274 274 0ce6481bfe07 caa65c940632 000000000000 inside 2c
275 275 0d4c867aeb23 e932969c3961 000000000000 inside 2d
276 276 53f543eb8e45 1be3e5221c6a 000000000000 inside 2a
277 277 7162cc6d11a4 f2a632f0082d 000000000000 inside 2b
278 278
279 279 $ cd ../pullmaster
280 280 $ hg update -r 'desc("outside 4a")'
281 281 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
282 282 $ hg merge -r 'desc("outside 4b")' 2>&1 | egrep -v '(warning:|incomplete!)'
283 283 merging inside/f
284 284 merging outside/f
285 285 0 files updated, 0 files merged, 0 files removed, 2 files unresolved
286 286 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
287 287 $ echo 3 > inside/f
288 288 $ echo 5 > outside/f
289 289 $ rm -f {in,out}side/f.orig
290 290 $ hg resolve --mark inside/f outside/f
291 291 (no more unresolved files)
292 292 $ hg commit -m 'merge a/b 5'
293 293
294 294 $ hg update -r 'desc("outside 4c")'
295 295 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
296 296 $ hg merge -r 'desc("outside 4d")' 2>&1 | egrep -v '(warning:|incomplete!)'
297 297 merging inside/f
298 298 merging outside/f
299 299 0 files updated, 0 files merged, 0 files removed, 2 files unresolved
300 300 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
301 301 $ echo 3 > inside/f
302 302 $ echo 5 > outside/f
303 303 $ rm -f {in,out}side/f.orig
304 304 $ hg resolve --mark inside/f outside/f
305 305 (no more unresolved files)
306 306 $ hg commit -m 'merge c/d 5'
307 307
308 308 $ hg update -r 'desc("merge a/b 5")'
309 309 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
310 310 $ hg merge -r 'desc("merge c/d 5")'
311 311 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
312 312 (branch merge, don't forget to commit)
313 313 $ echo 6 > outside/f
314 314 $ hg commit -m 'outside 6'
315 315 $ echo 7 > outside/f
316 316 $ hg commit -m 'outside 7'
317 317 $ echo 8 > outside/f
318 318 $ hg commit -m 'outside 8'
319 319
320 320 $ cd ../pullshallow
321 321 $ hg pull --depth 3
322 322 pulling from ssh://user@dummy/pullmaster
323 323 searching for changes
324 324 adding changesets
325 325 adding manifests
326 326 adding file changes
327 327 added 4 changesets with 3 changes to 1 files (-3 heads)
328 328 new changesets *:* (glob)
329 329 (run 'hg update' to get a working copy)
330 330
331 331 $ hg log -T '{if(ellipsis,"...")}{node|short} {p1node|short} {p2node|short} {desc}\n' | sort
332 332 ...0ebbd712a0c8 0d4c867aeb23 000000000000 outside 4d
333 333 ...1be3e5221c6a 000000000000 000000000000 outside 2a
334 334 ...33d530345455 0ce6481bfe07 000000000000 outside 4c
335 335 ...3df233defecc 7162cc6d11a4 000000000000 outside 4b
336 336 ...b8a3da16ba49 53f543eb8e45 000000000000 outside 4a
337 337 ...bf545653453e 968003d40c60 000000000000 outside 8
338 338 ...caa65c940632 000000000000 000000000000 outside 2c
339 339 ...e932969c3961 000000000000 000000000000 outside 2d
340 340 ...f2a632f0082d 000000000000 000000000000 outside 2b
341 341 0ce6481bfe07 caa65c940632 000000000000 inside 2c
342 342 0d4c867aeb23 e932969c3961 000000000000 inside 2d
343 343 53f543eb8e45 1be3e5221c6a 000000000000 inside 2a
344 344 67d49c0bdbda b8a3da16ba49 3df233defecc merge a/b 5
345 345 7162cc6d11a4 f2a632f0082d 000000000000 inside 2b
346 346 968003d40c60 67d49c0bdbda e867021d52c2 outside 6
347 347 e867021d52c2 33d530345455 0ebbd712a0c8 merge c/d 5
General Comments 0
You need to be logged in to leave comments. Login now