##// END OF EJS Templates
py3: fix sorting of obsolete markers in obsutil (issue6217)...
Denis Laxalde -
r43858:e513e87b stable
parent child Browse files
Show More
@@ -1,3090 +1,3086 b''
1 1 # exchange.py - utility to exchange data between repos.
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 hashlib
12 12
13 13 from .i18n import _
14 14 from .node import (
15 15 hex,
16 16 nullid,
17 17 nullrev,
18 18 )
19 19 from .thirdparty import attr
20 20 from . import (
21 21 bookmarks as bookmod,
22 22 bundle2,
23 23 changegroup,
24 24 discovery,
25 25 error,
26 26 exchangev2,
27 27 lock as lockmod,
28 28 logexchange,
29 29 narrowspec,
30 30 obsolete,
31 obsutil,
31 32 phases,
32 33 pushkey,
33 34 pycompat,
34 35 scmutil,
35 36 sslutil,
36 37 streamclone,
37 38 url as urlmod,
38 39 util,
39 40 wireprototypes,
40 41 )
41 42 from .interfaces import repository
42 43 from .utils import stringutil
43 44
44 45 urlerr = util.urlerr
45 46 urlreq = util.urlreq
46 47
47 48 _NARROWACL_SECTION = b'narrowacl'
48 49
49 50 # Maps bundle version human names to changegroup versions.
50 51 _bundlespeccgversions = {
51 52 b'v1': b'01',
52 53 b'v2': b'02',
53 54 b'packed1': b's1',
54 55 b'bundle2': b'02', # legacy
55 56 }
56 57
57 58 # Maps bundle version with content opts to choose which part to bundle
58 59 _bundlespeccontentopts = {
59 60 b'v1': {
60 61 b'changegroup': True,
61 62 b'cg.version': b'01',
62 63 b'obsolescence': False,
63 64 b'phases': False,
64 65 b'tagsfnodescache': False,
65 66 b'revbranchcache': False,
66 67 },
67 68 b'v2': {
68 69 b'changegroup': True,
69 70 b'cg.version': b'02',
70 71 b'obsolescence': False,
71 72 b'phases': False,
72 73 b'tagsfnodescache': True,
73 74 b'revbranchcache': True,
74 75 },
75 76 b'packed1': {b'cg.version': b's1'},
76 77 }
77 78 _bundlespeccontentopts[b'bundle2'] = _bundlespeccontentopts[b'v2']
78 79
79 80 _bundlespecvariants = {
80 81 b"streamv2": {
81 82 b"changegroup": False,
82 83 b"streamv2": True,
83 84 b"tagsfnodescache": False,
84 85 b"revbranchcache": False,
85 86 }
86 87 }
87 88
88 89 # Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE.
89 90 _bundlespecv1compengines = {b'gzip', b'bzip2', b'none'}
90 91
91 92
92 93 @attr.s
93 94 class bundlespec(object):
94 95 compression = attr.ib()
95 96 wirecompression = attr.ib()
96 97 version = attr.ib()
97 98 wireversion = attr.ib()
98 99 params = attr.ib()
99 100 contentopts = attr.ib()
100 101
101 102
102 def _sortedmarkers(markers):
103 # last item of marker tuple ('parents') may be None or a tuple
104 return sorted(markers, key=lambda m: m[:-1] + (m[-1] or (),))
105
106
107 103 def parsebundlespec(repo, spec, strict=True):
108 104 """Parse a bundle string specification into parts.
109 105
110 106 Bundle specifications denote a well-defined bundle/exchange format.
111 107 The content of a given specification should not change over time in
112 108 order to ensure that bundles produced by a newer version of Mercurial are
113 109 readable from an older version.
114 110
115 111 The string currently has the form:
116 112
117 113 <compression>-<type>[;<parameter0>[;<parameter1>]]
118 114
119 115 Where <compression> is one of the supported compression formats
120 116 and <type> is (currently) a version string. A ";" can follow the type and
121 117 all text afterwards is interpreted as URI encoded, ";" delimited key=value
122 118 pairs.
123 119
124 120 If ``strict`` is True (the default) <compression> is required. Otherwise,
125 121 it is optional.
126 122
127 123 Returns a bundlespec object of (compression, version, parameters).
128 124 Compression will be ``None`` if not in strict mode and a compression isn't
129 125 defined.
130 126
131 127 An ``InvalidBundleSpecification`` is raised when the specification is
132 128 not syntactically well formed.
133 129
134 130 An ``UnsupportedBundleSpecification`` is raised when the compression or
135 131 bundle type/version is not recognized.
136 132
137 133 Note: this function will likely eventually return a more complex data
138 134 structure, including bundle2 part information.
139 135 """
140 136
141 137 def parseparams(s):
142 138 if b';' not in s:
143 139 return s, {}
144 140
145 141 params = {}
146 142 version, paramstr = s.split(b';', 1)
147 143
148 144 for p in paramstr.split(b';'):
149 145 if b'=' not in p:
150 146 raise error.InvalidBundleSpecification(
151 147 _(
152 148 b'invalid bundle specification: '
153 149 b'missing "=" in parameter: %s'
154 150 )
155 151 % p
156 152 )
157 153
158 154 key, value = p.split(b'=', 1)
159 155 key = urlreq.unquote(key)
160 156 value = urlreq.unquote(value)
161 157 params[key] = value
162 158
163 159 return version, params
164 160
165 161 if strict and b'-' not in spec:
166 162 raise error.InvalidBundleSpecification(
167 163 _(
168 164 b'invalid bundle specification; '
169 165 b'must be prefixed with compression: %s'
170 166 )
171 167 % spec
172 168 )
173 169
174 170 if b'-' in spec:
175 171 compression, version = spec.split(b'-', 1)
176 172
177 173 if compression not in util.compengines.supportedbundlenames:
178 174 raise error.UnsupportedBundleSpecification(
179 175 _(b'%s compression is not supported') % compression
180 176 )
181 177
182 178 version, params = parseparams(version)
183 179
184 180 if version not in _bundlespeccgversions:
185 181 raise error.UnsupportedBundleSpecification(
186 182 _(b'%s is not a recognized bundle version') % version
187 183 )
188 184 else:
189 185 # Value could be just the compression or just the version, in which
190 186 # case some defaults are assumed (but only when not in strict mode).
191 187 assert not strict
192 188
193 189 spec, params = parseparams(spec)
194 190
195 191 if spec in util.compengines.supportedbundlenames:
196 192 compression = spec
197 193 version = b'v1'
198 194 # Generaldelta repos require v2.
199 195 if b'generaldelta' in repo.requirements:
200 196 version = b'v2'
201 197 # Modern compression engines require v2.
202 198 if compression not in _bundlespecv1compengines:
203 199 version = b'v2'
204 200 elif spec in _bundlespeccgversions:
205 201 if spec == b'packed1':
206 202 compression = b'none'
207 203 else:
208 204 compression = b'bzip2'
209 205 version = spec
210 206 else:
211 207 raise error.UnsupportedBundleSpecification(
212 208 _(b'%s is not a recognized bundle specification') % spec
213 209 )
214 210
215 211 # Bundle version 1 only supports a known set of compression engines.
216 212 if version == b'v1' and compression not in _bundlespecv1compengines:
217 213 raise error.UnsupportedBundleSpecification(
218 214 _(b'compression engine %s is not supported on v1 bundles')
219 215 % compression
220 216 )
221 217
222 218 # The specification for packed1 can optionally declare the data formats
223 219 # required to apply it. If we see this metadata, compare against what the
224 220 # repo supports and error if the bundle isn't compatible.
225 221 if version == b'packed1' and b'requirements' in params:
226 222 requirements = set(params[b'requirements'].split(b','))
227 223 missingreqs = requirements - repo.supportedformats
228 224 if missingreqs:
229 225 raise error.UnsupportedBundleSpecification(
230 226 _(b'missing support for repository features: %s')
231 227 % b', '.join(sorted(missingreqs))
232 228 )
233 229
234 230 # Compute contentopts based on the version
235 231 contentopts = _bundlespeccontentopts.get(version, {}).copy()
236 232
237 233 # Process the variants
238 234 if b"stream" in params and params[b"stream"] == b"v2":
239 235 variant = _bundlespecvariants[b"streamv2"]
240 236 contentopts.update(variant)
241 237
242 238 engine = util.compengines.forbundlename(compression)
243 239 compression, wirecompression = engine.bundletype()
244 240 wireversion = _bundlespeccgversions[version]
245 241
246 242 return bundlespec(
247 243 compression, wirecompression, version, wireversion, params, contentopts
248 244 )
249 245
250 246
251 247 def readbundle(ui, fh, fname, vfs=None):
252 248 header = changegroup.readexactly(fh, 4)
253 249
254 250 alg = None
255 251 if not fname:
256 252 fname = b"stream"
257 253 if not header.startswith(b'HG') and header.startswith(b'\0'):
258 254 fh = changegroup.headerlessfixup(fh, header)
259 255 header = b"HG10"
260 256 alg = b'UN'
261 257 elif vfs:
262 258 fname = vfs.join(fname)
263 259
264 260 magic, version = header[0:2], header[2:4]
265 261
266 262 if magic != b'HG':
267 263 raise error.Abort(_(b'%s: not a Mercurial bundle') % fname)
268 264 if version == b'10':
269 265 if alg is None:
270 266 alg = changegroup.readexactly(fh, 2)
271 267 return changegroup.cg1unpacker(fh, alg)
272 268 elif version.startswith(b'2'):
273 269 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
274 270 elif version == b'S1':
275 271 return streamclone.streamcloneapplier(fh)
276 272 else:
277 273 raise error.Abort(
278 274 _(b'%s: unknown bundle version %s') % (fname, version)
279 275 )
280 276
281 277
282 278 def getbundlespec(ui, fh):
283 279 """Infer the bundlespec from a bundle file handle.
284 280
285 281 The input file handle is seeked and the original seek position is not
286 282 restored.
287 283 """
288 284
289 285 def speccompression(alg):
290 286 try:
291 287 return util.compengines.forbundletype(alg).bundletype()[0]
292 288 except KeyError:
293 289 return None
294 290
295 291 b = readbundle(ui, fh, None)
296 292 if isinstance(b, changegroup.cg1unpacker):
297 293 alg = b._type
298 294 if alg == b'_truncatedBZ':
299 295 alg = b'BZ'
300 296 comp = speccompression(alg)
301 297 if not comp:
302 298 raise error.Abort(_(b'unknown compression algorithm: %s') % alg)
303 299 return b'%s-v1' % comp
304 300 elif isinstance(b, bundle2.unbundle20):
305 301 if b'Compression' in b.params:
306 302 comp = speccompression(b.params[b'Compression'])
307 303 if not comp:
308 304 raise error.Abort(
309 305 _(b'unknown compression algorithm: %s') % comp
310 306 )
311 307 else:
312 308 comp = b'none'
313 309
314 310 version = None
315 311 for part in b.iterparts():
316 312 if part.type == b'changegroup':
317 313 version = part.params[b'version']
318 314 if version in (b'01', b'02'):
319 315 version = b'v2'
320 316 else:
321 317 raise error.Abort(
322 318 _(
323 319 b'changegroup version %s does not have '
324 320 b'a known bundlespec'
325 321 )
326 322 % version,
327 323 hint=_(b'try upgrading your Mercurial client'),
328 324 )
329 325 elif part.type == b'stream2' and version is None:
330 326 # A stream2 part requires to be part of a v2 bundle
331 327 requirements = urlreq.unquote(part.params[b'requirements'])
332 328 splitted = requirements.split()
333 329 params = bundle2._formatrequirementsparams(splitted)
334 330 return b'none-v2;stream=v2;%s' % params
335 331
336 332 if not version:
337 333 raise error.Abort(
338 334 _(b'could not identify changegroup version in bundle')
339 335 )
340 336
341 337 return b'%s-%s' % (comp, version)
342 338 elif isinstance(b, streamclone.streamcloneapplier):
343 339 requirements = streamclone.readbundle1header(fh)[2]
344 340 formatted = bundle2._formatrequirementsparams(requirements)
345 341 return b'none-packed1;%s' % formatted
346 342 else:
347 343 raise error.Abort(_(b'unknown bundle type: %s') % b)
348 344
349 345
350 346 def _computeoutgoing(repo, heads, common):
351 347 """Computes which revs are outgoing given a set of common
352 348 and a set of heads.
353 349
354 350 This is a separate function so extensions can have access to
355 351 the logic.
356 352
357 353 Returns a discovery.outgoing object.
358 354 """
359 355 cl = repo.changelog
360 356 if common:
361 357 hasnode = cl.hasnode
362 358 common = [n for n in common if hasnode(n)]
363 359 else:
364 360 common = [nullid]
365 361 if not heads:
366 362 heads = cl.heads()
367 363 return discovery.outgoing(repo, common, heads)
368 364
369 365
370 366 def _checkpublish(pushop):
371 367 repo = pushop.repo
372 368 ui = repo.ui
373 369 behavior = ui.config(b'experimental', b'auto-publish')
374 370 if pushop.publish or behavior not in (b'warn', b'confirm', b'abort'):
375 371 return
376 372 remotephases = listkeys(pushop.remote, b'phases')
377 373 if not remotephases.get(b'publishing', False):
378 374 return
379 375
380 376 if pushop.revs is None:
381 377 published = repo.filtered(b'served').revs(b'not public()')
382 378 else:
383 379 published = repo.revs(b'::%ln - public()', pushop.revs)
384 380 if published:
385 381 if behavior == b'warn':
386 382 ui.warn(
387 383 _(b'%i changesets about to be published\n') % len(published)
388 384 )
389 385 elif behavior == b'confirm':
390 386 if ui.promptchoice(
391 387 _(b'push and publish %i changesets (yn)?$$ &Yes $$ &No')
392 388 % len(published)
393 389 ):
394 390 raise error.Abort(_(b'user quit'))
395 391 elif behavior == b'abort':
396 392 msg = _(b'push would publish %i changesets') % len(published)
397 393 hint = _(
398 394 b"use --publish or adjust 'experimental.auto-publish'"
399 395 b" config"
400 396 )
401 397 raise error.Abort(msg, hint=hint)
402 398
403 399
404 400 def _forcebundle1(op):
405 401 """return true if a pull/push must use bundle1
406 402
407 403 This function is used to allow testing of the older bundle version"""
408 404 ui = op.repo.ui
409 405 # The goal is this config is to allow developer to choose the bundle
410 406 # version used during exchanged. This is especially handy during test.
411 407 # Value is a list of bundle version to be picked from, highest version
412 408 # should be used.
413 409 #
414 410 # developer config: devel.legacy.exchange
415 411 exchange = ui.configlist(b'devel', b'legacy.exchange')
416 412 forcebundle1 = b'bundle2' not in exchange and b'bundle1' in exchange
417 413 return forcebundle1 or not op.remote.capable(b'bundle2')
418 414
419 415
420 416 class pushoperation(object):
421 417 """A object that represent a single push operation
422 418
423 419 Its purpose is to carry push related state and very common operations.
424 420
425 421 A new pushoperation should be created at the beginning of each push and
426 422 discarded afterward.
427 423 """
428 424
429 425 def __init__(
430 426 self,
431 427 repo,
432 428 remote,
433 429 force=False,
434 430 revs=None,
435 431 newbranch=False,
436 432 bookmarks=(),
437 433 publish=False,
438 434 pushvars=None,
439 435 ):
440 436 # repo we push from
441 437 self.repo = repo
442 438 self.ui = repo.ui
443 439 # repo we push to
444 440 self.remote = remote
445 441 # force option provided
446 442 self.force = force
447 443 # revs to be pushed (None is "all")
448 444 self.revs = revs
449 445 # bookmark explicitly pushed
450 446 self.bookmarks = bookmarks
451 447 # allow push of new branch
452 448 self.newbranch = newbranch
453 449 # step already performed
454 450 # (used to check what steps have been already performed through bundle2)
455 451 self.stepsdone = set()
456 452 # Integer version of the changegroup push result
457 453 # - None means nothing to push
458 454 # - 0 means HTTP error
459 455 # - 1 means we pushed and remote head count is unchanged *or*
460 456 # we have outgoing changesets but refused to push
461 457 # - other values as described by addchangegroup()
462 458 self.cgresult = None
463 459 # Boolean value for the bookmark push
464 460 self.bkresult = None
465 461 # discover.outgoing object (contains common and outgoing data)
466 462 self.outgoing = None
467 463 # all remote topological heads before the push
468 464 self.remoteheads = None
469 465 # Details of the remote branch pre and post push
470 466 #
471 467 # mapping: {'branch': ([remoteheads],
472 468 # [newheads],
473 469 # [unsyncedheads],
474 470 # [discardedheads])}
475 471 # - branch: the branch name
476 472 # - remoteheads: the list of remote heads known locally
477 473 # None if the branch is new
478 474 # - newheads: the new remote heads (known locally) with outgoing pushed
479 475 # - unsyncedheads: the list of remote heads unknown locally.
480 476 # - discardedheads: the list of remote heads made obsolete by the push
481 477 self.pushbranchmap = None
482 478 # testable as a boolean indicating if any nodes are missing locally.
483 479 self.incoming = None
484 480 # summary of the remote phase situation
485 481 self.remotephases = None
486 482 # phases changes that must be pushed along side the changesets
487 483 self.outdatedphases = None
488 484 # phases changes that must be pushed if changeset push fails
489 485 self.fallbackoutdatedphases = None
490 486 # outgoing obsmarkers
491 487 self.outobsmarkers = set()
492 488 # outgoing bookmarks, list of (bm, oldnode | '', newnode | '')
493 489 self.outbookmarks = []
494 490 # transaction manager
495 491 self.trmanager = None
496 492 # map { pushkey partid -> callback handling failure}
497 493 # used to handle exception from mandatory pushkey part failure
498 494 self.pkfailcb = {}
499 495 # an iterable of pushvars or None
500 496 self.pushvars = pushvars
501 497 # publish pushed changesets
502 498 self.publish = publish
503 499
504 500 @util.propertycache
505 501 def futureheads(self):
506 502 """future remote heads if the changeset push succeeds"""
507 503 return self.outgoing.missingheads
508 504
509 505 @util.propertycache
510 506 def fallbackheads(self):
511 507 """future remote heads if the changeset push fails"""
512 508 if self.revs is None:
513 509 # not target to push, all common are relevant
514 510 return self.outgoing.commonheads
515 511 unfi = self.repo.unfiltered()
516 512 # I want cheads = heads(::missingheads and ::commonheads)
517 513 # (missingheads is revs with secret changeset filtered out)
518 514 #
519 515 # This can be expressed as:
520 516 # cheads = ( (missingheads and ::commonheads)
521 517 # + (commonheads and ::missingheads))"
522 518 # )
523 519 #
524 520 # while trying to push we already computed the following:
525 521 # common = (::commonheads)
526 522 # missing = ((commonheads::missingheads) - commonheads)
527 523 #
528 524 # We can pick:
529 525 # * missingheads part of common (::commonheads)
530 526 common = self.outgoing.common
531 527 nm = self.repo.changelog.nodemap
532 528 cheads = [node for node in self.revs if nm[node] in common]
533 529 # and
534 530 # * commonheads parents on missing
535 531 revset = unfi.set(
536 532 b'%ln and parents(roots(%ln))',
537 533 self.outgoing.commonheads,
538 534 self.outgoing.missing,
539 535 )
540 536 cheads.extend(c.node() for c in revset)
541 537 return cheads
542 538
543 539 @property
544 540 def commonheads(self):
545 541 """set of all common heads after changeset bundle push"""
546 542 if self.cgresult:
547 543 return self.futureheads
548 544 else:
549 545 return self.fallbackheads
550 546
551 547
552 548 # mapping of message used when pushing bookmark
553 549 bookmsgmap = {
554 550 b'update': (
555 551 _(b"updating bookmark %s\n"),
556 552 _(b'updating bookmark %s failed!\n'),
557 553 ),
558 554 b'export': (
559 555 _(b"exporting bookmark %s\n"),
560 556 _(b'exporting bookmark %s failed!\n'),
561 557 ),
562 558 b'delete': (
563 559 _(b"deleting remote bookmark %s\n"),
564 560 _(b'deleting remote bookmark %s failed!\n'),
565 561 ),
566 562 }
567 563
568 564
569 565 def push(
570 566 repo,
571 567 remote,
572 568 force=False,
573 569 revs=None,
574 570 newbranch=False,
575 571 bookmarks=(),
576 572 publish=False,
577 573 opargs=None,
578 574 ):
579 575 '''Push outgoing changesets (limited by revs) from a local
580 576 repository to remote. Return an integer:
581 577 - None means nothing to push
582 578 - 0 means HTTP error
583 579 - 1 means we pushed and remote head count is unchanged *or*
584 580 we have outgoing changesets but refused to push
585 581 - other values as described by addchangegroup()
586 582 '''
587 583 if opargs is None:
588 584 opargs = {}
589 585 pushop = pushoperation(
590 586 repo,
591 587 remote,
592 588 force,
593 589 revs,
594 590 newbranch,
595 591 bookmarks,
596 592 publish,
597 593 **pycompat.strkwargs(opargs)
598 594 )
599 595 if pushop.remote.local():
600 596 missing = (
601 597 set(pushop.repo.requirements) - pushop.remote.local().supported
602 598 )
603 599 if missing:
604 600 msg = _(
605 601 b"required features are not"
606 602 b" supported in the destination:"
607 603 b" %s"
608 604 ) % (b', '.join(sorted(missing)))
609 605 raise error.Abort(msg)
610 606
611 607 if not pushop.remote.canpush():
612 608 raise error.Abort(_(b"destination does not support push"))
613 609
614 610 if not pushop.remote.capable(b'unbundle'):
615 611 raise error.Abort(
616 612 _(
617 613 b'cannot push: destination does not support the '
618 614 b'unbundle wire protocol command'
619 615 )
620 616 )
621 617
622 618 # get lock as we might write phase data
623 619 wlock = lock = None
624 620 try:
625 621 # bundle2 push may receive a reply bundle touching bookmarks
626 622 # requiring the wlock. Take it now to ensure proper ordering.
627 623 maypushback = pushop.ui.configbool(b'experimental', b'bundle2.pushback')
628 624 if (
629 625 (not _forcebundle1(pushop))
630 626 and maypushback
631 627 and not bookmod.bookmarksinstore(repo)
632 628 ):
633 629 wlock = pushop.repo.wlock()
634 630 lock = pushop.repo.lock()
635 631 pushop.trmanager = transactionmanager(
636 632 pushop.repo, b'push-response', pushop.remote.url()
637 633 )
638 634 except error.LockUnavailable as err:
639 635 # source repo cannot be locked.
640 636 # We do not abort the push, but just disable the local phase
641 637 # synchronisation.
642 638 msg = b'cannot lock source repository: %s\n' % stringutil.forcebytestr(
643 639 err
644 640 )
645 641 pushop.ui.debug(msg)
646 642
647 643 with wlock or util.nullcontextmanager():
648 644 with lock or util.nullcontextmanager():
649 645 with pushop.trmanager or util.nullcontextmanager():
650 646 pushop.repo.checkpush(pushop)
651 647 _checkpublish(pushop)
652 648 _pushdiscovery(pushop)
653 649 if not _forcebundle1(pushop):
654 650 _pushbundle2(pushop)
655 651 _pushchangeset(pushop)
656 652 _pushsyncphase(pushop)
657 653 _pushobsolete(pushop)
658 654 _pushbookmark(pushop)
659 655
660 656 if repo.ui.configbool(b'experimental', b'remotenames'):
661 657 logexchange.pullremotenames(repo, remote)
662 658
663 659 return pushop
664 660
665 661
666 662 # list of steps to perform discovery before push
667 663 pushdiscoveryorder = []
668 664
669 665 # Mapping between step name and function
670 666 #
671 667 # This exists to help extensions wrap steps if necessary
672 668 pushdiscoverymapping = {}
673 669
674 670
675 671 def pushdiscovery(stepname):
676 672 """decorator for function performing discovery before push
677 673
678 674 The function is added to the step -> function mapping and appended to the
679 675 list of steps. Beware that decorated function will be added in order (this
680 676 may matter).
681 677
682 678 You can only use this decorator for a new step, if you want to wrap a step
683 679 from an extension, change the pushdiscovery dictionary directly."""
684 680
685 681 def dec(func):
686 682 assert stepname not in pushdiscoverymapping
687 683 pushdiscoverymapping[stepname] = func
688 684 pushdiscoveryorder.append(stepname)
689 685 return func
690 686
691 687 return dec
692 688
693 689
694 690 def _pushdiscovery(pushop):
695 691 """Run all discovery steps"""
696 692 for stepname in pushdiscoveryorder:
697 693 step = pushdiscoverymapping[stepname]
698 694 step(pushop)
699 695
700 696
701 697 @pushdiscovery(b'changeset')
702 698 def _pushdiscoverychangeset(pushop):
703 699 """discover the changeset that need to be pushed"""
704 700 fci = discovery.findcommonincoming
705 701 if pushop.revs:
706 702 commoninc = fci(
707 703 pushop.repo,
708 704 pushop.remote,
709 705 force=pushop.force,
710 706 ancestorsof=pushop.revs,
711 707 )
712 708 else:
713 709 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
714 710 common, inc, remoteheads = commoninc
715 711 fco = discovery.findcommonoutgoing
716 712 outgoing = fco(
717 713 pushop.repo,
718 714 pushop.remote,
719 715 onlyheads=pushop.revs,
720 716 commoninc=commoninc,
721 717 force=pushop.force,
722 718 )
723 719 pushop.outgoing = outgoing
724 720 pushop.remoteheads = remoteheads
725 721 pushop.incoming = inc
726 722
727 723
728 724 @pushdiscovery(b'phase')
729 725 def _pushdiscoveryphase(pushop):
730 726 """discover the phase that needs to be pushed
731 727
732 728 (computed for both success and failure case for changesets push)"""
733 729 outgoing = pushop.outgoing
734 730 unfi = pushop.repo.unfiltered()
735 731 remotephases = listkeys(pushop.remote, b'phases')
736 732
737 733 if (
738 734 pushop.ui.configbool(b'ui', b'_usedassubrepo')
739 735 and remotephases # server supports phases
740 736 and not pushop.outgoing.missing # no changesets to be pushed
741 737 and remotephases.get(b'publishing', False)
742 738 ):
743 739 # When:
744 740 # - this is a subrepo push
745 741 # - and remote support phase
746 742 # - and no changeset are to be pushed
747 743 # - and remote is publishing
748 744 # We may be in issue 3781 case!
749 745 # We drop the possible phase synchronisation done by
750 746 # courtesy to publish changesets possibly locally draft
751 747 # on the remote.
752 748 pushop.outdatedphases = []
753 749 pushop.fallbackoutdatedphases = []
754 750 return
755 751
756 752 pushop.remotephases = phases.remotephasessummary(
757 753 pushop.repo, pushop.fallbackheads, remotephases
758 754 )
759 755 droots = pushop.remotephases.draftroots
760 756
761 757 extracond = b''
762 758 if not pushop.remotephases.publishing:
763 759 extracond = b' and public()'
764 760 revset = b'heads((%%ln::%%ln) %s)' % extracond
765 761 # Get the list of all revs draft on remote by public here.
766 762 # XXX Beware that revset break if droots is not strictly
767 763 # XXX root we may want to ensure it is but it is costly
768 764 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
769 765 if not pushop.remotephases.publishing and pushop.publish:
770 766 future = list(
771 767 unfi.set(
772 768 b'%ln and (not public() or %ln::)', pushop.futureheads, droots
773 769 )
774 770 )
775 771 elif not outgoing.missing:
776 772 future = fallback
777 773 else:
778 774 # adds changeset we are going to push as draft
779 775 #
780 776 # should not be necessary for publishing server, but because of an
781 777 # issue fixed in xxxxx we have to do it anyway.
782 778 fdroots = list(
783 779 unfi.set(b'roots(%ln + %ln::)', outgoing.missing, droots)
784 780 )
785 781 fdroots = [f.node() for f in fdroots]
786 782 future = list(unfi.set(revset, fdroots, pushop.futureheads))
787 783 pushop.outdatedphases = future
788 784 pushop.fallbackoutdatedphases = fallback
789 785
790 786
791 787 @pushdiscovery(b'obsmarker')
792 788 def _pushdiscoveryobsmarkers(pushop):
793 789 if not obsolete.isenabled(pushop.repo, obsolete.exchangeopt):
794 790 return
795 791
796 792 if not pushop.repo.obsstore:
797 793 return
798 794
799 795 if b'obsolete' not in listkeys(pushop.remote, b'namespaces'):
800 796 return
801 797
802 798 repo = pushop.repo
803 799 # very naive computation, that can be quite expensive on big repo.
804 800 # However: evolution is currently slow on them anyway.
805 801 nodes = (c.node() for c in repo.set(b'::%ln', pushop.futureheads))
806 802 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
807 803
808 804
809 805 @pushdiscovery(b'bookmarks')
810 806 def _pushdiscoverybookmarks(pushop):
811 807 ui = pushop.ui
812 808 repo = pushop.repo.unfiltered()
813 809 remote = pushop.remote
814 810 ui.debug(b"checking for updated bookmarks\n")
815 811 ancestors = ()
816 812 if pushop.revs:
817 813 revnums = pycompat.maplist(repo.changelog.rev, pushop.revs)
818 814 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
819 815
820 816 remotebookmark = bookmod.unhexlifybookmarks(listkeys(remote, b'bookmarks'))
821 817
822 818 explicit = {
823 819 repo._bookmarks.expandname(bookmark) for bookmark in pushop.bookmarks
824 820 }
825 821
826 822 comp = bookmod.comparebookmarks(repo, repo._bookmarks, remotebookmark)
827 823 return _processcompared(pushop, ancestors, explicit, remotebookmark, comp)
828 824
829 825
830 826 def _processcompared(pushop, pushed, explicit, remotebms, comp):
831 827 """take decision on bookmarks to push to the remote repo
832 828
833 829 Exists to help extensions alter this behavior.
834 830 """
835 831 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
836 832
837 833 repo = pushop.repo
838 834
839 835 for b, scid, dcid in advsrc:
840 836 if b in explicit:
841 837 explicit.remove(b)
842 838 if not pushed or repo[scid].rev() in pushed:
843 839 pushop.outbookmarks.append((b, dcid, scid))
844 840 # search added bookmark
845 841 for b, scid, dcid in addsrc:
846 842 if b in explicit:
847 843 explicit.remove(b)
848 844 pushop.outbookmarks.append((b, b'', scid))
849 845 # search for overwritten bookmark
850 846 for b, scid, dcid in list(advdst) + list(diverge) + list(differ):
851 847 if b in explicit:
852 848 explicit.remove(b)
853 849 pushop.outbookmarks.append((b, dcid, scid))
854 850 # search for bookmark to delete
855 851 for b, scid, dcid in adddst:
856 852 if b in explicit:
857 853 explicit.remove(b)
858 854 # treat as "deleted locally"
859 855 pushop.outbookmarks.append((b, dcid, b''))
860 856 # identical bookmarks shouldn't get reported
861 857 for b, scid, dcid in same:
862 858 if b in explicit:
863 859 explicit.remove(b)
864 860
865 861 if explicit:
866 862 explicit = sorted(explicit)
867 863 # we should probably list all of them
868 864 pushop.ui.warn(
869 865 _(
870 866 b'bookmark %s does not exist on the local '
871 867 b'or remote repository!\n'
872 868 )
873 869 % explicit[0]
874 870 )
875 871 pushop.bkresult = 2
876 872
877 873 pushop.outbookmarks.sort()
878 874
879 875
880 876 def _pushcheckoutgoing(pushop):
881 877 outgoing = pushop.outgoing
882 878 unfi = pushop.repo.unfiltered()
883 879 if not outgoing.missing:
884 880 # nothing to push
885 881 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
886 882 return False
887 883 # something to push
888 884 if not pushop.force:
889 885 # if repo.obsstore == False --> no obsolete
890 886 # then, save the iteration
891 887 if unfi.obsstore:
892 888 # this message are here for 80 char limit reason
893 889 mso = _(b"push includes obsolete changeset: %s!")
894 890 mspd = _(b"push includes phase-divergent changeset: %s!")
895 891 mscd = _(b"push includes content-divergent changeset: %s!")
896 892 mst = {
897 893 b"orphan": _(b"push includes orphan changeset: %s!"),
898 894 b"phase-divergent": mspd,
899 895 b"content-divergent": mscd,
900 896 }
901 897 # If we are to push if there is at least one
902 898 # obsolete or unstable changeset in missing, at
903 899 # least one of the missinghead will be obsolete or
904 900 # unstable. So checking heads only is ok
905 901 for node in outgoing.missingheads:
906 902 ctx = unfi[node]
907 903 if ctx.obsolete():
908 904 raise error.Abort(mso % ctx)
909 905 elif ctx.isunstable():
910 906 # TODO print more than one instability in the abort
911 907 # message
912 908 raise error.Abort(mst[ctx.instabilities()[0]] % ctx)
913 909
914 910 discovery.checkheads(pushop)
915 911 return True
916 912
917 913
918 914 # List of names of steps to perform for an outgoing bundle2, order matters.
919 915 b2partsgenorder = []
920 916
921 917 # Mapping between step name and function
922 918 #
923 919 # This exists to help extensions wrap steps if necessary
924 920 b2partsgenmapping = {}
925 921
926 922
927 923 def b2partsgenerator(stepname, idx=None):
928 924 """decorator for function generating bundle2 part
929 925
930 926 The function is added to the step -> function mapping and appended to the
931 927 list of steps. Beware that decorated functions will be added in order
932 928 (this may matter).
933 929
934 930 You can only use this decorator for new steps, if you want to wrap a step
935 931 from an extension, attack the b2partsgenmapping dictionary directly."""
936 932
937 933 def dec(func):
938 934 assert stepname not in b2partsgenmapping
939 935 b2partsgenmapping[stepname] = func
940 936 if idx is None:
941 937 b2partsgenorder.append(stepname)
942 938 else:
943 939 b2partsgenorder.insert(idx, stepname)
944 940 return func
945 941
946 942 return dec
947 943
948 944
949 945 def _pushb2ctxcheckheads(pushop, bundler):
950 946 """Generate race condition checking parts
951 947
952 948 Exists as an independent function to aid extensions
953 949 """
954 950 # * 'force' do not check for push race,
955 951 # * if we don't push anything, there are nothing to check.
956 952 if not pushop.force and pushop.outgoing.missingheads:
957 953 allowunrelated = b'related' in bundler.capabilities.get(
958 954 b'checkheads', ()
959 955 )
960 956 emptyremote = pushop.pushbranchmap is None
961 957 if not allowunrelated or emptyremote:
962 958 bundler.newpart(b'check:heads', data=iter(pushop.remoteheads))
963 959 else:
964 960 affected = set()
965 961 for branch, heads in pycompat.iteritems(pushop.pushbranchmap):
966 962 remoteheads, newheads, unsyncedheads, discardedheads = heads
967 963 if remoteheads is not None:
968 964 remote = set(remoteheads)
969 965 affected |= set(discardedheads) & remote
970 966 affected |= remote - set(newheads)
971 967 if affected:
972 968 data = iter(sorted(affected))
973 969 bundler.newpart(b'check:updated-heads', data=data)
974 970
975 971
976 972 def _pushing(pushop):
977 973 """return True if we are pushing anything"""
978 974 return bool(
979 975 pushop.outgoing.missing
980 976 or pushop.outdatedphases
981 977 or pushop.outobsmarkers
982 978 or pushop.outbookmarks
983 979 )
984 980
985 981
986 982 @b2partsgenerator(b'check-bookmarks')
987 983 def _pushb2checkbookmarks(pushop, bundler):
988 984 """insert bookmark move checking"""
989 985 if not _pushing(pushop) or pushop.force:
990 986 return
991 987 b2caps = bundle2.bundle2caps(pushop.remote)
992 988 hasbookmarkcheck = b'bookmarks' in b2caps
993 989 if not (pushop.outbookmarks and hasbookmarkcheck):
994 990 return
995 991 data = []
996 992 for book, old, new in pushop.outbookmarks:
997 993 data.append((book, old))
998 994 checkdata = bookmod.binaryencode(data)
999 995 bundler.newpart(b'check:bookmarks', data=checkdata)
1000 996
1001 997
1002 998 @b2partsgenerator(b'check-phases')
1003 999 def _pushb2checkphases(pushop, bundler):
1004 1000 """insert phase move checking"""
1005 1001 if not _pushing(pushop) or pushop.force:
1006 1002 return
1007 1003 b2caps = bundle2.bundle2caps(pushop.remote)
1008 1004 hasphaseheads = b'heads' in b2caps.get(b'phases', ())
1009 1005 if pushop.remotephases is not None and hasphaseheads:
1010 1006 # check that the remote phase has not changed
1011 1007 checks = [[] for p in phases.allphases]
1012 1008 checks[phases.public].extend(pushop.remotephases.publicheads)
1013 1009 checks[phases.draft].extend(pushop.remotephases.draftroots)
1014 1010 if any(checks):
1015 1011 for nodes in checks:
1016 1012 nodes.sort()
1017 1013 checkdata = phases.binaryencode(checks)
1018 1014 bundler.newpart(b'check:phases', data=checkdata)
1019 1015
1020 1016
1021 1017 @b2partsgenerator(b'changeset')
1022 1018 def _pushb2ctx(pushop, bundler):
1023 1019 """handle changegroup push through bundle2
1024 1020
1025 1021 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
1026 1022 """
1027 1023 if b'changesets' in pushop.stepsdone:
1028 1024 return
1029 1025 pushop.stepsdone.add(b'changesets')
1030 1026 # Send known heads to the server for race detection.
1031 1027 if not _pushcheckoutgoing(pushop):
1032 1028 return
1033 1029 pushop.repo.prepushoutgoinghooks(pushop)
1034 1030
1035 1031 _pushb2ctxcheckheads(pushop, bundler)
1036 1032
1037 1033 b2caps = bundle2.bundle2caps(pushop.remote)
1038 1034 version = b'01'
1039 1035 cgversions = b2caps.get(b'changegroup')
1040 1036 if cgversions: # 3.1 and 3.2 ship with an empty value
1041 1037 cgversions = [
1042 1038 v
1043 1039 for v in cgversions
1044 1040 if v in changegroup.supportedoutgoingversions(pushop.repo)
1045 1041 ]
1046 1042 if not cgversions:
1047 1043 raise error.Abort(_(b'no common changegroup version'))
1048 1044 version = max(cgversions)
1049 1045 cgstream = changegroup.makestream(
1050 1046 pushop.repo, pushop.outgoing, version, b'push'
1051 1047 )
1052 1048 cgpart = bundler.newpart(b'changegroup', data=cgstream)
1053 1049 if cgversions:
1054 1050 cgpart.addparam(b'version', version)
1055 1051 if b'treemanifest' in pushop.repo.requirements:
1056 1052 cgpart.addparam(b'treemanifest', b'1')
1057 1053 if b'exp-sidedata-flag' in pushop.repo.requirements:
1058 1054 cgpart.addparam(b'exp-sidedata', b'1')
1059 1055
1060 1056 def handlereply(op):
1061 1057 """extract addchangegroup returns from server reply"""
1062 1058 cgreplies = op.records.getreplies(cgpart.id)
1063 1059 assert len(cgreplies[b'changegroup']) == 1
1064 1060 pushop.cgresult = cgreplies[b'changegroup'][0][b'return']
1065 1061
1066 1062 return handlereply
1067 1063
1068 1064
1069 1065 @b2partsgenerator(b'phase')
1070 1066 def _pushb2phases(pushop, bundler):
1071 1067 """handle phase push through bundle2"""
1072 1068 if b'phases' in pushop.stepsdone:
1073 1069 return
1074 1070 b2caps = bundle2.bundle2caps(pushop.remote)
1075 1071 ui = pushop.repo.ui
1076 1072
1077 1073 legacyphase = b'phases' in ui.configlist(b'devel', b'legacy.exchange')
1078 1074 haspushkey = b'pushkey' in b2caps
1079 1075 hasphaseheads = b'heads' in b2caps.get(b'phases', ())
1080 1076
1081 1077 if hasphaseheads and not legacyphase:
1082 1078 return _pushb2phaseheads(pushop, bundler)
1083 1079 elif haspushkey:
1084 1080 return _pushb2phasespushkey(pushop, bundler)
1085 1081
1086 1082
1087 1083 def _pushb2phaseheads(pushop, bundler):
1088 1084 """push phase information through a bundle2 - binary part"""
1089 1085 pushop.stepsdone.add(b'phases')
1090 1086 if pushop.outdatedphases:
1091 1087 updates = [[] for p in phases.allphases]
1092 1088 updates[0].extend(h.node() for h in pushop.outdatedphases)
1093 1089 phasedata = phases.binaryencode(updates)
1094 1090 bundler.newpart(b'phase-heads', data=phasedata)
1095 1091
1096 1092
1097 1093 def _pushb2phasespushkey(pushop, bundler):
1098 1094 """push phase information through a bundle2 - pushkey part"""
1099 1095 pushop.stepsdone.add(b'phases')
1100 1096 part2node = []
1101 1097
1102 1098 def handlefailure(pushop, exc):
1103 1099 targetid = int(exc.partid)
1104 1100 for partid, node in part2node:
1105 1101 if partid == targetid:
1106 1102 raise error.Abort(_(b'updating %s to public failed') % node)
1107 1103
1108 1104 enc = pushkey.encode
1109 1105 for newremotehead in pushop.outdatedphases:
1110 1106 part = bundler.newpart(b'pushkey')
1111 1107 part.addparam(b'namespace', enc(b'phases'))
1112 1108 part.addparam(b'key', enc(newremotehead.hex()))
1113 1109 part.addparam(b'old', enc(b'%d' % phases.draft))
1114 1110 part.addparam(b'new', enc(b'%d' % phases.public))
1115 1111 part2node.append((part.id, newremotehead))
1116 1112 pushop.pkfailcb[part.id] = handlefailure
1117 1113
1118 1114 def handlereply(op):
1119 1115 for partid, node in part2node:
1120 1116 partrep = op.records.getreplies(partid)
1121 1117 results = partrep[b'pushkey']
1122 1118 assert len(results) <= 1
1123 1119 msg = None
1124 1120 if not results:
1125 1121 msg = _(b'server ignored update of %s to public!\n') % node
1126 1122 elif not int(results[0][b'return']):
1127 1123 msg = _(b'updating %s to public failed!\n') % node
1128 1124 if msg is not None:
1129 1125 pushop.ui.warn(msg)
1130 1126
1131 1127 return handlereply
1132 1128
1133 1129
1134 1130 @b2partsgenerator(b'obsmarkers')
1135 1131 def _pushb2obsmarkers(pushop, bundler):
1136 1132 if b'obsmarkers' in pushop.stepsdone:
1137 1133 return
1138 1134 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
1139 1135 if obsolete.commonversion(remoteversions) is None:
1140 1136 return
1141 1137 pushop.stepsdone.add(b'obsmarkers')
1142 1138 if pushop.outobsmarkers:
1143 markers = _sortedmarkers(pushop.outobsmarkers)
1139 markers = obsutil.sortedmarkers(pushop.outobsmarkers)
1144 1140 bundle2.buildobsmarkerspart(bundler, markers)
1145 1141
1146 1142
1147 1143 @b2partsgenerator(b'bookmarks')
1148 1144 def _pushb2bookmarks(pushop, bundler):
1149 1145 """handle bookmark push through bundle2"""
1150 1146 if b'bookmarks' in pushop.stepsdone:
1151 1147 return
1152 1148 b2caps = bundle2.bundle2caps(pushop.remote)
1153 1149
1154 1150 legacy = pushop.repo.ui.configlist(b'devel', b'legacy.exchange')
1155 1151 legacybooks = b'bookmarks' in legacy
1156 1152
1157 1153 if not legacybooks and b'bookmarks' in b2caps:
1158 1154 return _pushb2bookmarkspart(pushop, bundler)
1159 1155 elif b'pushkey' in b2caps:
1160 1156 return _pushb2bookmarkspushkey(pushop, bundler)
1161 1157
1162 1158
1163 1159 def _bmaction(old, new):
1164 1160 """small utility for bookmark pushing"""
1165 1161 if not old:
1166 1162 return b'export'
1167 1163 elif not new:
1168 1164 return b'delete'
1169 1165 return b'update'
1170 1166
1171 1167
1172 1168 def _abortonsecretctx(pushop, node, b):
1173 1169 """abort if a given bookmark points to a secret changeset"""
1174 1170 if node and pushop.repo[node].phase() == phases.secret:
1175 1171 raise error.Abort(
1176 1172 _(b'cannot push bookmark %s as it points to a secret changeset') % b
1177 1173 )
1178 1174
1179 1175
1180 1176 def _pushb2bookmarkspart(pushop, bundler):
1181 1177 pushop.stepsdone.add(b'bookmarks')
1182 1178 if not pushop.outbookmarks:
1183 1179 return
1184 1180
1185 1181 allactions = []
1186 1182 data = []
1187 1183 for book, old, new in pushop.outbookmarks:
1188 1184 _abortonsecretctx(pushop, new, book)
1189 1185 data.append((book, new))
1190 1186 allactions.append((book, _bmaction(old, new)))
1191 1187 checkdata = bookmod.binaryencode(data)
1192 1188 bundler.newpart(b'bookmarks', data=checkdata)
1193 1189
1194 1190 def handlereply(op):
1195 1191 ui = pushop.ui
1196 1192 # if success
1197 1193 for book, action in allactions:
1198 1194 ui.status(bookmsgmap[action][0] % book)
1199 1195
1200 1196 return handlereply
1201 1197
1202 1198
1203 1199 def _pushb2bookmarkspushkey(pushop, bundler):
1204 1200 pushop.stepsdone.add(b'bookmarks')
1205 1201 part2book = []
1206 1202 enc = pushkey.encode
1207 1203
1208 1204 def handlefailure(pushop, exc):
1209 1205 targetid = int(exc.partid)
1210 1206 for partid, book, action in part2book:
1211 1207 if partid == targetid:
1212 1208 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
1213 1209 # we should not be called for part we did not generated
1214 1210 assert False
1215 1211
1216 1212 for book, old, new in pushop.outbookmarks:
1217 1213 _abortonsecretctx(pushop, new, book)
1218 1214 part = bundler.newpart(b'pushkey')
1219 1215 part.addparam(b'namespace', enc(b'bookmarks'))
1220 1216 part.addparam(b'key', enc(book))
1221 1217 part.addparam(b'old', enc(hex(old)))
1222 1218 part.addparam(b'new', enc(hex(new)))
1223 1219 action = b'update'
1224 1220 if not old:
1225 1221 action = b'export'
1226 1222 elif not new:
1227 1223 action = b'delete'
1228 1224 part2book.append((part.id, book, action))
1229 1225 pushop.pkfailcb[part.id] = handlefailure
1230 1226
1231 1227 def handlereply(op):
1232 1228 ui = pushop.ui
1233 1229 for partid, book, action in part2book:
1234 1230 partrep = op.records.getreplies(partid)
1235 1231 results = partrep[b'pushkey']
1236 1232 assert len(results) <= 1
1237 1233 if not results:
1238 1234 pushop.ui.warn(_(b'server ignored bookmark %s update\n') % book)
1239 1235 else:
1240 1236 ret = int(results[0][b'return'])
1241 1237 if ret:
1242 1238 ui.status(bookmsgmap[action][0] % book)
1243 1239 else:
1244 1240 ui.warn(bookmsgmap[action][1] % book)
1245 1241 if pushop.bkresult is not None:
1246 1242 pushop.bkresult = 1
1247 1243
1248 1244 return handlereply
1249 1245
1250 1246
1251 1247 @b2partsgenerator(b'pushvars', idx=0)
1252 1248 def _getbundlesendvars(pushop, bundler):
1253 1249 '''send shellvars via bundle2'''
1254 1250 pushvars = pushop.pushvars
1255 1251 if pushvars:
1256 1252 shellvars = {}
1257 1253 for raw in pushvars:
1258 1254 if b'=' not in raw:
1259 1255 msg = (
1260 1256 b"unable to parse variable '%s', should follow "
1261 1257 b"'KEY=VALUE' or 'KEY=' format"
1262 1258 )
1263 1259 raise error.Abort(msg % raw)
1264 1260 k, v = raw.split(b'=', 1)
1265 1261 shellvars[k] = v
1266 1262
1267 1263 part = bundler.newpart(b'pushvars')
1268 1264
1269 1265 for key, value in pycompat.iteritems(shellvars):
1270 1266 part.addparam(key, value, mandatory=False)
1271 1267
1272 1268
1273 1269 def _pushbundle2(pushop):
1274 1270 """push data to the remote using bundle2
1275 1271
1276 1272 The only currently supported type of data is changegroup but this will
1277 1273 evolve in the future."""
1278 1274 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
1279 1275 pushback = pushop.trmanager and pushop.ui.configbool(
1280 1276 b'experimental', b'bundle2.pushback'
1281 1277 )
1282 1278
1283 1279 # create reply capability
1284 1280 capsblob = bundle2.encodecaps(
1285 1281 bundle2.getrepocaps(pushop.repo, allowpushback=pushback, role=b'client')
1286 1282 )
1287 1283 bundler.newpart(b'replycaps', data=capsblob)
1288 1284 replyhandlers = []
1289 1285 for partgenname in b2partsgenorder:
1290 1286 partgen = b2partsgenmapping[partgenname]
1291 1287 ret = partgen(pushop, bundler)
1292 1288 if callable(ret):
1293 1289 replyhandlers.append(ret)
1294 1290 # do not push if nothing to push
1295 1291 if bundler.nbparts <= 1:
1296 1292 return
1297 1293 stream = util.chunkbuffer(bundler.getchunks())
1298 1294 try:
1299 1295 try:
1300 1296 with pushop.remote.commandexecutor() as e:
1301 1297 reply = e.callcommand(
1302 1298 b'unbundle',
1303 1299 {
1304 1300 b'bundle': stream,
1305 1301 b'heads': [b'force'],
1306 1302 b'url': pushop.remote.url(),
1307 1303 },
1308 1304 ).result()
1309 1305 except error.BundleValueError as exc:
1310 1306 raise error.Abort(_(b'missing support for %s') % exc)
1311 1307 try:
1312 1308 trgetter = None
1313 1309 if pushback:
1314 1310 trgetter = pushop.trmanager.transaction
1315 1311 op = bundle2.processbundle(pushop.repo, reply, trgetter)
1316 1312 except error.BundleValueError as exc:
1317 1313 raise error.Abort(_(b'missing support for %s') % exc)
1318 1314 except bundle2.AbortFromPart as exc:
1319 1315 pushop.ui.status(_(b'remote: %s\n') % exc)
1320 1316 if exc.hint is not None:
1321 1317 pushop.ui.status(_(b'remote: %s\n') % (b'(%s)' % exc.hint))
1322 1318 raise error.Abort(_(b'push failed on remote'))
1323 1319 except error.PushkeyFailed as exc:
1324 1320 partid = int(exc.partid)
1325 1321 if partid not in pushop.pkfailcb:
1326 1322 raise
1327 1323 pushop.pkfailcb[partid](pushop, exc)
1328 1324 for rephand in replyhandlers:
1329 1325 rephand(op)
1330 1326
1331 1327
1332 1328 def _pushchangeset(pushop):
1333 1329 """Make the actual push of changeset bundle to remote repo"""
1334 1330 if b'changesets' in pushop.stepsdone:
1335 1331 return
1336 1332 pushop.stepsdone.add(b'changesets')
1337 1333 if not _pushcheckoutgoing(pushop):
1338 1334 return
1339 1335
1340 1336 # Should have verified this in push().
1341 1337 assert pushop.remote.capable(b'unbundle')
1342 1338
1343 1339 pushop.repo.prepushoutgoinghooks(pushop)
1344 1340 outgoing = pushop.outgoing
1345 1341 # TODO: get bundlecaps from remote
1346 1342 bundlecaps = None
1347 1343 # create a changegroup from local
1348 1344 if pushop.revs is None and not (
1349 1345 outgoing.excluded or pushop.repo.changelog.filteredrevs
1350 1346 ):
1351 1347 # push everything,
1352 1348 # use the fast path, no race possible on push
1353 1349 cg = changegroup.makechangegroup(
1354 1350 pushop.repo,
1355 1351 outgoing,
1356 1352 b'01',
1357 1353 b'push',
1358 1354 fastpath=True,
1359 1355 bundlecaps=bundlecaps,
1360 1356 )
1361 1357 else:
1362 1358 cg = changegroup.makechangegroup(
1363 1359 pushop.repo, outgoing, b'01', b'push', bundlecaps=bundlecaps
1364 1360 )
1365 1361
1366 1362 # apply changegroup to remote
1367 1363 # local repo finds heads on server, finds out what
1368 1364 # revs it must push. once revs transferred, if server
1369 1365 # finds it has different heads (someone else won
1370 1366 # commit/push race), server aborts.
1371 1367 if pushop.force:
1372 1368 remoteheads = [b'force']
1373 1369 else:
1374 1370 remoteheads = pushop.remoteheads
1375 1371 # ssh: return remote's addchangegroup()
1376 1372 # http: return remote's addchangegroup() or 0 for error
1377 1373 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads, pushop.repo.url())
1378 1374
1379 1375
1380 1376 def _pushsyncphase(pushop):
1381 1377 """synchronise phase information locally and remotely"""
1382 1378 cheads = pushop.commonheads
1383 1379 # even when we don't push, exchanging phase data is useful
1384 1380 remotephases = listkeys(pushop.remote, b'phases')
1385 1381 if (
1386 1382 pushop.ui.configbool(b'ui', b'_usedassubrepo')
1387 1383 and remotephases # server supports phases
1388 1384 and pushop.cgresult is None # nothing was pushed
1389 1385 and remotephases.get(b'publishing', False)
1390 1386 ):
1391 1387 # When:
1392 1388 # - this is a subrepo push
1393 1389 # - and remote support phase
1394 1390 # - and no changeset was pushed
1395 1391 # - and remote is publishing
1396 1392 # We may be in issue 3871 case!
1397 1393 # We drop the possible phase synchronisation done by
1398 1394 # courtesy to publish changesets possibly locally draft
1399 1395 # on the remote.
1400 1396 remotephases = {b'publishing': b'True'}
1401 1397 if not remotephases: # old server or public only reply from non-publishing
1402 1398 _localphasemove(pushop, cheads)
1403 1399 # don't push any phase data as there is nothing to push
1404 1400 else:
1405 1401 ana = phases.analyzeremotephases(pushop.repo, cheads, remotephases)
1406 1402 pheads, droots = ana
1407 1403 ### Apply remote phase on local
1408 1404 if remotephases.get(b'publishing', False):
1409 1405 _localphasemove(pushop, cheads)
1410 1406 else: # publish = False
1411 1407 _localphasemove(pushop, pheads)
1412 1408 _localphasemove(pushop, cheads, phases.draft)
1413 1409 ### Apply local phase on remote
1414 1410
1415 1411 if pushop.cgresult:
1416 1412 if b'phases' in pushop.stepsdone:
1417 1413 # phases already pushed though bundle2
1418 1414 return
1419 1415 outdated = pushop.outdatedphases
1420 1416 else:
1421 1417 outdated = pushop.fallbackoutdatedphases
1422 1418
1423 1419 pushop.stepsdone.add(b'phases')
1424 1420
1425 1421 # filter heads already turned public by the push
1426 1422 outdated = [c for c in outdated if c.node() not in pheads]
1427 1423 # fallback to independent pushkey command
1428 1424 for newremotehead in outdated:
1429 1425 with pushop.remote.commandexecutor() as e:
1430 1426 r = e.callcommand(
1431 1427 b'pushkey',
1432 1428 {
1433 1429 b'namespace': b'phases',
1434 1430 b'key': newremotehead.hex(),
1435 1431 b'old': b'%d' % phases.draft,
1436 1432 b'new': b'%d' % phases.public,
1437 1433 },
1438 1434 ).result()
1439 1435
1440 1436 if not r:
1441 1437 pushop.ui.warn(
1442 1438 _(b'updating %s to public failed!\n') % newremotehead
1443 1439 )
1444 1440
1445 1441
1446 1442 def _localphasemove(pushop, nodes, phase=phases.public):
1447 1443 """move <nodes> to <phase> in the local source repo"""
1448 1444 if pushop.trmanager:
1449 1445 phases.advanceboundary(
1450 1446 pushop.repo, pushop.trmanager.transaction(), phase, nodes
1451 1447 )
1452 1448 else:
1453 1449 # repo is not locked, do not change any phases!
1454 1450 # Informs the user that phases should have been moved when
1455 1451 # applicable.
1456 1452 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
1457 1453 phasestr = phases.phasenames[phase]
1458 1454 if actualmoves:
1459 1455 pushop.ui.status(
1460 1456 _(
1461 1457 b'cannot lock source repo, skipping '
1462 1458 b'local %s phase update\n'
1463 1459 )
1464 1460 % phasestr
1465 1461 )
1466 1462
1467 1463
1468 1464 def _pushobsolete(pushop):
1469 1465 """utility function to push obsolete markers to a remote"""
1470 1466 if b'obsmarkers' in pushop.stepsdone:
1471 1467 return
1472 1468 repo = pushop.repo
1473 1469 remote = pushop.remote
1474 1470 pushop.stepsdone.add(b'obsmarkers')
1475 1471 if pushop.outobsmarkers:
1476 1472 pushop.ui.debug(b'try to push obsolete markers to remote\n')
1477 1473 rslts = []
1478 markers = _sortedmarkers(pushop.outobsmarkers)
1474 markers = obsutil.sortedmarkers(pushop.outobsmarkers)
1479 1475 remotedata = obsolete._pushkeyescape(markers)
1480 1476 for key in sorted(remotedata, reverse=True):
1481 1477 # reverse sort to ensure we end with dump0
1482 1478 data = remotedata[key]
1483 1479 rslts.append(remote.pushkey(b'obsolete', key, b'', data))
1484 1480 if [r for r in rslts if not r]:
1485 1481 msg = _(b'failed to push some obsolete markers!\n')
1486 1482 repo.ui.warn(msg)
1487 1483
1488 1484
1489 1485 def _pushbookmark(pushop):
1490 1486 """Update bookmark position on remote"""
1491 1487 if pushop.cgresult == 0 or b'bookmarks' in pushop.stepsdone:
1492 1488 return
1493 1489 pushop.stepsdone.add(b'bookmarks')
1494 1490 ui = pushop.ui
1495 1491 remote = pushop.remote
1496 1492
1497 1493 for b, old, new in pushop.outbookmarks:
1498 1494 action = b'update'
1499 1495 if not old:
1500 1496 action = b'export'
1501 1497 elif not new:
1502 1498 action = b'delete'
1503 1499
1504 1500 with remote.commandexecutor() as e:
1505 1501 r = e.callcommand(
1506 1502 b'pushkey',
1507 1503 {
1508 1504 b'namespace': b'bookmarks',
1509 1505 b'key': b,
1510 1506 b'old': hex(old),
1511 1507 b'new': hex(new),
1512 1508 },
1513 1509 ).result()
1514 1510
1515 1511 if r:
1516 1512 ui.status(bookmsgmap[action][0] % b)
1517 1513 else:
1518 1514 ui.warn(bookmsgmap[action][1] % b)
1519 1515 # discovery can have set the value form invalid entry
1520 1516 if pushop.bkresult is not None:
1521 1517 pushop.bkresult = 1
1522 1518
1523 1519
1524 1520 class pulloperation(object):
1525 1521 """A object that represent a single pull operation
1526 1522
1527 1523 It purpose is to carry pull related state and very common operation.
1528 1524
1529 1525 A new should be created at the beginning of each pull and discarded
1530 1526 afterward.
1531 1527 """
1532 1528
1533 1529 def __init__(
1534 1530 self,
1535 1531 repo,
1536 1532 remote,
1537 1533 heads=None,
1538 1534 force=False,
1539 1535 bookmarks=(),
1540 1536 remotebookmarks=None,
1541 1537 streamclonerequested=None,
1542 1538 includepats=None,
1543 1539 excludepats=None,
1544 1540 depth=None,
1545 1541 ):
1546 1542 # repo we pull into
1547 1543 self.repo = repo
1548 1544 # repo we pull from
1549 1545 self.remote = remote
1550 1546 # revision we try to pull (None is "all")
1551 1547 self.heads = heads
1552 1548 # bookmark pulled explicitly
1553 1549 self.explicitbookmarks = [
1554 1550 repo._bookmarks.expandname(bookmark) for bookmark in bookmarks
1555 1551 ]
1556 1552 # do we force pull?
1557 1553 self.force = force
1558 1554 # whether a streaming clone was requested
1559 1555 self.streamclonerequested = streamclonerequested
1560 1556 # transaction manager
1561 1557 self.trmanager = None
1562 1558 # set of common changeset between local and remote before pull
1563 1559 self.common = None
1564 1560 # set of pulled head
1565 1561 self.rheads = None
1566 1562 # list of missing changeset to fetch remotely
1567 1563 self.fetch = None
1568 1564 # remote bookmarks data
1569 1565 self.remotebookmarks = remotebookmarks
1570 1566 # result of changegroup pulling (used as return code by pull)
1571 1567 self.cgresult = None
1572 1568 # list of step already done
1573 1569 self.stepsdone = set()
1574 1570 # Whether we attempted a clone from pre-generated bundles.
1575 1571 self.clonebundleattempted = False
1576 1572 # Set of file patterns to include.
1577 1573 self.includepats = includepats
1578 1574 # Set of file patterns to exclude.
1579 1575 self.excludepats = excludepats
1580 1576 # Number of ancestor changesets to pull from each pulled head.
1581 1577 self.depth = depth
1582 1578
1583 1579 @util.propertycache
1584 1580 def pulledsubset(self):
1585 1581 """heads of the set of changeset target by the pull"""
1586 1582 # compute target subset
1587 1583 if self.heads is None:
1588 1584 # We pulled every thing possible
1589 1585 # sync on everything common
1590 1586 c = set(self.common)
1591 1587 ret = list(self.common)
1592 1588 for n in self.rheads:
1593 1589 if n not in c:
1594 1590 ret.append(n)
1595 1591 return ret
1596 1592 else:
1597 1593 # We pulled a specific subset
1598 1594 # sync on this subset
1599 1595 return self.heads
1600 1596
1601 1597 @util.propertycache
1602 1598 def canusebundle2(self):
1603 1599 return not _forcebundle1(self)
1604 1600
1605 1601 @util.propertycache
1606 1602 def remotebundle2caps(self):
1607 1603 return bundle2.bundle2caps(self.remote)
1608 1604
1609 1605 def gettransaction(self):
1610 1606 # deprecated; talk to trmanager directly
1611 1607 return self.trmanager.transaction()
1612 1608
1613 1609
1614 1610 class transactionmanager(util.transactional):
1615 1611 """An object to manage the life cycle of a transaction
1616 1612
1617 1613 It creates the transaction on demand and calls the appropriate hooks when
1618 1614 closing the transaction."""
1619 1615
1620 1616 def __init__(self, repo, source, url):
1621 1617 self.repo = repo
1622 1618 self.source = source
1623 1619 self.url = url
1624 1620 self._tr = None
1625 1621
1626 1622 def transaction(self):
1627 1623 """Return an open transaction object, constructing if necessary"""
1628 1624 if not self._tr:
1629 1625 trname = b'%s\n%s' % (self.source, util.hidepassword(self.url))
1630 1626 self._tr = self.repo.transaction(trname)
1631 1627 self._tr.hookargs[b'source'] = self.source
1632 1628 self._tr.hookargs[b'url'] = self.url
1633 1629 return self._tr
1634 1630
1635 1631 def close(self):
1636 1632 """close transaction if created"""
1637 1633 if self._tr is not None:
1638 1634 self._tr.close()
1639 1635
1640 1636 def release(self):
1641 1637 """release transaction if created"""
1642 1638 if self._tr is not None:
1643 1639 self._tr.release()
1644 1640
1645 1641
1646 1642 def listkeys(remote, namespace):
1647 1643 with remote.commandexecutor() as e:
1648 1644 return e.callcommand(b'listkeys', {b'namespace': namespace}).result()
1649 1645
1650 1646
1651 1647 def _fullpullbundle2(repo, pullop):
1652 1648 # The server may send a partial reply, i.e. when inlining
1653 1649 # pre-computed bundles. In that case, update the common
1654 1650 # set based on the results and pull another bundle.
1655 1651 #
1656 1652 # There are two indicators that the process is finished:
1657 1653 # - no changeset has been added, or
1658 1654 # - all remote heads are known locally.
1659 1655 # The head check must use the unfiltered view as obsoletion
1660 1656 # markers can hide heads.
1661 1657 unfi = repo.unfiltered()
1662 1658 unficl = unfi.changelog
1663 1659
1664 1660 def headsofdiff(h1, h2):
1665 1661 """Returns heads(h1 % h2)"""
1666 1662 res = unfi.set(b'heads(%ln %% %ln)', h1, h2)
1667 1663 return set(ctx.node() for ctx in res)
1668 1664
1669 1665 def headsofunion(h1, h2):
1670 1666 """Returns heads((h1 + h2) - null)"""
1671 1667 res = unfi.set(b'heads((%ln + %ln - null))', h1, h2)
1672 1668 return set(ctx.node() for ctx in res)
1673 1669
1674 1670 while True:
1675 1671 old_heads = unficl.heads()
1676 1672 clstart = len(unficl)
1677 1673 _pullbundle2(pullop)
1678 1674 if repository.NARROW_REQUIREMENT in repo.requirements:
1679 1675 # XXX narrow clones filter the heads on the server side during
1680 1676 # XXX getbundle and result in partial replies as well.
1681 1677 # XXX Disable pull bundles in this case as band aid to avoid
1682 1678 # XXX extra round trips.
1683 1679 break
1684 1680 if clstart == len(unficl):
1685 1681 break
1686 1682 if all(unficl.hasnode(n) for n in pullop.rheads):
1687 1683 break
1688 1684 new_heads = headsofdiff(unficl.heads(), old_heads)
1689 1685 pullop.common = headsofunion(new_heads, pullop.common)
1690 1686 pullop.rheads = set(pullop.rheads) - pullop.common
1691 1687
1692 1688
1693 1689 def pull(
1694 1690 repo,
1695 1691 remote,
1696 1692 heads=None,
1697 1693 force=False,
1698 1694 bookmarks=(),
1699 1695 opargs=None,
1700 1696 streamclonerequested=None,
1701 1697 includepats=None,
1702 1698 excludepats=None,
1703 1699 depth=None,
1704 1700 ):
1705 1701 """Fetch repository data from a remote.
1706 1702
1707 1703 This is the main function used to retrieve data from a remote repository.
1708 1704
1709 1705 ``repo`` is the local repository to clone into.
1710 1706 ``remote`` is a peer instance.
1711 1707 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1712 1708 default) means to pull everything from the remote.
1713 1709 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1714 1710 default, all remote bookmarks are pulled.
1715 1711 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1716 1712 initialization.
1717 1713 ``streamclonerequested`` is a boolean indicating whether a "streaming
1718 1714 clone" is requested. A "streaming clone" is essentially a raw file copy
1719 1715 of revlogs from the server. This only works when the local repository is
1720 1716 empty. The default value of ``None`` means to respect the server
1721 1717 configuration for preferring stream clones.
1722 1718 ``includepats`` and ``excludepats`` define explicit file patterns to
1723 1719 include and exclude in storage, respectively. If not defined, narrow
1724 1720 patterns from the repo instance are used, if available.
1725 1721 ``depth`` is an integer indicating the DAG depth of history we're
1726 1722 interested in. If defined, for each revision specified in ``heads``, we
1727 1723 will fetch up to this many of its ancestors and data associated with them.
1728 1724
1729 1725 Returns the ``pulloperation`` created for this pull.
1730 1726 """
1731 1727 if opargs is None:
1732 1728 opargs = {}
1733 1729
1734 1730 # We allow the narrow patterns to be passed in explicitly to provide more
1735 1731 # flexibility for API consumers.
1736 1732 if includepats or excludepats:
1737 1733 includepats = includepats or set()
1738 1734 excludepats = excludepats or set()
1739 1735 else:
1740 1736 includepats, excludepats = repo.narrowpats
1741 1737
1742 1738 narrowspec.validatepatterns(includepats)
1743 1739 narrowspec.validatepatterns(excludepats)
1744 1740
1745 1741 pullop = pulloperation(
1746 1742 repo,
1747 1743 remote,
1748 1744 heads,
1749 1745 force,
1750 1746 bookmarks=bookmarks,
1751 1747 streamclonerequested=streamclonerequested,
1752 1748 includepats=includepats,
1753 1749 excludepats=excludepats,
1754 1750 depth=depth,
1755 1751 **pycompat.strkwargs(opargs)
1756 1752 )
1757 1753
1758 1754 peerlocal = pullop.remote.local()
1759 1755 if peerlocal:
1760 1756 missing = set(peerlocal.requirements) - pullop.repo.supported
1761 1757 if missing:
1762 1758 msg = _(
1763 1759 b"required features are not"
1764 1760 b" supported in the destination:"
1765 1761 b" %s"
1766 1762 ) % (b', '.join(sorted(missing)))
1767 1763 raise error.Abort(msg)
1768 1764
1769 1765 pullop.trmanager = transactionmanager(repo, b'pull', remote.url())
1770 1766 wlock = util.nullcontextmanager()
1771 1767 if not bookmod.bookmarksinstore(repo):
1772 1768 wlock = repo.wlock()
1773 1769 with wlock, repo.lock(), pullop.trmanager:
1774 1770 # Use the modern wire protocol, if available.
1775 1771 if remote.capable(b'command-changesetdata'):
1776 1772 exchangev2.pull(pullop)
1777 1773 else:
1778 1774 # This should ideally be in _pullbundle2(). However, it needs to run
1779 1775 # before discovery to avoid extra work.
1780 1776 _maybeapplyclonebundle(pullop)
1781 1777 streamclone.maybeperformlegacystreamclone(pullop)
1782 1778 _pulldiscovery(pullop)
1783 1779 if pullop.canusebundle2:
1784 1780 _fullpullbundle2(repo, pullop)
1785 1781 _pullchangeset(pullop)
1786 1782 _pullphase(pullop)
1787 1783 _pullbookmarks(pullop)
1788 1784 _pullobsolete(pullop)
1789 1785
1790 1786 # storing remotenames
1791 1787 if repo.ui.configbool(b'experimental', b'remotenames'):
1792 1788 logexchange.pullremotenames(repo, remote)
1793 1789
1794 1790 return pullop
1795 1791
1796 1792
1797 1793 # list of steps to perform discovery before pull
1798 1794 pulldiscoveryorder = []
1799 1795
1800 1796 # Mapping between step name and function
1801 1797 #
1802 1798 # This exists to help extensions wrap steps if necessary
1803 1799 pulldiscoverymapping = {}
1804 1800
1805 1801
1806 1802 def pulldiscovery(stepname):
1807 1803 """decorator for function performing discovery before pull
1808 1804
1809 1805 The function is added to the step -> function mapping and appended to the
1810 1806 list of steps. Beware that decorated function will be added in order (this
1811 1807 may matter).
1812 1808
1813 1809 You can only use this decorator for a new step, if you want to wrap a step
1814 1810 from an extension, change the pulldiscovery dictionary directly."""
1815 1811
1816 1812 def dec(func):
1817 1813 assert stepname not in pulldiscoverymapping
1818 1814 pulldiscoverymapping[stepname] = func
1819 1815 pulldiscoveryorder.append(stepname)
1820 1816 return func
1821 1817
1822 1818 return dec
1823 1819
1824 1820
1825 1821 def _pulldiscovery(pullop):
1826 1822 """Run all discovery steps"""
1827 1823 for stepname in pulldiscoveryorder:
1828 1824 step = pulldiscoverymapping[stepname]
1829 1825 step(pullop)
1830 1826
1831 1827
1832 1828 @pulldiscovery(b'b1:bookmarks')
1833 1829 def _pullbookmarkbundle1(pullop):
1834 1830 """fetch bookmark data in bundle1 case
1835 1831
1836 1832 If not using bundle2, we have to fetch bookmarks before changeset
1837 1833 discovery to reduce the chance and impact of race conditions."""
1838 1834 if pullop.remotebookmarks is not None:
1839 1835 return
1840 1836 if pullop.canusebundle2 and b'listkeys' in pullop.remotebundle2caps:
1841 1837 # all known bundle2 servers now support listkeys, but lets be nice with
1842 1838 # new implementation.
1843 1839 return
1844 1840 books = listkeys(pullop.remote, b'bookmarks')
1845 1841 pullop.remotebookmarks = bookmod.unhexlifybookmarks(books)
1846 1842
1847 1843
1848 1844 @pulldiscovery(b'changegroup')
1849 1845 def _pulldiscoverychangegroup(pullop):
1850 1846 """discovery phase for the pull
1851 1847
1852 1848 Current handle changeset discovery only, will change handle all discovery
1853 1849 at some point."""
1854 1850 tmp = discovery.findcommonincoming(
1855 1851 pullop.repo, pullop.remote, heads=pullop.heads, force=pullop.force
1856 1852 )
1857 1853 common, fetch, rheads = tmp
1858 1854 nm = pullop.repo.unfiltered().changelog.nodemap
1859 1855 if fetch and rheads:
1860 1856 # If a remote heads is filtered locally, put in back in common.
1861 1857 #
1862 1858 # This is a hackish solution to catch most of "common but locally
1863 1859 # hidden situation". We do not performs discovery on unfiltered
1864 1860 # repository because it end up doing a pathological amount of round
1865 1861 # trip for w huge amount of changeset we do not care about.
1866 1862 #
1867 1863 # If a set of such "common but filtered" changeset exist on the server
1868 1864 # but are not including a remote heads, we'll not be able to detect it,
1869 1865 scommon = set(common)
1870 1866 for n in rheads:
1871 1867 if n in nm:
1872 1868 if n not in scommon:
1873 1869 common.append(n)
1874 1870 if set(rheads).issubset(set(common)):
1875 1871 fetch = []
1876 1872 pullop.common = common
1877 1873 pullop.fetch = fetch
1878 1874 pullop.rheads = rheads
1879 1875
1880 1876
1881 1877 def _pullbundle2(pullop):
1882 1878 """pull data using bundle2
1883 1879
1884 1880 For now, the only supported data are changegroup."""
1885 1881 kwargs = {b'bundlecaps': caps20to10(pullop.repo, role=b'client')}
1886 1882
1887 1883 # make ui easier to access
1888 1884 ui = pullop.repo.ui
1889 1885
1890 1886 # At the moment we don't do stream clones over bundle2. If that is
1891 1887 # implemented then here's where the check for that will go.
1892 1888 streaming = streamclone.canperformstreamclone(pullop, bundle2=True)[0]
1893 1889
1894 1890 # declare pull perimeters
1895 1891 kwargs[b'common'] = pullop.common
1896 1892 kwargs[b'heads'] = pullop.heads or pullop.rheads
1897 1893
1898 1894 # check server supports narrow and then adding includepats and excludepats
1899 1895 servernarrow = pullop.remote.capable(wireprototypes.NARROWCAP)
1900 1896 if servernarrow and pullop.includepats:
1901 1897 kwargs[b'includepats'] = pullop.includepats
1902 1898 if servernarrow and pullop.excludepats:
1903 1899 kwargs[b'excludepats'] = pullop.excludepats
1904 1900
1905 1901 if streaming:
1906 1902 kwargs[b'cg'] = False
1907 1903 kwargs[b'stream'] = True
1908 1904 pullop.stepsdone.add(b'changegroup')
1909 1905 pullop.stepsdone.add(b'phases')
1910 1906
1911 1907 else:
1912 1908 # pulling changegroup
1913 1909 pullop.stepsdone.add(b'changegroup')
1914 1910
1915 1911 kwargs[b'cg'] = pullop.fetch
1916 1912
1917 1913 legacyphase = b'phases' in ui.configlist(b'devel', b'legacy.exchange')
1918 1914 hasbinaryphase = b'heads' in pullop.remotebundle2caps.get(b'phases', ())
1919 1915 if not legacyphase and hasbinaryphase:
1920 1916 kwargs[b'phases'] = True
1921 1917 pullop.stepsdone.add(b'phases')
1922 1918
1923 1919 if b'listkeys' in pullop.remotebundle2caps:
1924 1920 if b'phases' not in pullop.stepsdone:
1925 1921 kwargs[b'listkeys'] = [b'phases']
1926 1922
1927 1923 bookmarksrequested = False
1928 1924 legacybookmark = b'bookmarks' in ui.configlist(b'devel', b'legacy.exchange')
1929 1925 hasbinarybook = b'bookmarks' in pullop.remotebundle2caps
1930 1926
1931 1927 if pullop.remotebookmarks is not None:
1932 1928 pullop.stepsdone.add(b'request-bookmarks')
1933 1929
1934 1930 if (
1935 1931 b'request-bookmarks' not in pullop.stepsdone
1936 1932 and pullop.remotebookmarks is None
1937 1933 and not legacybookmark
1938 1934 and hasbinarybook
1939 1935 ):
1940 1936 kwargs[b'bookmarks'] = True
1941 1937 bookmarksrequested = True
1942 1938
1943 1939 if b'listkeys' in pullop.remotebundle2caps:
1944 1940 if b'request-bookmarks' not in pullop.stepsdone:
1945 1941 # make sure to always includes bookmark data when migrating
1946 1942 # `hg incoming --bundle` to using this function.
1947 1943 pullop.stepsdone.add(b'request-bookmarks')
1948 1944 kwargs.setdefault(b'listkeys', []).append(b'bookmarks')
1949 1945
1950 1946 # If this is a full pull / clone and the server supports the clone bundles
1951 1947 # feature, tell the server whether we attempted a clone bundle. The
1952 1948 # presence of this flag indicates the client supports clone bundles. This
1953 1949 # will enable the server to treat clients that support clone bundles
1954 1950 # differently from those that don't.
1955 1951 if (
1956 1952 pullop.remote.capable(b'clonebundles')
1957 1953 and pullop.heads is None
1958 1954 and list(pullop.common) == [nullid]
1959 1955 ):
1960 1956 kwargs[b'cbattempted'] = pullop.clonebundleattempted
1961 1957
1962 1958 if streaming:
1963 1959 pullop.repo.ui.status(_(b'streaming all changes\n'))
1964 1960 elif not pullop.fetch:
1965 1961 pullop.repo.ui.status(_(b"no changes found\n"))
1966 1962 pullop.cgresult = 0
1967 1963 else:
1968 1964 if pullop.heads is None and list(pullop.common) == [nullid]:
1969 1965 pullop.repo.ui.status(_(b"requesting all changes\n"))
1970 1966 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1971 1967 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1972 1968 if obsolete.commonversion(remoteversions) is not None:
1973 1969 kwargs[b'obsmarkers'] = True
1974 1970 pullop.stepsdone.add(b'obsmarkers')
1975 1971 _pullbundle2extraprepare(pullop, kwargs)
1976 1972
1977 1973 with pullop.remote.commandexecutor() as e:
1978 1974 args = dict(kwargs)
1979 1975 args[b'source'] = b'pull'
1980 1976 bundle = e.callcommand(b'getbundle', args).result()
1981 1977
1982 1978 try:
1983 1979 op = bundle2.bundleoperation(
1984 1980 pullop.repo, pullop.gettransaction, source=b'pull'
1985 1981 )
1986 1982 op.modes[b'bookmarks'] = b'records'
1987 1983 bundle2.processbundle(pullop.repo, bundle, op=op)
1988 1984 except bundle2.AbortFromPart as exc:
1989 1985 pullop.repo.ui.status(_(b'remote: abort: %s\n') % exc)
1990 1986 raise error.Abort(_(b'pull failed on remote'), hint=exc.hint)
1991 1987 except error.BundleValueError as exc:
1992 1988 raise error.Abort(_(b'missing support for %s') % exc)
1993 1989
1994 1990 if pullop.fetch:
1995 1991 pullop.cgresult = bundle2.combinechangegroupresults(op)
1996 1992
1997 1993 # processing phases change
1998 1994 for namespace, value in op.records[b'listkeys']:
1999 1995 if namespace == b'phases':
2000 1996 _pullapplyphases(pullop, value)
2001 1997
2002 1998 # processing bookmark update
2003 1999 if bookmarksrequested:
2004 2000 books = {}
2005 2001 for record in op.records[b'bookmarks']:
2006 2002 books[record[b'bookmark']] = record[b"node"]
2007 2003 pullop.remotebookmarks = books
2008 2004 else:
2009 2005 for namespace, value in op.records[b'listkeys']:
2010 2006 if namespace == b'bookmarks':
2011 2007 pullop.remotebookmarks = bookmod.unhexlifybookmarks(value)
2012 2008
2013 2009 # bookmark data were either already there or pulled in the bundle
2014 2010 if pullop.remotebookmarks is not None:
2015 2011 _pullbookmarks(pullop)
2016 2012
2017 2013
2018 2014 def _pullbundle2extraprepare(pullop, kwargs):
2019 2015 """hook function so that extensions can extend the getbundle call"""
2020 2016
2021 2017
2022 2018 def _pullchangeset(pullop):
2023 2019 """pull changeset from unbundle into the local repo"""
2024 2020 # We delay the open of the transaction as late as possible so we
2025 2021 # don't open transaction for nothing or you break future useful
2026 2022 # rollback call
2027 2023 if b'changegroup' in pullop.stepsdone:
2028 2024 return
2029 2025 pullop.stepsdone.add(b'changegroup')
2030 2026 if not pullop.fetch:
2031 2027 pullop.repo.ui.status(_(b"no changes found\n"))
2032 2028 pullop.cgresult = 0
2033 2029 return
2034 2030 tr = pullop.gettransaction()
2035 2031 if pullop.heads is None and list(pullop.common) == [nullid]:
2036 2032 pullop.repo.ui.status(_(b"requesting all changes\n"))
2037 2033 elif pullop.heads is None and pullop.remote.capable(b'changegroupsubset'):
2038 2034 # issue1320, avoid a race if remote changed after discovery
2039 2035 pullop.heads = pullop.rheads
2040 2036
2041 2037 if pullop.remote.capable(b'getbundle'):
2042 2038 # TODO: get bundlecaps from remote
2043 2039 cg = pullop.remote.getbundle(
2044 2040 b'pull', common=pullop.common, heads=pullop.heads or pullop.rheads
2045 2041 )
2046 2042 elif pullop.heads is None:
2047 2043 with pullop.remote.commandexecutor() as e:
2048 2044 cg = e.callcommand(
2049 2045 b'changegroup', {b'nodes': pullop.fetch, b'source': b'pull',}
2050 2046 ).result()
2051 2047
2052 2048 elif not pullop.remote.capable(b'changegroupsubset'):
2053 2049 raise error.Abort(
2054 2050 _(
2055 2051 b"partial pull cannot be done because "
2056 2052 b"other repository doesn't support "
2057 2053 b"changegroupsubset."
2058 2054 )
2059 2055 )
2060 2056 else:
2061 2057 with pullop.remote.commandexecutor() as e:
2062 2058 cg = e.callcommand(
2063 2059 b'changegroupsubset',
2064 2060 {
2065 2061 b'bases': pullop.fetch,
2066 2062 b'heads': pullop.heads,
2067 2063 b'source': b'pull',
2068 2064 },
2069 2065 ).result()
2070 2066
2071 2067 bundleop = bundle2.applybundle(
2072 2068 pullop.repo, cg, tr, b'pull', pullop.remote.url()
2073 2069 )
2074 2070 pullop.cgresult = bundle2.combinechangegroupresults(bundleop)
2075 2071
2076 2072
2077 2073 def _pullphase(pullop):
2078 2074 # Get remote phases data from remote
2079 2075 if b'phases' in pullop.stepsdone:
2080 2076 return
2081 2077 remotephases = listkeys(pullop.remote, b'phases')
2082 2078 _pullapplyphases(pullop, remotephases)
2083 2079
2084 2080
2085 2081 def _pullapplyphases(pullop, remotephases):
2086 2082 """apply phase movement from observed remote state"""
2087 2083 if b'phases' in pullop.stepsdone:
2088 2084 return
2089 2085 pullop.stepsdone.add(b'phases')
2090 2086 publishing = bool(remotephases.get(b'publishing', False))
2091 2087 if remotephases and not publishing:
2092 2088 # remote is new and non-publishing
2093 2089 pheads, _dr = phases.analyzeremotephases(
2094 2090 pullop.repo, pullop.pulledsubset, remotephases
2095 2091 )
2096 2092 dheads = pullop.pulledsubset
2097 2093 else:
2098 2094 # Remote is old or publishing all common changesets
2099 2095 # should be seen as public
2100 2096 pheads = pullop.pulledsubset
2101 2097 dheads = []
2102 2098 unfi = pullop.repo.unfiltered()
2103 2099 phase = unfi._phasecache.phase
2104 2100 rev = unfi.changelog.nodemap.get
2105 2101 public = phases.public
2106 2102 draft = phases.draft
2107 2103
2108 2104 # exclude changesets already public locally and update the others
2109 2105 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
2110 2106 if pheads:
2111 2107 tr = pullop.gettransaction()
2112 2108 phases.advanceboundary(pullop.repo, tr, public, pheads)
2113 2109
2114 2110 # exclude changesets already draft locally and update the others
2115 2111 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
2116 2112 if dheads:
2117 2113 tr = pullop.gettransaction()
2118 2114 phases.advanceboundary(pullop.repo, tr, draft, dheads)
2119 2115
2120 2116
2121 2117 def _pullbookmarks(pullop):
2122 2118 """process the remote bookmark information to update the local one"""
2123 2119 if b'bookmarks' in pullop.stepsdone:
2124 2120 return
2125 2121 pullop.stepsdone.add(b'bookmarks')
2126 2122 repo = pullop.repo
2127 2123 remotebookmarks = pullop.remotebookmarks
2128 2124 bookmod.updatefromremote(
2129 2125 repo.ui,
2130 2126 repo,
2131 2127 remotebookmarks,
2132 2128 pullop.remote.url(),
2133 2129 pullop.gettransaction,
2134 2130 explicit=pullop.explicitbookmarks,
2135 2131 )
2136 2132
2137 2133
2138 2134 def _pullobsolete(pullop):
2139 2135 """utility function to pull obsolete markers from a remote
2140 2136
2141 2137 The `gettransaction` is function that return the pull transaction, creating
2142 2138 one if necessary. We return the transaction to inform the calling code that
2143 2139 a new transaction have been created (when applicable).
2144 2140
2145 2141 Exists mostly to allow overriding for experimentation purpose"""
2146 2142 if b'obsmarkers' in pullop.stepsdone:
2147 2143 return
2148 2144 pullop.stepsdone.add(b'obsmarkers')
2149 2145 tr = None
2150 2146 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
2151 2147 pullop.repo.ui.debug(b'fetching remote obsolete markers\n')
2152 2148 remoteobs = listkeys(pullop.remote, b'obsolete')
2153 2149 if b'dump0' in remoteobs:
2154 2150 tr = pullop.gettransaction()
2155 2151 markers = []
2156 2152 for key in sorted(remoteobs, reverse=True):
2157 2153 if key.startswith(b'dump'):
2158 2154 data = util.b85decode(remoteobs[key])
2159 2155 version, newmarks = obsolete._readmarkers(data)
2160 2156 markers += newmarks
2161 2157 if markers:
2162 2158 pullop.repo.obsstore.add(tr, markers)
2163 2159 pullop.repo.invalidatevolatilesets()
2164 2160 return tr
2165 2161
2166 2162
2167 2163 def applynarrowacl(repo, kwargs):
2168 2164 """Apply narrow fetch access control.
2169 2165
2170 2166 This massages the named arguments for getbundle wire protocol commands
2171 2167 so requested data is filtered through access control rules.
2172 2168 """
2173 2169 ui = repo.ui
2174 2170 # TODO this assumes existence of HTTP and is a layering violation.
2175 2171 username = ui.shortuser(ui.environ.get(b'REMOTE_USER') or ui.username())
2176 2172 user_includes = ui.configlist(
2177 2173 _NARROWACL_SECTION,
2178 2174 username + b'.includes',
2179 2175 ui.configlist(_NARROWACL_SECTION, b'default.includes'),
2180 2176 )
2181 2177 user_excludes = ui.configlist(
2182 2178 _NARROWACL_SECTION,
2183 2179 username + b'.excludes',
2184 2180 ui.configlist(_NARROWACL_SECTION, b'default.excludes'),
2185 2181 )
2186 2182 if not user_includes:
2187 2183 raise error.Abort(
2188 2184 _(b"{} configuration for user {} is empty").format(
2189 2185 _NARROWACL_SECTION, username
2190 2186 )
2191 2187 )
2192 2188
2193 2189 user_includes = [
2194 2190 b'path:.' if p == b'*' else b'path:' + p for p in user_includes
2195 2191 ]
2196 2192 user_excludes = [
2197 2193 b'path:.' if p == b'*' else b'path:' + p for p in user_excludes
2198 2194 ]
2199 2195
2200 2196 req_includes = set(kwargs.get(r'includepats', []))
2201 2197 req_excludes = set(kwargs.get(r'excludepats', []))
2202 2198
2203 2199 req_includes, req_excludes, invalid_includes = narrowspec.restrictpatterns(
2204 2200 req_includes, req_excludes, user_includes, user_excludes
2205 2201 )
2206 2202
2207 2203 if invalid_includes:
2208 2204 raise error.Abort(
2209 2205 _(b"The following includes are not accessible for {}: {}").format(
2210 2206 username, invalid_includes
2211 2207 )
2212 2208 )
2213 2209
2214 2210 new_args = {}
2215 2211 new_args.update(kwargs)
2216 2212 new_args[r'narrow'] = True
2217 2213 new_args[r'narrow_acl'] = True
2218 2214 new_args[r'includepats'] = req_includes
2219 2215 if req_excludes:
2220 2216 new_args[r'excludepats'] = req_excludes
2221 2217
2222 2218 return new_args
2223 2219
2224 2220
2225 2221 def _computeellipsis(repo, common, heads, known, match, depth=None):
2226 2222 """Compute the shape of a narrowed DAG.
2227 2223
2228 2224 Args:
2229 2225 repo: The repository we're transferring.
2230 2226 common: The roots of the DAG range we're transferring.
2231 2227 May be just [nullid], which means all ancestors of heads.
2232 2228 heads: The heads of the DAG range we're transferring.
2233 2229 match: The narrowmatcher that allows us to identify relevant changes.
2234 2230 depth: If not None, only consider nodes to be full nodes if they are at
2235 2231 most depth changesets away from one of heads.
2236 2232
2237 2233 Returns:
2238 2234 A tuple of (visitnodes, relevant_nodes, ellipsisroots) where:
2239 2235
2240 2236 visitnodes: The list of nodes (either full or ellipsis) which
2241 2237 need to be sent to the client.
2242 2238 relevant_nodes: The set of changelog nodes which change a file inside
2243 2239 the narrowspec. The client needs these as non-ellipsis nodes.
2244 2240 ellipsisroots: A dict of {rev: parents} that is used in
2245 2241 narrowchangegroup to produce ellipsis nodes with the
2246 2242 correct parents.
2247 2243 """
2248 2244 cl = repo.changelog
2249 2245 mfl = repo.manifestlog
2250 2246
2251 2247 clrev = cl.rev
2252 2248
2253 2249 commonrevs = {clrev(n) for n in common} | {nullrev}
2254 2250 headsrevs = {clrev(n) for n in heads}
2255 2251
2256 2252 if depth:
2257 2253 revdepth = {h: 0 for h in headsrevs}
2258 2254
2259 2255 ellipsisheads = collections.defaultdict(set)
2260 2256 ellipsisroots = collections.defaultdict(set)
2261 2257
2262 2258 def addroot(head, curchange):
2263 2259 """Add a root to an ellipsis head, splitting heads with 3 roots."""
2264 2260 ellipsisroots[head].add(curchange)
2265 2261 # Recursively split ellipsis heads with 3 roots by finding the
2266 2262 # roots' youngest common descendant which is an elided merge commit.
2267 2263 # That descendant takes 2 of the 3 roots as its own, and becomes a
2268 2264 # root of the head.
2269 2265 while len(ellipsisroots[head]) > 2:
2270 2266 child, roots = splithead(head)
2271 2267 splitroots(head, child, roots)
2272 2268 head = child # Recurse in case we just added a 3rd root
2273 2269
2274 2270 def splitroots(head, child, roots):
2275 2271 ellipsisroots[head].difference_update(roots)
2276 2272 ellipsisroots[head].add(child)
2277 2273 ellipsisroots[child].update(roots)
2278 2274 ellipsisroots[child].discard(child)
2279 2275
2280 2276 def splithead(head):
2281 2277 r1, r2, r3 = sorted(ellipsisroots[head])
2282 2278 for nr1, nr2 in ((r2, r3), (r1, r3), (r1, r2)):
2283 2279 mid = repo.revs(
2284 2280 b'sort(merge() & %d::%d & %d::%d, -rev)', nr1, head, nr2, head
2285 2281 )
2286 2282 for j in mid:
2287 2283 if j == nr2:
2288 2284 return nr2, (nr1, nr2)
2289 2285 if j not in ellipsisroots or len(ellipsisroots[j]) < 2:
2290 2286 return j, (nr1, nr2)
2291 2287 raise error.Abort(
2292 2288 _(
2293 2289 b'Failed to split up ellipsis node! head: %d, '
2294 2290 b'roots: %d %d %d'
2295 2291 )
2296 2292 % (head, r1, r2, r3)
2297 2293 )
2298 2294
2299 2295 missing = list(cl.findmissingrevs(common=commonrevs, heads=headsrevs))
2300 2296 visit = reversed(missing)
2301 2297 relevant_nodes = set()
2302 2298 visitnodes = [cl.node(m) for m in missing]
2303 2299 required = set(headsrevs) | known
2304 2300 for rev in visit:
2305 2301 clrev = cl.changelogrevision(rev)
2306 2302 ps = [prev for prev in cl.parentrevs(rev) if prev != nullrev]
2307 2303 if depth is not None:
2308 2304 curdepth = revdepth[rev]
2309 2305 for p in ps:
2310 2306 revdepth[p] = min(curdepth + 1, revdepth.get(p, depth + 1))
2311 2307 needed = False
2312 2308 shallow_enough = depth is None or revdepth[rev] <= depth
2313 2309 if shallow_enough:
2314 2310 curmf = mfl[clrev.manifest].read()
2315 2311 if ps:
2316 2312 # We choose to not trust the changed files list in
2317 2313 # changesets because it's not always correct. TODO: could
2318 2314 # we trust it for the non-merge case?
2319 2315 p1mf = mfl[cl.changelogrevision(ps[0]).manifest].read()
2320 2316 needed = bool(curmf.diff(p1mf, match))
2321 2317 if not needed and len(ps) > 1:
2322 2318 # For merge changes, the list of changed files is not
2323 2319 # helpful, since we need to emit the merge if a file
2324 2320 # in the narrow spec has changed on either side of the
2325 2321 # merge. As a result, we do a manifest diff to check.
2326 2322 p2mf = mfl[cl.changelogrevision(ps[1]).manifest].read()
2327 2323 needed = bool(curmf.diff(p2mf, match))
2328 2324 else:
2329 2325 # For a root node, we need to include the node if any
2330 2326 # files in the node match the narrowspec.
2331 2327 needed = any(curmf.walk(match))
2332 2328
2333 2329 if needed:
2334 2330 for head in ellipsisheads[rev]:
2335 2331 addroot(head, rev)
2336 2332 for p in ps:
2337 2333 required.add(p)
2338 2334 relevant_nodes.add(cl.node(rev))
2339 2335 else:
2340 2336 if not ps:
2341 2337 ps = [nullrev]
2342 2338 if rev in required:
2343 2339 for head in ellipsisheads[rev]:
2344 2340 addroot(head, rev)
2345 2341 for p in ps:
2346 2342 ellipsisheads[p].add(rev)
2347 2343 else:
2348 2344 for p in ps:
2349 2345 ellipsisheads[p] |= ellipsisheads[rev]
2350 2346
2351 2347 # add common changesets as roots of their reachable ellipsis heads
2352 2348 for c in commonrevs:
2353 2349 for head in ellipsisheads[c]:
2354 2350 addroot(head, c)
2355 2351 return visitnodes, relevant_nodes, ellipsisroots
2356 2352
2357 2353
2358 2354 def caps20to10(repo, role):
2359 2355 """return a set with appropriate options to use bundle20 during getbundle"""
2360 2356 caps = {b'HG20'}
2361 2357 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role=role))
2362 2358 caps.add(b'bundle2=' + urlreq.quote(capsblob))
2363 2359 return caps
2364 2360
2365 2361
2366 2362 # List of names of steps to perform for a bundle2 for getbundle, order matters.
2367 2363 getbundle2partsorder = []
2368 2364
2369 2365 # Mapping between step name and function
2370 2366 #
2371 2367 # This exists to help extensions wrap steps if necessary
2372 2368 getbundle2partsmapping = {}
2373 2369
2374 2370
2375 2371 def getbundle2partsgenerator(stepname, idx=None):
2376 2372 """decorator for function generating bundle2 part for getbundle
2377 2373
2378 2374 The function is added to the step -> function mapping and appended to the
2379 2375 list of steps. Beware that decorated functions will be added in order
2380 2376 (this may matter).
2381 2377
2382 2378 You can only use this decorator for new steps, if you want to wrap a step
2383 2379 from an extension, attack the getbundle2partsmapping dictionary directly."""
2384 2380
2385 2381 def dec(func):
2386 2382 assert stepname not in getbundle2partsmapping
2387 2383 getbundle2partsmapping[stepname] = func
2388 2384 if idx is None:
2389 2385 getbundle2partsorder.append(stepname)
2390 2386 else:
2391 2387 getbundle2partsorder.insert(idx, stepname)
2392 2388 return func
2393 2389
2394 2390 return dec
2395 2391
2396 2392
2397 2393 def bundle2requested(bundlecaps):
2398 2394 if bundlecaps is not None:
2399 2395 return any(cap.startswith(b'HG2') for cap in bundlecaps)
2400 2396 return False
2401 2397
2402 2398
2403 2399 def getbundlechunks(
2404 2400 repo, source, heads=None, common=None, bundlecaps=None, **kwargs
2405 2401 ):
2406 2402 """Return chunks constituting a bundle's raw data.
2407 2403
2408 2404 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
2409 2405 passed.
2410 2406
2411 2407 Returns a 2-tuple of a dict with metadata about the generated bundle
2412 2408 and an iterator over raw chunks (of varying sizes).
2413 2409 """
2414 2410 kwargs = pycompat.byteskwargs(kwargs)
2415 2411 info = {}
2416 2412 usebundle2 = bundle2requested(bundlecaps)
2417 2413 # bundle10 case
2418 2414 if not usebundle2:
2419 2415 if bundlecaps and not kwargs.get(b'cg', True):
2420 2416 raise ValueError(
2421 2417 _(b'request for bundle10 must include changegroup')
2422 2418 )
2423 2419
2424 2420 if kwargs:
2425 2421 raise ValueError(
2426 2422 _(b'unsupported getbundle arguments: %s')
2427 2423 % b', '.join(sorted(kwargs.keys()))
2428 2424 )
2429 2425 outgoing = _computeoutgoing(repo, heads, common)
2430 2426 info[b'bundleversion'] = 1
2431 2427 return (
2432 2428 info,
2433 2429 changegroup.makestream(
2434 2430 repo, outgoing, b'01', source, bundlecaps=bundlecaps
2435 2431 ),
2436 2432 )
2437 2433
2438 2434 # bundle20 case
2439 2435 info[b'bundleversion'] = 2
2440 2436 b2caps = {}
2441 2437 for bcaps in bundlecaps:
2442 2438 if bcaps.startswith(b'bundle2='):
2443 2439 blob = urlreq.unquote(bcaps[len(b'bundle2=') :])
2444 2440 b2caps.update(bundle2.decodecaps(blob))
2445 2441 bundler = bundle2.bundle20(repo.ui, b2caps)
2446 2442
2447 2443 kwargs[b'heads'] = heads
2448 2444 kwargs[b'common'] = common
2449 2445
2450 2446 for name in getbundle2partsorder:
2451 2447 func = getbundle2partsmapping[name]
2452 2448 func(
2453 2449 bundler,
2454 2450 repo,
2455 2451 source,
2456 2452 bundlecaps=bundlecaps,
2457 2453 b2caps=b2caps,
2458 2454 **pycompat.strkwargs(kwargs)
2459 2455 )
2460 2456
2461 2457 info[b'prefercompressed'] = bundler.prefercompressed
2462 2458
2463 2459 return info, bundler.getchunks()
2464 2460
2465 2461
2466 2462 @getbundle2partsgenerator(b'stream2')
2467 2463 def _getbundlestream2(bundler, repo, *args, **kwargs):
2468 2464 return bundle2.addpartbundlestream2(bundler, repo, **kwargs)
2469 2465
2470 2466
2471 2467 @getbundle2partsgenerator(b'changegroup')
2472 2468 def _getbundlechangegrouppart(
2473 2469 bundler,
2474 2470 repo,
2475 2471 source,
2476 2472 bundlecaps=None,
2477 2473 b2caps=None,
2478 2474 heads=None,
2479 2475 common=None,
2480 2476 **kwargs
2481 2477 ):
2482 2478 """add a changegroup part to the requested bundle"""
2483 2479 if not kwargs.get(r'cg', True):
2484 2480 return
2485 2481
2486 2482 version = b'01'
2487 2483 cgversions = b2caps.get(b'changegroup')
2488 2484 if cgversions: # 3.1 and 3.2 ship with an empty value
2489 2485 cgversions = [
2490 2486 v
2491 2487 for v in cgversions
2492 2488 if v in changegroup.supportedoutgoingversions(repo)
2493 2489 ]
2494 2490 if not cgversions:
2495 2491 raise error.Abort(_(b'no common changegroup version'))
2496 2492 version = max(cgversions)
2497 2493
2498 2494 outgoing = _computeoutgoing(repo, heads, common)
2499 2495 if not outgoing.missing:
2500 2496 return
2501 2497
2502 2498 if kwargs.get(r'narrow', False):
2503 2499 include = sorted(filter(bool, kwargs.get(r'includepats', [])))
2504 2500 exclude = sorted(filter(bool, kwargs.get(r'excludepats', [])))
2505 2501 matcher = narrowspec.match(repo.root, include=include, exclude=exclude)
2506 2502 else:
2507 2503 matcher = None
2508 2504
2509 2505 cgstream = changegroup.makestream(
2510 2506 repo, outgoing, version, source, bundlecaps=bundlecaps, matcher=matcher
2511 2507 )
2512 2508
2513 2509 part = bundler.newpart(b'changegroup', data=cgstream)
2514 2510 if cgversions:
2515 2511 part.addparam(b'version', version)
2516 2512
2517 2513 part.addparam(b'nbchanges', b'%d' % len(outgoing.missing), mandatory=False)
2518 2514
2519 2515 if b'treemanifest' in repo.requirements:
2520 2516 part.addparam(b'treemanifest', b'1')
2521 2517
2522 2518 if b'exp-sidedata-flag' in repo.requirements:
2523 2519 part.addparam(b'exp-sidedata', b'1')
2524 2520
2525 2521 if (
2526 2522 kwargs.get(r'narrow', False)
2527 2523 and kwargs.get(r'narrow_acl', False)
2528 2524 and (include or exclude)
2529 2525 ):
2530 2526 # this is mandatory because otherwise ACL clients won't work
2531 2527 narrowspecpart = bundler.newpart(b'Narrow:responsespec')
2532 2528 narrowspecpart.data = b'%s\0%s' % (
2533 2529 b'\n'.join(include),
2534 2530 b'\n'.join(exclude),
2535 2531 )
2536 2532
2537 2533
2538 2534 @getbundle2partsgenerator(b'bookmarks')
2539 2535 def _getbundlebookmarkpart(
2540 2536 bundler, repo, source, bundlecaps=None, b2caps=None, **kwargs
2541 2537 ):
2542 2538 """add a bookmark part to the requested bundle"""
2543 2539 if not kwargs.get(r'bookmarks', False):
2544 2540 return
2545 2541 if b'bookmarks' not in b2caps:
2546 2542 raise error.Abort(_(b'no common bookmarks exchange method'))
2547 2543 books = bookmod.listbinbookmarks(repo)
2548 2544 data = bookmod.binaryencode(books)
2549 2545 if data:
2550 2546 bundler.newpart(b'bookmarks', data=data)
2551 2547
2552 2548
2553 2549 @getbundle2partsgenerator(b'listkeys')
2554 2550 def _getbundlelistkeysparts(
2555 2551 bundler, repo, source, bundlecaps=None, b2caps=None, **kwargs
2556 2552 ):
2557 2553 """add parts containing listkeys namespaces to the requested bundle"""
2558 2554 listkeys = kwargs.get(r'listkeys', ())
2559 2555 for namespace in listkeys:
2560 2556 part = bundler.newpart(b'listkeys')
2561 2557 part.addparam(b'namespace', namespace)
2562 2558 keys = repo.listkeys(namespace).items()
2563 2559 part.data = pushkey.encodekeys(keys)
2564 2560
2565 2561
2566 2562 @getbundle2partsgenerator(b'obsmarkers')
2567 2563 def _getbundleobsmarkerpart(
2568 2564 bundler, repo, source, bundlecaps=None, b2caps=None, heads=None, **kwargs
2569 2565 ):
2570 2566 """add an obsolescence markers part to the requested bundle"""
2571 2567 if kwargs.get(r'obsmarkers', False):
2572 2568 if heads is None:
2573 2569 heads = repo.heads()
2574 2570 subset = [c.node() for c in repo.set(b'::%ln', heads)]
2575 2571 markers = repo.obsstore.relevantmarkers(subset)
2576 markers = _sortedmarkers(markers)
2572 markers = obsutil.sortedmarkers(markers)
2577 2573 bundle2.buildobsmarkerspart(bundler, markers)
2578 2574
2579 2575
2580 2576 @getbundle2partsgenerator(b'phases')
2581 2577 def _getbundlephasespart(
2582 2578 bundler, repo, source, bundlecaps=None, b2caps=None, heads=None, **kwargs
2583 2579 ):
2584 2580 """add phase heads part to the requested bundle"""
2585 2581 if kwargs.get(r'phases', False):
2586 2582 if not b'heads' in b2caps.get(b'phases'):
2587 2583 raise error.Abort(_(b'no common phases exchange method'))
2588 2584 if heads is None:
2589 2585 heads = repo.heads()
2590 2586
2591 2587 headsbyphase = collections.defaultdict(set)
2592 2588 if repo.publishing():
2593 2589 headsbyphase[phases.public] = heads
2594 2590 else:
2595 2591 # find the appropriate heads to move
2596 2592
2597 2593 phase = repo._phasecache.phase
2598 2594 node = repo.changelog.node
2599 2595 rev = repo.changelog.rev
2600 2596 for h in heads:
2601 2597 headsbyphase[phase(repo, rev(h))].add(h)
2602 2598 seenphases = list(headsbyphase.keys())
2603 2599
2604 2600 # We do not handle anything but public and draft phase for now)
2605 2601 if seenphases:
2606 2602 assert max(seenphases) <= phases.draft
2607 2603
2608 2604 # if client is pulling non-public changesets, we need to find
2609 2605 # intermediate public heads.
2610 2606 draftheads = headsbyphase.get(phases.draft, set())
2611 2607 if draftheads:
2612 2608 publicheads = headsbyphase.get(phases.public, set())
2613 2609
2614 2610 revset = b'heads(only(%ln, %ln) and public())'
2615 2611 extraheads = repo.revs(revset, draftheads, publicheads)
2616 2612 for r in extraheads:
2617 2613 headsbyphase[phases.public].add(node(r))
2618 2614
2619 2615 # transform data in a format used by the encoding function
2620 2616 phasemapping = []
2621 2617 for phase in phases.allphases:
2622 2618 phasemapping.append(sorted(headsbyphase[phase]))
2623 2619
2624 2620 # generate the actual part
2625 2621 phasedata = phases.binaryencode(phasemapping)
2626 2622 bundler.newpart(b'phase-heads', data=phasedata)
2627 2623
2628 2624
2629 2625 @getbundle2partsgenerator(b'hgtagsfnodes')
2630 2626 def _getbundletagsfnodes(
2631 2627 bundler,
2632 2628 repo,
2633 2629 source,
2634 2630 bundlecaps=None,
2635 2631 b2caps=None,
2636 2632 heads=None,
2637 2633 common=None,
2638 2634 **kwargs
2639 2635 ):
2640 2636 """Transfer the .hgtags filenodes mapping.
2641 2637
2642 2638 Only values for heads in this bundle will be transferred.
2643 2639
2644 2640 The part data consists of pairs of 20 byte changeset node and .hgtags
2645 2641 filenodes raw values.
2646 2642 """
2647 2643 # Don't send unless:
2648 2644 # - changeset are being exchanged,
2649 2645 # - the client supports it.
2650 2646 if not (kwargs.get(r'cg', True) and b'hgtagsfnodes' in b2caps):
2651 2647 return
2652 2648
2653 2649 outgoing = _computeoutgoing(repo, heads, common)
2654 2650 bundle2.addparttagsfnodescache(repo, bundler, outgoing)
2655 2651
2656 2652
2657 2653 @getbundle2partsgenerator(b'cache:rev-branch-cache')
2658 2654 def _getbundlerevbranchcache(
2659 2655 bundler,
2660 2656 repo,
2661 2657 source,
2662 2658 bundlecaps=None,
2663 2659 b2caps=None,
2664 2660 heads=None,
2665 2661 common=None,
2666 2662 **kwargs
2667 2663 ):
2668 2664 """Transfer the rev-branch-cache mapping
2669 2665
2670 2666 The payload is a series of data related to each branch
2671 2667
2672 2668 1) branch name length
2673 2669 2) number of open heads
2674 2670 3) number of closed heads
2675 2671 4) open heads nodes
2676 2672 5) closed heads nodes
2677 2673 """
2678 2674 # Don't send unless:
2679 2675 # - changeset are being exchanged,
2680 2676 # - the client supports it.
2681 2677 # - narrow bundle isn't in play (not currently compatible).
2682 2678 if (
2683 2679 not kwargs.get(r'cg', True)
2684 2680 or b'rev-branch-cache' not in b2caps
2685 2681 or kwargs.get(r'narrow', False)
2686 2682 or repo.ui.has_section(_NARROWACL_SECTION)
2687 2683 ):
2688 2684 return
2689 2685
2690 2686 outgoing = _computeoutgoing(repo, heads, common)
2691 2687 bundle2.addpartrevbranchcache(repo, bundler, outgoing)
2692 2688
2693 2689
2694 2690 def check_heads(repo, their_heads, context):
2695 2691 """check if the heads of a repo have been modified
2696 2692
2697 2693 Used by peer for unbundling.
2698 2694 """
2699 2695 heads = repo.heads()
2700 2696 heads_hash = hashlib.sha1(b''.join(sorted(heads))).digest()
2701 2697 if not (
2702 2698 their_heads == [b'force']
2703 2699 or their_heads == heads
2704 2700 or their_heads == [b'hashed', heads_hash]
2705 2701 ):
2706 2702 # someone else committed/pushed/unbundled while we
2707 2703 # were transferring data
2708 2704 raise error.PushRaced(
2709 2705 b'repository changed while %s - please try again' % context
2710 2706 )
2711 2707
2712 2708
2713 2709 def unbundle(repo, cg, heads, source, url):
2714 2710 """Apply a bundle to a repo.
2715 2711
2716 2712 this function makes sure the repo is locked during the application and have
2717 2713 mechanism to check that no push race occurred between the creation of the
2718 2714 bundle and its application.
2719 2715
2720 2716 If the push was raced as PushRaced exception is raised."""
2721 2717 r = 0
2722 2718 # need a transaction when processing a bundle2 stream
2723 2719 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
2724 2720 lockandtr = [None, None, None]
2725 2721 recordout = None
2726 2722 # quick fix for output mismatch with bundle2 in 3.4
2727 2723 captureoutput = repo.ui.configbool(
2728 2724 b'experimental', b'bundle2-output-capture'
2729 2725 )
2730 2726 if url.startswith(b'remote:http:') or url.startswith(b'remote:https:'):
2731 2727 captureoutput = True
2732 2728 try:
2733 2729 # note: outside bundle1, 'heads' is expected to be empty and this
2734 2730 # 'check_heads' call wil be a no-op
2735 2731 check_heads(repo, heads, b'uploading changes')
2736 2732 # push can proceed
2737 2733 if not isinstance(cg, bundle2.unbundle20):
2738 2734 # legacy case: bundle1 (changegroup 01)
2739 2735 txnname = b"\n".join([source, util.hidepassword(url)])
2740 2736 with repo.lock(), repo.transaction(txnname) as tr:
2741 2737 op = bundle2.applybundle(repo, cg, tr, source, url)
2742 2738 r = bundle2.combinechangegroupresults(op)
2743 2739 else:
2744 2740 r = None
2745 2741 try:
2746 2742
2747 2743 def gettransaction():
2748 2744 if not lockandtr[2]:
2749 2745 if not bookmod.bookmarksinstore(repo):
2750 2746 lockandtr[0] = repo.wlock()
2751 2747 lockandtr[1] = repo.lock()
2752 2748 lockandtr[2] = repo.transaction(source)
2753 2749 lockandtr[2].hookargs[b'source'] = source
2754 2750 lockandtr[2].hookargs[b'url'] = url
2755 2751 lockandtr[2].hookargs[b'bundle2'] = b'1'
2756 2752 return lockandtr[2]
2757 2753
2758 2754 # Do greedy locking by default until we're satisfied with lazy
2759 2755 # locking.
2760 2756 if not repo.ui.configbool(
2761 2757 b'experimental', b'bundle2lazylocking'
2762 2758 ):
2763 2759 gettransaction()
2764 2760
2765 2761 op = bundle2.bundleoperation(
2766 2762 repo,
2767 2763 gettransaction,
2768 2764 captureoutput=captureoutput,
2769 2765 source=b'push',
2770 2766 )
2771 2767 try:
2772 2768 op = bundle2.processbundle(repo, cg, op=op)
2773 2769 finally:
2774 2770 r = op.reply
2775 2771 if captureoutput and r is not None:
2776 2772 repo.ui.pushbuffer(error=True, subproc=True)
2777 2773
2778 2774 def recordout(output):
2779 2775 r.newpart(b'output', data=output, mandatory=False)
2780 2776
2781 2777 if lockandtr[2] is not None:
2782 2778 lockandtr[2].close()
2783 2779 except BaseException as exc:
2784 2780 exc.duringunbundle2 = True
2785 2781 if captureoutput and r is not None:
2786 2782 parts = exc._bundle2salvagedoutput = r.salvageoutput()
2787 2783
2788 2784 def recordout(output):
2789 2785 part = bundle2.bundlepart(
2790 2786 b'output', data=output, mandatory=False
2791 2787 )
2792 2788 parts.append(part)
2793 2789
2794 2790 raise
2795 2791 finally:
2796 2792 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
2797 2793 if recordout is not None:
2798 2794 recordout(repo.ui.popbuffer())
2799 2795 return r
2800 2796
2801 2797
2802 2798 def _maybeapplyclonebundle(pullop):
2803 2799 """Apply a clone bundle from a remote, if possible."""
2804 2800
2805 2801 repo = pullop.repo
2806 2802 remote = pullop.remote
2807 2803
2808 2804 if not repo.ui.configbool(b'ui', b'clonebundles'):
2809 2805 return
2810 2806
2811 2807 # Only run if local repo is empty.
2812 2808 if len(repo):
2813 2809 return
2814 2810
2815 2811 if pullop.heads:
2816 2812 return
2817 2813
2818 2814 if not remote.capable(b'clonebundles'):
2819 2815 return
2820 2816
2821 2817 with remote.commandexecutor() as e:
2822 2818 res = e.callcommand(b'clonebundles', {}).result()
2823 2819
2824 2820 # If we call the wire protocol command, that's good enough to record the
2825 2821 # attempt.
2826 2822 pullop.clonebundleattempted = True
2827 2823
2828 2824 entries = parseclonebundlesmanifest(repo, res)
2829 2825 if not entries:
2830 2826 repo.ui.note(
2831 2827 _(
2832 2828 b'no clone bundles available on remote; '
2833 2829 b'falling back to regular clone\n'
2834 2830 )
2835 2831 )
2836 2832 return
2837 2833
2838 2834 entries = filterclonebundleentries(
2839 2835 repo, entries, streamclonerequested=pullop.streamclonerequested
2840 2836 )
2841 2837
2842 2838 if not entries:
2843 2839 # There is a thundering herd concern here. However, if a server
2844 2840 # operator doesn't advertise bundles appropriate for its clients,
2845 2841 # they deserve what's coming. Furthermore, from a client's
2846 2842 # perspective, no automatic fallback would mean not being able to
2847 2843 # clone!
2848 2844 repo.ui.warn(
2849 2845 _(
2850 2846 b'no compatible clone bundles available on server; '
2851 2847 b'falling back to regular clone\n'
2852 2848 )
2853 2849 )
2854 2850 repo.ui.warn(
2855 2851 _(b'(you may want to report this to the server operator)\n')
2856 2852 )
2857 2853 return
2858 2854
2859 2855 entries = sortclonebundleentries(repo.ui, entries)
2860 2856
2861 2857 url = entries[0][b'URL']
2862 2858 repo.ui.status(_(b'applying clone bundle from %s\n') % url)
2863 2859 if trypullbundlefromurl(repo.ui, repo, url):
2864 2860 repo.ui.status(_(b'finished applying clone bundle\n'))
2865 2861 # Bundle failed.
2866 2862 #
2867 2863 # We abort by default to avoid the thundering herd of
2868 2864 # clients flooding a server that was expecting expensive
2869 2865 # clone load to be offloaded.
2870 2866 elif repo.ui.configbool(b'ui', b'clonebundlefallback'):
2871 2867 repo.ui.warn(_(b'falling back to normal clone\n'))
2872 2868 else:
2873 2869 raise error.Abort(
2874 2870 _(b'error applying bundle'),
2875 2871 hint=_(
2876 2872 b'if this error persists, consider contacting '
2877 2873 b'the server operator or disable clone '
2878 2874 b'bundles via '
2879 2875 b'"--config ui.clonebundles=false"'
2880 2876 ),
2881 2877 )
2882 2878
2883 2879
2884 2880 def parseclonebundlesmanifest(repo, s):
2885 2881 """Parses the raw text of a clone bundles manifest.
2886 2882
2887 2883 Returns a list of dicts. The dicts have a ``URL`` key corresponding
2888 2884 to the URL and other keys are the attributes for the entry.
2889 2885 """
2890 2886 m = []
2891 2887 for line in s.splitlines():
2892 2888 fields = line.split()
2893 2889 if not fields:
2894 2890 continue
2895 2891 attrs = {b'URL': fields[0]}
2896 2892 for rawattr in fields[1:]:
2897 2893 key, value = rawattr.split(b'=', 1)
2898 2894 key = urlreq.unquote(key)
2899 2895 value = urlreq.unquote(value)
2900 2896 attrs[key] = value
2901 2897
2902 2898 # Parse BUNDLESPEC into components. This makes client-side
2903 2899 # preferences easier to specify since you can prefer a single
2904 2900 # component of the BUNDLESPEC.
2905 2901 if key == b'BUNDLESPEC':
2906 2902 try:
2907 2903 bundlespec = parsebundlespec(repo, value)
2908 2904 attrs[b'COMPRESSION'] = bundlespec.compression
2909 2905 attrs[b'VERSION'] = bundlespec.version
2910 2906 except error.InvalidBundleSpecification:
2911 2907 pass
2912 2908 except error.UnsupportedBundleSpecification:
2913 2909 pass
2914 2910
2915 2911 m.append(attrs)
2916 2912
2917 2913 return m
2918 2914
2919 2915
2920 2916 def isstreamclonespec(bundlespec):
2921 2917 # Stream clone v1
2922 2918 if bundlespec.wirecompression == b'UN' and bundlespec.wireversion == b's1':
2923 2919 return True
2924 2920
2925 2921 # Stream clone v2
2926 2922 if (
2927 2923 bundlespec.wirecompression == b'UN'
2928 2924 and bundlespec.wireversion == b'02'
2929 2925 and bundlespec.contentopts.get(b'streamv2')
2930 2926 ):
2931 2927 return True
2932 2928
2933 2929 return False
2934 2930
2935 2931
2936 2932 def filterclonebundleentries(repo, entries, streamclonerequested=False):
2937 2933 """Remove incompatible clone bundle manifest entries.
2938 2934
2939 2935 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
2940 2936 and returns a new list consisting of only the entries that this client
2941 2937 should be able to apply.
2942 2938
2943 2939 There is no guarantee we'll be able to apply all returned entries because
2944 2940 the metadata we use to filter on may be missing or wrong.
2945 2941 """
2946 2942 newentries = []
2947 2943 for entry in entries:
2948 2944 spec = entry.get(b'BUNDLESPEC')
2949 2945 if spec:
2950 2946 try:
2951 2947 bundlespec = parsebundlespec(repo, spec, strict=True)
2952 2948
2953 2949 # If a stream clone was requested, filter out non-streamclone
2954 2950 # entries.
2955 2951 if streamclonerequested and not isstreamclonespec(bundlespec):
2956 2952 repo.ui.debug(
2957 2953 b'filtering %s because not a stream clone\n'
2958 2954 % entry[b'URL']
2959 2955 )
2960 2956 continue
2961 2957
2962 2958 except error.InvalidBundleSpecification as e:
2963 2959 repo.ui.debug(stringutil.forcebytestr(e) + b'\n')
2964 2960 continue
2965 2961 except error.UnsupportedBundleSpecification as e:
2966 2962 repo.ui.debug(
2967 2963 b'filtering %s because unsupported bundle '
2968 2964 b'spec: %s\n' % (entry[b'URL'], stringutil.forcebytestr(e))
2969 2965 )
2970 2966 continue
2971 2967 # If we don't have a spec and requested a stream clone, we don't know
2972 2968 # what the entry is so don't attempt to apply it.
2973 2969 elif streamclonerequested:
2974 2970 repo.ui.debug(
2975 2971 b'filtering %s because cannot determine if a stream '
2976 2972 b'clone bundle\n' % entry[b'URL']
2977 2973 )
2978 2974 continue
2979 2975
2980 2976 if b'REQUIRESNI' in entry and not sslutil.hassni:
2981 2977 repo.ui.debug(
2982 2978 b'filtering %s because SNI not supported\n' % entry[b'URL']
2983 2979 )
2984 2980 continue
2985 2981
2986 2982 newentries.append(entry)
2987 2983
2988 2984 return newentries
2989 2985
2990 2986
2991 2987 class clonebundleentry(object):
2992 2988 """Represents an item in a clone bundles manifest.
2993 2989
2994 2990 This rich class is needed to support sorting since sorted() in Python 3
2995 2991 doesn't support ``cmp`` and our comparison is complex enough that ``key=``
2996 2992 won't work.
2997 2993 """
2998 2994
2999 2995 def __init__(self, value, prefers):
3000 2996 self.value = value
3001 2997 self.prefers = prefers
3002 2998
3003 2999 def _cmp(self, other):
3004 3000 for prefkey, prefvalue in self.prefers:
3005 3001 avalue = self.value.get(prefkey)
3006 3002 bvalue = other.value.get(prefkey)
3007 3003
3008 3004 # Special case for b missing attribute and a matches exactly.
3009 3005 if avalue is not None and bvalue is None and avalue == prefvalue:
3010 3006 return -1
3011 3007
3012 3008 # Special case for a missing attribute and b matches exactly.
3013 3009 if bvalue is not None and avalue is None and bvalue == prefvalue:
3014 3010 return 1
3015 3011
3016 3012 # We can't compare unless attribute present on both.
3017 3013 if avalue is None or bvalue is None:
3018 3014 continue
3019 3015
3020 3016 # Same values should fall back to next attribute.
3021 3017 if avalue == bvalue:
3022 3018 continue
3023 3019
3024 3020 # Exact matches come first.
3025 3021 if avalue == prefvalue:
3026 3022 return -1
3027 3023 if bvalue == prefvalue:
3028 3024 return 1
3029 3025
3030 3026 # Fall back to next attribute.
3031 3027 continue
3032 3028
3033 3029 # If we got here we couldn't sort by attributes and prefers. Fall
3034 3030 # back to index order.
3035 3031 return 0
3036 3032
3037 3033 def __lt__(self, other):
3038 3034 return self._cmp(other) < 0
3039 3035
3040 3036 def __gt__(self, other):
3041 3037 return self._cmp(other) > 0
3042 3038
3043 3039 def __eq__(self, other):
3044 3040 return self._cmp(other) == 0
3045 3041
3046 3042 def __le__(self, other):
3047 3043 return self._cmp(other) <= 0
3048 3044
3049 3045 def __ge__(self, other):
3050 3046 return self._cmp(other) >= 0
3051 3047
3052 3048 def __ne__(self, other):
3053 3049 return self._cmp(other) != 0
3054 3050
3055 3051
3056 3052 def sortclonebundleentries(ui, entries):
3057 3053 prefers = ui.configlist(b'ui', b'clonebundleprefers')
3058 3054 if not prefers:
3059 3055 return list(entries)
3060 3056
3061 3057 prefers = [p.split(b'=', 1) for p in prefers]
3062 3058
3063 3059 items = sorted(clonebundleentry(v, prefers) for v in entries)
3064 3060 return [i.value for i in items]
3065 3061
3066 3062
3067 3063 def trypullbundlefromurl(ui, repo, url):
3068 3064 """Attempt to apply a bundle from a URL."""
3069 3065 with repo.lock(), repo.transaction(b'bundleurl') as tr:
3070 3066 try:
3071 3067 fh = urlmod.open(ui, url)
3072 3068 cg = readbundle(ui, fh, b'stream')
3073 3069
3074 3070 if isinstance(cg, streamclone.streamcloneapplier):
3075 3071 cg.apply(repo)
3076 3072 else:
3077 3073 bundle2.applybundle(repo, cg, tr, b'clonebundles', url)
3078 3074 return True
3079 3075 except urlerr.httperror as e:
3080 3076 ui.warn(
3081 3077 _(b'HTTP error fetching bundle: %s\n')
3082 3078 % stringutil.forcebytestr(e)
3083 3079 )
3084 3080 except urlerr.urlerror as e:
3085 3081 ui.warn(
3086 3082 _(b'error fetching bundle: %s\n')
3087 3083 % stringutil.forcebytestr(e.reason)
3088 3084 )
3089 3085
3090 3086 return False
@@ -1,1035 +1,1040 b''
1 1 # obsutil.py - utility functions for obsolescence
2 2 #
3 3 # Copyright 2017 Boris Feld <boris.feld@octobus.net>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import re
11 11
12 12 from .i18n import _
13 13 from . import (
14 14 diffutil,
15 15 encoding,
16 16 node as nodemod,
17 17 phases,
18 18 pycompat,
19 19 util,
20 20 )
21 21 from .utils import dateutil
22 22
23 23 ### obsolescence marker flag
24 24
25 25 ## bumpedfix flag
26 26 #
27 27 # When a changeset A' succeed to a changeset A which became public, we call A'
28 28 # "bumped" because it's a successors of a public changesets
29 29 #
30 30 # o A' (bumped)
31 31 # |`:
32 32 # | o A
33 33 # |/
34 34 # o Z
35 35 #
36 36 # The way to solve this situation is to create a new changeset Ad as children
37 37 # of A. This changeset have the same content than A'. So the diff from A to A'
38 38 # is the same than the diff from A to Ad. Ad is marked as a successors of A'
39 39 #
40 40 # o Ad
41 41 # |`:
42 42 # | x A'
43 43 # |'|
44 44 # o | A
45 45 # |/
46 46 # o Z
47 47 #
48 48 # But by transitivity Ad is also a successors of A. To avoid having Ad marked
49 49 # as bumped too, we add the `bumpedfix` flag to the marker. <A', (Ad,)>.
50 50 # This flag mean that the successors express the changes between the public and
51 51 # bumped version and fix the situation, breaking the transitivity of
52 52 # "bumped" here.
53 53 bumpedfix = 1
54 54 usingsha256 = 2
55 55
56 56
57 57 class marker(object):
58 58 """Wrap obsolete marker raw data"""
59 59
60 60 def __init__(self, repo, data):
61 61 # the repo argument will be used to create changectx in later version
62 62 self._repo = repo
63 63 self._data = data
64 64 self._decodedmeta = None
65 65
66 66 def __hash__(self):
67 67 return hash(self._data)
68 68
69 69 def __eq__(self, other):
70 70 if type(other) != type(self):
71 71 return False
72 72 return self._data == other._data
73 73
74 74 def prednode(self):
75 75 """Predecessor changeset node identifier"""
76 76 return self._data[0]
77 77
78 78 def succnodes(self):
79 79 """List of successor changesets node identifiers"""
80 80 return self._data[1]
81 81
82 82 def parentnodes(self):
83 83 """Parents of the predecessors (None if not recorded)"""
84 84 return self._data[5]
85 85
86 86 def metadata(self):
87 87 """Decoded metadata dictionary"""
88 88 return dict(self._data[3])
89 89
90 90 def date(self):
91 91 """Creation date as (unixtime, offset)"""
92 92 return self._data[4]
93 93
94 94 def flags(self):
95 95 """The flags field of the marker"""
96 96 return self._data[2]
97 97
98 98
99 99 def getmarkers(repo, nodes=None, exclusive=False):
100 100 """returns markers known in a repository
101 101
102 102 If <nodes> is specified, only markers "relevant" to those nodes are are
103 103 returned"""
104 104 if nodes is None:
105 105 rawmarkers = repo.obsstore
106 106 elif exclusive:
107 107 rawmarkers = exclusivemarkers(repo, nodes)
108 108 else:
109 109 rawmarkers = repo.obsstore.relevantmarkers(nodes)
110 110
111 111 for markerdata in rawmarkers:
112 112 yield marker(repo, markerdata)
113 113
114 114
115 def sortedmarkers(markers):
116 # last item of marker tuple ('parents') may be None or a tuple
117 return sorted(markers, key=lambda m: m[:-1] + (m[-1] or (),))
118
119
115 120 def closestpredecessors(repo, nodeid):
116 121 """yield the list of next predecessors pointing on visible changectx nodes
117 122
118 123 This function respect the repoview filtering, filtered revision will be
119 124 considered missing.
120 125 """
121 126
122 127 precursors = repo.obsstore.predecessors
123 128 stack = [nodeid]
124 129 seen = set(stack)
125 130
126 131 while stack:
127 132 current = stack.pop()
128 133 currentpreccs = precursors.get(current, ())
129 134
130 135 for prec in currentpreccs:
131 136 precnodeid = prec[0]
132 137
133 138 # Basic cycle protection
134 139 if precnodeid in seen:
135 140 continue
136 141 seen.add(precnodeid)
137 142
138 143 if precnodeid in repo:
139 144 yield precnodeid
140 145 else:
141 146 stack.append(precnodeid)
142 147
143 148
144 149 def allpredecessors(obsstore, nodes, ignoreflags=0):
145 150 """Yield node for every precursors of <nodes>.
146 151
147 152 Some precursors may be unknown locally.
148 153
149 154 This is a linear yield unsuited to detecting folded changesets. It includes
150 155 initial nodes too."""
151 156
152 157 remaining = set(nodes)
153 158 seen = set(remaining)
154 159 prec = obsstore.predecessors.get
155 160 while remaining:
156 161 current = remaining.pop()
157 162 yield current
158 163 for mark in prec(current, ()):
159 164 # ignore marker flagged with specified flag
160 165 if mark[2] & ignoreflags:
161 166 continue
162 167 suc = mark[0]
163 168 if suc not in seen:
164 169 seen.add(suc)
165 170 remaining.add(suc)
166 171
167 172
168 173 def allsuccessors(obsstore, nodes, ignoreflags=0):
169 174 """Yield node for every successor of <nodes>.
170 175
171 176 Some successors may be unknown locally.
172 177
173 178 This is a linear yield unsuited to detecting split changesets. It includes
174 179 initial nodes too."""
175 180 remaining = set(nodes)
176 181 seen = set(remaining)
177 182 while remaining:
178 183 current = remaining.pop()
179 184 yield current
180 185 for mark in obsstore.successors.get(current, ()):
181 186 # ignore marker flagged with specified flag
182 187 if mark[2] & ignoreflags:
183 188 continue
184 189 for suc in mark[1]:
185 190 if suc not in seen:
186 191 seen.add(suc)
187 192 remaining.add(suc)
188 193
189 194
190 195 def _filterprunes(markers):
191 196 """return a set with no prune markers"""
192 197 return set(m for m in markers if m[1])
193 198
194 199
195 200 def exclusivemarkers(repo, nodes):
196 201 """set of markers relevant to "nodes" but no other locally-known nodes
197 202
198 203 This function compute the set of markers "exclusive" to a locally-known
199 204 node. This means we walk the markers starting from <nodes> until we reach a
200 205 locally-known precursors outside of <nodes>. Element of <nodes> with
201 206 locally-known successors outside of <nodes> are ignored (since their
202 207 precursors markers are also relevant to these successors).
203 208
204 209 For example:
205 210
206 211 # (A0 rewritten as A1)
207 212 #
208 213 # A0 <-1- A1 # Marker "1" is exclusive to A1
209 214
210 215 or
211 216
212 217 # (A0 rewritten as AX; AX rewritten as A1; AX is unkown locally)
213 218 #
214 219 # <-1- A0 <-2- AX <-3- A1 # Marker "2,3" are exclusive to A1
215 220
216 221 or
217 222
218 223 # (A0 has unknown precursors, A0 rewritten as A1 and A2 (divergence))
219 224 #
220 225 # <-2- A1 # Marker "2" is exclusive to A0,A1
221 226 # /
222 227 # <-1- A0
223 228 # \
224 229 # <-3- A2 # Marker "3" is exclusive to A0,A2
225 230 #
226 231 # in addition:
227 232 #
228 233 # Markers "2,3" are exclusive to A1,A2
229 234 # Markers "1,2,3" are exclusive to A0,A1,A2
230 235
231 236 See test/test-obsolete-bundle-strip.t for more examples.
232 237
233 238 An example usage is strip. When stripping a changeset, we also want to
234 239 strip the markers exclusive to this changeset. Otherwise we would have
235 240 "dangling"" obsolescence markers from its precursors: Obsolescence markers
236 241 marking a node as obsolete without any successors available locally.
237 242
238 243 As for relevant markers, the prune markers for children will be followed.
239 244 Of course, they will only be followed if the pruned children is
240 245 locally-known. Since the prune markers are relevant to the pruned node.
241 246 However, while prune markers are considered relevant to the parent of the
242 247 pruned changesets, prune markers for locally-known changeset (with no
243 248 successors) are considered exclusive to the pruned nodes. This allows
244 249 to strip the prune markers (with the rest of the exclusive chain) alongside
245 250 the pruned changesets.
246 251 """
247 252 # running on a filtered repository would be dangerous as markers could be
248 253 # reported as exclusive when they are relevant for other filtered nodes.
249 254 unfi = repo.unfiltered()
250 255
251 256 # shortcut to various useful item
252 257 nm = unfi.changelog.nodemap
253 258 precursorsmarkers = unfi.obsstore.predecessors
254 259 successormarkers = unfi.obsstore.successors
255 260 childrenmarkers = unfi.obsstore.children
256 261
257 262 # exclusive markers (return of the function)
258 263 exclmarkers = set()
259 264 # we need fast membership testing
260 265 nodes = set(nodes)
261 266 # looking for head in the obshistory
262 267 #
263 268 # XXX we are ignoring all issues in regard with cycle for now.
264 269 stack = [n for n in nodes if not _filterprunes(successormarkers.get(n, ()))]
265 270 stack.sort()
266 271 # nodes already stacked
267 272 seennodes = set(stack)
268 273 while stack:
269 274 current = stack.pop()
270 275 # fetch precursors markers
271 276 markers = list(precursorsmarkers.get(current, ()))
272 277 # extend the list with prune markers
273 278 for mark in successormarkers.get(current, ()):
274 279 if not mark[1]:
275 280 markers.append(mark)
276 281 # and markers from children (looking for prune)
277 282 for mark in childrenmarkers.get(current, ()):
278 283 if not mark[1]:
279 284 markers.append(mark)
280 285 # traverse the markers
281 286 for mark in markers:
282 287 if mark in exclmarkers:
283 288 # markers already selected
284 289 continue
285 290
286 291 # If the markers is about the current node, select it
287 292 #
288 293 # (this delay the addition of markers from children)
289 294 if mark[1] or mark[0] == current:
290 295 exclmarkers.add(mark)
291 296
292 297 # should we keep traversing through the precursors?
293 298 prec = mark[0]
294 299
295 300 # nodes in the stack or already processed
296 301 if prec in seennodes:
297 302 continue
298 303
299 304 # is this a locally known node ?
300 305 known = prec in nm
301 306 # if locally-known and not in the <nodes> set the traversal
302 307 # stop here.
303 308 if known and prec not in nodes:
304 309 continue
305 310
306 311 # do not keep going if there are unselected markers pointing to this
307 312 # nodes. If we end up traversing these unselected markers later the
308 313 # node will be taken care of at that point.
309 314 precmarkers = _filterprunes(successormarkers.get(prec))
310 315 if precmarkers.issubset(exclmarkers):
311 316 seennodes.add(prec)
312 317 stack.append(prec)
313 318
314 319 return exclmarkers
315 320
316 321
317 322 def foreground(repo, nodes):
318 323 """return all nodes in the "foreground" of other node
319 324
320 325 The foreground of a revision is anything reachable using parent -> children
321 326 or precursor -> successor relation. It is very similar to "descendant" but
322 327 augmented with obsolescence information.
323 328
324 329 Beware that possible obsolescence cycle may result if complex situation.
325 330 """
326 331 repo = repo.unfiltered()
327 332 foreground = set(repo.set(b'%ln::', nodes))
328 333 if repo.obsstore:
329 334 # We only need this complicated logic if there is obsolescence
330 335 # XXX will probably deserve an optimised revset.
331 336 nm = repo.changelog.nodemap
332 337 plen = -1
333 338 # compute the whole set of successors or descendants
334 339 while len(foreground) != plen:
335 340 plen = len(foreground)
336 341 succs = set(c.node() for c in foreground)
337 342 mutable = [c.node() for c in foreground if c.mutable()]
338 343 succs.update(allsuccessors(repo.obsstore, mutable))
339 344 known = (n for n in succs if n in nm)
340 345 foreground = set(repo.set(b'%ln::', known))
341 346 return set(c.node() for c in foreground)
342 347
343 348
344 349 # effectflag field
345 350 #
346 351 # Effect-flag is a 1-byte bit field used to store what changed between a
347 352 # changeset and its successor(s).
348 353 #
349 354 # The effect flag is stored in obs-markers metadata while we iterate on the
350 355 # information design. That's why we have the EFFECTFLAGFIELD. If we come up
351 356 # with an incompatible design for effect flag, we can store a new design under
352 357 # another field name so we don't break readers. We plan to extend the existing
353 358 # obsmarkers bit-field when the effect flag design will be stabilized.
354 359 #
355 360 # The effect-flag is placed behind an experimental flag
356 361 # `effect-flags` set to off by default.
357 362 #
358 363
359 364 EFFECTFLAGFIELD = b"ef1"
360 365
361 366 DESCCHANGED = 1 << 0 # action changed the description
362 367 METACHANGED = 1 << 1 # action change the meta
363 368 DIFFCHANGED = 1 << 3 # action change diff introduced by the changeset
364 369 PARENTCHANGED = 1 << 2 # action change the parent
365 370 USERCHANGED = 1 << 4 # the user changed
366 371 DATECHANGED = 1 << 5 # the date changed
367 372 BRANCHCHANGED = 1 << 6 # the branch changed
368 373
369 374 METABLACKLIST = [
370 375 re.compile(b'^branch$'),
371 376 re.compile(b'^.*-source$'),
372 377 re.compile(b'^.*_source$'),
373 378 re.compile(b'^source$'),
374 379 ]
375 380
376 381
377 382 def metanotblacklisted(metaitem):
378 383 """ Check that the key of a meta item (extrakey, extravalue) does not
379 384 match at least one of the blacklist pattern
380 385 """
381 386 metakey = metaitem[0]
382 387
383 388 return not any(pattern.match(metakey) for pattern in METABLACKLIST)
384 389
385 390
386 391 def _prepare_hunk(hunk):
387 392 """Drop all information but the username and patch"""
388 393 cleanhunk = []
389 394 for line in hunk.splitlines():
390 395 if line.startswith(b'# User') or not line.startswith(b'#'):
391 396 if line.startswith(b'@@'):
392 397 line = b'@@\n'
393 398 cleanhunk.append(line)
394 399 return cleanhunk
395 400
396 401
397 402 def _getdifflines(iterdiff):
398 403 """return a cleaned up lines"""
399 404 lines = next(iterdiff, None)
400 405
401 406 if lines is None:
402 407 return lines
403 408
404 409 return _prepare_hunk(lines)
405 410
406 411
407 412 def _cmpdiff(leftctx, rightctx):
408 413 """return True if both ctx introduce the "same diff"
409 414
410 415 This is a first and basic implementation, with many shortcoming.
411 416 """
412 417 diffopts = diffutil.diffallopts(leftctx.repo().ui, {b'git': True})
413 418
414 419 # Leftctx or right ctx might be filtered, so we need to use the contexts
415 420 # with an unfiltered repository to safely compute the diff
416 421
417 422 # leftctx and rightctx can be from different repository views in case of
418 423 # hgsubversion, do don't try to access them from same repository
419 424 # rightctx.repo() and leftctx.repo() are not always the same
420 425 leftunfi = leftctx._repo.unfiltered()[leftctx.rev()]
421 426 leftdiff = leftunfi.diff(opts=diffopts)
422 427 rightunfi = rightctx._repo.unfiltered()[rightctx.rev()]
423 428 rightdiff = rightunfi.diff(opts=diffopts)
424 429
425 430 left, right = (0, 0)
426 431 while None not in (left, right):
427 432 left = _getdifflines(leftdiff)
428 433 right = _getdifflines(rightdiff)
429 434
430 435 if left != right:
431 436 return False
432 437 return True
433 438
434 439
435 440 def geteffectflag(source, successors):
436 441 """ From an obs-marker relation, compute what changed between the
437 442 predecessor and the successor.
438 443 """
439 444 effects = 0
440 445
441 446 for changectx in successors:
442 447 # Check if description has changed
443 448 if changectx.description() != source.description():
444 449 effects |= DESCCHANGED
445 450
446 451 # Check if user has changed
447 452 if changectx.user() != source.user():
448 453 effects |= USERCHANGED
449 454
450 455 # Check if date has changed
451 456 if changectx.date() != source.date():
452 457 effects |= DATECHANGED
453 458
454 459 # Check if branch has changed
455 460 if changectx.branch() != source.branch():
456 461 effects |= BRANCHCHANGED
457 462
458 463 # Check if at least one of the parent has changed
459 464 if changectx.parents() != source.parents():
460 465 effects |= PARENTCHANGED
461 466
462 467 # Check if other meta has changed
463 468 changeextra = changectx.extra().items()
464 469 ctxmeta = list(filter(metanotblacklisted, changeextra))
465 470
466 471 sourceextra = source.extra().items()
467 472 srcmeta = list(filter(metanotblacklisted, sourceextra))
468 473
469 474 if ctxmeta != srcmeta:
470 475 effects |= METACHANGED
471 476
472 477 # Check if the diff has changed
473 478 if not _cmpdiff(source, changectx):
474 479 effects |= DIFFCHANGED
475 480
476 481 return effects
477 482
478 483
479 484 def getobsoleted(repo, tr):
480 485 """return the set of pre-existing revisions obsoleted by a transaction"""
481 486 torev = repo.unfiltered().changelog.nodemap.get
482 487 phase = repo._phasecache.phase
483 488 succsmarkers = repo.obsstore.successors.get
484 489 public = phases.public
485 490 addedmarkers = tr.changes[b'obsmarkers']
486 491 origrepolen = tr.changes[b'origrepolen']
487 492 seenrevs = set()
488 493 obsoleted = set()
489 494 for mark in addedmarkers:
490 495 node = mark[0]
491 496 rev = torev(node)
492 497 if rev is None or rev in seenrevs or rev >= origrepolen:
493 498 continue
494 499 seenrevs.add(rev)
495 500 if phase(repo, rev) == public:
496 501 continue
497 502 if set(succsmarkers(node) or []).issubset(addedmarkers):
498 503 obsoleted.add(rev)
499 504 return obsoleted
500 505
501 506
502 507 class _succs(list):
503 508 """small class to represent a successors with some metadata about it"""
504 509
505 510 def __init__(self, *args, **kwargs):
506 511 super(_succs, self).__init__(*args, **kwargs)
507 512 self.markers = set()
508 513
509 514 def copy(self):
510 515 new = _succs(self)
511 516 new.markers = self.markers.copy()
512 517 return new
513 518
514 519 @util.propertycache
515 520 def _set(self):
516 521 # immutable
517 522 return set(self)
518 523
519 524 def canmerge(self, other):
520 525 return self._set.issubset(other._set)
521 526
522 527
523 528 def successorssets(repo, initialnode, closest=False, cache=None):
524 529 """Return set of all latest successors of initial nodes
525 530
526 531 The successors set of a changeset A are the group of revisions that succeed
527 532 A. It succeeds A as a consistent whole, each revision being only a partial
528 533 replacement. By default, the successors set contains non-obsolete
529 534 changesets only, walking the obsolescence graph until reaching a leaf. If
530 535 'closest' is set to True, closest successors-sets are return (the
531 536 obsolescence walk stops on known changesets).
532 537
533 538 This function returns the full list of successor sets which is why it
534 539 returns a list of tuples and not just a single tuple. Each tuple is a valid
535 540 successors set. Note that (A,) may be a valid successors set for changeset A
536 541 (see below).
537 542
538 543 In most cases, a changeset A will have a single element (e.g. the changeset
539 544 A is replaced by A') in its successors set. Though, it is also common for a
540 545 changeset A to have no elements in its successor set (e.g. the changeset
541 546 has been pruned). Therefore, the returned list of successors sets will be
542 547 [(A',)] or [], respectively.
543 548
544 549 When a changeset A is split into A' and B', however, it will result in a
545 550 successors set containing more than a single element, i.e. [(A',B')].
546 551 Divergent changesets will result in multiple successors sets, i.e. [(A',),
547 552 (A'')].
548 553
549 554 If a changeset A is not obsolete, then it will conceptually have no
550 555 successors set. To distinguish this from a pruned changeset, the successor
551 556 set will contain itself only, i.e. [(A,)].
552 557
553 558 Finally, final successors unknown locally are considered to be pruned
554 559 (pruned: obsoleted without any successors). (Final: successors not affected
555 560 by markers).
556 561
557 562 The 'closest' mode respect the repoview filtering. For example, without
558 563 filter it will stop at the first locally known changeset, with 'visible'
559 564 filter it will stop on visible changesets).
560 565
561 566 The optional `cache` parameter is a dictionary that may contains
562 567 precomputed successors sets. It is meant to reuse the computation of a
563 568 previous call to `successorssets` when multiple calls are made at the same
564 569 time. The cache dictionary is updated in place. The caller is responsible
565 570 for its life span. Code that makes multiple calls to `successorssets`
566 571 *should* use this cache mechanism or risk a performance hit.
567 572
568 573 Since results are different depending of the 'closest' most, the same cache
569 574 cannot be reused for both mode.
570 575 """
571 576
572 577 succmarkers = repo.obsstore.successors
573 578
574 579 # Stack of nodes we search successors sets for
575 580 toproceed = [initialnode]
576 581 # set version of above list for fast loop detection
577 582 # element added to "toproceed" must be added here
578 583 stackedset = set(toproceed)
579 584 if cache is None:
580 585 cache = {}
581 586
582 587 # This while loop is the flattened version of a recursive search for
583 588 # successors sets
584 589 #
585 590 # def successorssets(x):
586 591 # successors = directsuccessors(x)
587 592 # ss = [[]]
588 593 # for succ in directsuccessors(x):
589 594 # # product as in itertools cartesian product
590 595 # ss = product(ss, successorssets(succ))
591 596 # return ss
592 597 #
593 598 # But we can not use plain recursive calls here:
594 599 # - that would blow the python call stack
595 600 # - obsolescence markers may have cycles, we need to handle them.
596 601 #
597 602 # The `toproceed` list act as our call stack. Every node we search
598 603 # successors set for are stacked there.
599 604 #
600 605 # The `stackedset` is set version of this stack used to check if a node is
601 606 # already stacked. This check is used to detect cycles and prevent infinite
602 607 # loop.
603 608 #
604 609 # successors set of all nodes are stored in the `cache` dictionary.
605 610 #
606 611 # After this while loop ends we use the cache to return the successors sets
607 612 # for the node requested by the caller.
608 613 while toproceed:
609 614 # Every iteration tries to compute the successors sets of the topmost
610 615 # node of the stack: CURRENT.
611 616 #
612 617 # There are four possible outcomes:
613 618 #
614 619 # 1) We already know the successors sets of CURRENT:
615 620 # -> mission accomplished, pop it from the stack.
616 621 # 2) Stop the walk:
617 622 # default case: Node is not obsolete
618 623 # closest case: Node is known at this repo filter level
619 624 # -> the node is its own successors sets. Add it to the cache.
620 625 # 3) We do not know successors set of direct successors of CURRENT:
621 626 # -> We add those successors to the stack.
622 627 # 4) We know successors sets of all direct successors of CURRENT:
623 628 # -> We can compute CURRENT successors set and add it to the
624 629 # cache.
625 630 #
626 631 current = toproceed[-1]
627 632
628 633 # case 2 condition is a bit hairy because of closest,
629 634 # we compute it on its own
630 635 case2condition = (current not in succmarkers) or (
631 636 closest and current != initialnode and current in repo
632 637 )
633 638
634 639 if current in cache:
635 640 # case (1): We already know the successors sets
636 641 stackedset.remove(toproceed.pop())
637 642 elif case2condition:
638 643 # case (2): end of walk.
639 644 if current in repo:
640 645 # We have a valid successors.
641 646 cache[current] = [_succs((current,))]
642 647 else:
643 648 # Final obsolete version is unknown locally.
644 649 # Do not count that as a valid successors
645 650 cache[current] = []
646 651 else:
647 652 # cases (3) and (4)
648 653 #
649 654 # We proceed in two phases. Phase 1 aims to distinguish case (3)
650 655 # from case (4):
651 656 #
652 657 # For each direct successors of CURRENT, we check whether its
653 658 # successors sets are known. If they are not, we stack the
654 659 # unknown node and proceed to the next iteration of the while
655 660 # loop. (case 3)
656 661 #
657 662 # During this step, we may detect obsolescence cycles: a node
658 663 # with unknown successors sets but already in the call stack.
659 664 # In such a situation, we arbitrary set the successors sets of
660 665 # the node to nothing (node pruned) to break the cycle.
661 666 #
662 667 # If no break was encountered we proceed to phase 2.
663 668 #
664 669 # Phase 2 computes successors sets of CURRENT (case 4); see details
665 670 # in phase 2 itself.
666 671 #
667 672 # Note the two levels of iteration in each phase.
668 673 # - The first one handles obsolescence markers using CURRENT as
669 674 # precursor (successors markers of CURRENT).
670 675 #
671 676 # Having multiple entry here means divergence.
672 677 #
673 678 # - The second one handles successors defined in each marker.
674 679 #
675 680 # Having none means pruned node, multiple successors means split,
676 681 # single successors are standard replacement.
677 682 #
678 for mark in sorted(succmarkers[current]):
683 for mark in sortedmarkers(succmarkers[current]):
679 684 for suc in mark[1]:
680 685 if suc not in cache:
681 686 if suc in stackedset:
682 687 # cycle breaking
683 688 cache[suc] = []
684 689 else:
685 690 # case (3) If we have not computed successors sets
686 691 # of one of those successors we add it to the
687 692 # `toproceed` stack and stop all work for this
688 693 # iteration.
689 694 toproceed.append(suc)
690 695 stackedset.add(suc)
691 696 break
692 697 else:
693 698 continue
694 699 break
695 700 else:
696 701 # case (4): we know all successors sets of all direct
697 702 # successors
698 703 #
699 704 # Successors set contributed by each marker depends on the
700 705 # successors sets of all its "successors" node.
701 706 #
702 707 # Each different marker is a divergence in the obsolescence
703 708 # history. It contributes successors sets distinct from other
704 709 # markers.
705 710 #
706 711 # Within a marker, a successor may have divergent successors
707 712 # sets. In such a case, the marker will contribute multiple
708 713 # divergent successors sets. If multiple successors have
709 714 # divergent successors sets, a Cartesian product is used.
710 715 #
711 716 # At the end we post-process successors sets to remove
712 717 # duplicated entry and successors set that are strict subset of
713 718 # another one.
714 719 succssets = []
715 for mark in sorted(succmarkers[current]):
720 for mark in sortedmarkers(succmarkers[current]):
716 721 # successors sets contributed by this marker
717 722 base = _succs()
718 723 base.markers.add(mark)
719 724 markss = [base]
720 725 for suc in mark[1]:
721 726 # cardinal product with previous successors
722 727 productresult = []
723 728 for prefix in markss:
724 729 for suffix in cache[suc]:
725 730 newss = prefix.copy()
726 731 newss.markers.update(suffix.markers)
727 732 for part in suffix:
728 733 # do not duplicated entry in successors set
729 734 # first entry wins.
730 735 if part not in newss:
731 736 newss.append(part)
732 737 productresult.append(newss)
733 738 if productresult:
734 739 markss = productresult
735 740 succssets.extend(markss)
736 741 # remove duplicated and subset
737 742 seen = []
738 743 final = []
739 744 candidates = sorted(
740 745 (s for s in succssets if s), key=len, reverse=True
741 746 )
742 747 for cand in candidates:
743 748 for seensuccs in seen:
744 749 if cand.canmerge(seensuccs):
745 750 seensuccs.markers.update(cand.markers)
746 751 break
747 752 else:
748 753 final.append(cand)
749 754 seen.append(cand)
750 755 final.reverse() # put small successors set first
751 756 cache[current] = final
752 757 return cache[initialnode]
753 758
754 759
755 760 def successorsandmarkers(repo, ctx):
756 761 """compute the raw data needed for computing obsfate
757 762 Returns a list of dict, one dict per successors set
758 763 """
759 764 if not ctx.obsolete():
760 765 return None
761 766
762 767 ssets = successorssets(repo, ctx.node(), closest=True)
763 768
764 769 # closestsuccessors returns an empty list for pruned revisions, remap it
765 770 # into a list containing an empty list for future processing
766 771 if ssets == []:
767 772 ssets = [[]]
768 773
769 774 # Try to recover pruned markers
770 775 succsmap = repo.obsstore.successors
771 776 fullsuccessorsets = [] # successor set + markers
772 777 for sset in ssets:
773 778 if sset:
774 779 fullsuccessorsets.append(sset)
775 780 else:
776 781 # successorsset return an empty set() when ctx or one of its
777 782 # successors is pruned.
778 783 # In this case, walk the obs-markers tree again starting with ctx
779 784 # and find the relevant pruning obs-makers, the ones without
780 785 # successors.
781 786 # Having these markers allow us to compute some information about
782 787 # its fate, like who pruned this changeset and when.
783 788
784 789 # XXX we do not catch all prune markers (eg rewritten then pruned)
785 790 # (fix me later)
786 791 foundany = False
787 792 for mark in succsmap.get(ctx.node(), ()):
788 793 if not mark[1]:
789 794 foundany = True
790 795 sset = _succs()
791 796 sset.markers.add(mark)
792 797 fullsuccessorsets.append(sset)
793 798 if not foundany:
794 799 fullsuccessorsets.append(_succs())
795 800
796 801 values = []
797 802 for sset in fullsuccessorsets:
798 803 values.append({b'successors': sset, b'markers': sset.markers})
799 804
800 805 return values
801 806
802 807
803 808 def _getobsfate(successorssets):
804 809 """ Compute a changeset obsolescence fate based on its successorssets.
805 810 Successors can be the tipmost ones or the immediate ones. This function
806 811 return values are not meant to be shown directly to users, it is meant to
807 812 be used by internal functions only.
808 813 Returns one fate from the following values:
809 814 - pruned
810 815 - diverged
811 816 - superseded
812 817 - superseded_split
813 818 """
814 819
815 820 if len(successorssets) == 0:
816 821 # The commit has been pruned
817 822 return b'pruned'
818 823 elif len(successorssets) > 1:
819 824 return b'diverged'
820 825 else:
821 826 # No divergence, only one set of successors
822 827 successors = successorssets[0]
823 828
824 829 if len(successors) == 1:
825 830 return b'superseded'
826 831 else:
827 832 return b'superseded_split'
828 833
829 834
830 835 def obsfateverb(successorset, markers):
831 836 """ Return the verb summarizing the successorset and potentially using
832 837 information from the markers
833 838 """
834 839 if not successorset:
835 840 verb = b'pruned'
836 841 elif len(successorset) == 1:
837 842 verb = b'rewritten'
838 843 else:
839 844 verb = b'split'
840 845 return verb
841 846
842 847
843 848 def markersdates(markers):
844 849 """returns the list of dates for a list of markers
845 850 """
846 851 return [m[4] for m in markers]
847 852
848 853
849 854 def markersusers(markers):
850 855 """ Returns a sorted list of markers users without duplicates
851 856 """
852 857 markersmeta = [dict(m[3]) for m in markers]
853 858 users = set(
854 859 encoding.tolocal(meta[b'user'])
855 860 for meta in markersmeta
856 861 if meta.get(b'user')
857 862 )
858 863
859 864 return sorted(users)
860 865
861 866
862 867 def markersoperations(markers):
863 868 """ Returns a sorted list of markers operations without duplicates
864 869 """
865 870 markersmeta = [dict(m[3]) for m in markers]
866 871 operations = set(
867 872 meta.get(b'operation') for meta in markersmeta if meta.get(b'operation')
868 873 )
869 874
870 875 return sorted(operations)
871 876
872 877
873 878 def obsfateprinter(ui, repo, successors, markers, formatctx):
874 879 """ Build a obsfate string for a single successorset using all obsfate
875 880 related function defined in obsutil
876 881 """
877 882 quiet = ui.quiet
878 883 verbose = ui.verbose
879 884 normal = not verbose and not quiet
880 885
881 886 line = []
882 887
883 888 # Verb
884 889 line.append(obsfateverb(successors, markers))
885 890
886 891 # Operations
887 892 operations = markersoperations(markers)
888 893 if operations:
889 894 line.append(b" using %s" % b", ".join(operations))
890 895
891 896 # Successors
892 897 if successors:
893 898 fmtsuccessors = [formatctx(repo[succ]) for succ in successors]
894 899 line.append(b" as %s" % b", ".join(fmtsuccessors))
895 900
896 901 # Users
897 902 users = markersusers(markers)
898 903 # Filter out current user in not verbose mode to reduce amount of
899 904 # information
900 905 if not verbose:
901 906 currentuser = ui.username(acceptempty=True)
902 907 if len(users) == 1 and currentuser in users:
903 908 users = None
904 909
905 910 if (verbose or normal) and users:
906 911 line.append(b" by %s" % b", ".join(users))
907 912
908 913 # Date
909 914 dates = markersdates(markers)
910 915
911 916 if dates and verbose:
912 917 min_date = min(dates)
913 918 max_date = max(dates)
914 919
915 920 if min_date == max_date:
916 921 fmtmin_date = dateutil.datestr(min_date, b'%Y-%m-%d %H:%M %1%2')
917 922 line.append(b" (at %s)" % fmtmin_date)
918 923 else:
919 924 fmtmin_date = dateutil.datestr(min_date, b'%Y-%m-%d %H:%M %1%2')
920 925 fmtmax_date = dateutil.datestr(max_date, b'%Y-%m-%d %H:%M %1%2')
921 926 line.append(b" (between %s and %s)" % (fmtmin_date, fmtmax_date))
922 927
923 928 return b"".join(line)
924 929
925 930
926 931 filteredmsgtable = {
927 932 b"pruned": _(b"hidden revision '%s' is pruned"),
928 933 b"diverged": _(b"hidden revision '%s' has diverged"),
929 934 b"superseded": _(b"hidden revision '%s' was rewritten as: %s"),
930 935 b"superseded_split": _(b"hidden revision '%s' was split as: %s"),
931 936 b"superseded_split_several": _(
932 937 b"hidden revision '%s' was split as: %s and %d more"
933 938 ),
934 939 }
935 940
936 941
937 942 def _getfilteredreason(repo, changeid, ctx):
938 943 """return a human-friendly string on why a obsolete changeset is hidden
939 944 """
940 945 successors = successorssets(repo, ctx.node())
941 946 fate = _getobsfate(successors)
942 947
943 948 # Be more precise in case the revision is superseded
944 949 if fate == b'pruned':
945 950 return filteredmsgtable[b'pruned'] % changeid
946 951 elif fate == b'diverged':
947 952 return filteredmsgtable[b'diverged'] % changeid
948 953 elif fate == b'superseded':
949 954 single_successor = nodemod.short(successors[0][0])
950 955 return filteredmsgtable[b'superseded'] % (changeid, single_successor)
951 956 elif fate == b'superseded_split':
952 957
953 958 succs = []
954 959 for node_id in successors[0]:
955 960 succs.append(nodemod.short(node_id))
956 961
957 962 if len(succs) <= 2:
958 963 fmtsuccs = b', '.join(succs)
959 964 return filteredmsgtable[b'superseded_split'] % (changeid, fmtsuccs)
960 965 else:
961 966 firstsuccessors = b', '.join(succs[:2])
962 967 remainingnumber = len(succs) - 2
963 968
964 969 args = (changeid, firstsuccessors, remainingnumber)
965 970 return filteredmsgtable[b'superseded_split_several'] % args
966 971
967 972
968 973 def divergentsets(repo, ctx):
969 974 """Compute sets of commits divergent with a given one"""
970 975 cache = {}
971 976 base = {}
972 977 for n in allpredecessors(repo.obsstore, [ctx.node()]):
973 978 if n == ctx.node():
974 979 # a node can't be a base for divergence with itself
975 980 continue
976 981 nsuccsets = successorssets(repo, n, cache)
977 982 for nsuccset in nsuccsets:
978 983 if ctx.node() in nsuccset:
979 984 # we are only interested in *other* successor sets
980 985 continue
981 986 if tuple(nsuccset) in base:
982 987 # we already know the latest base for this divergency
983 988 continue
984 989 base[tuple(nsuccset)] = n
985 990 return [
986 991 {b'divergentnodes': divset, b'commonpredecessor': b}
987 992 for divset, b in pycompat.iteritems(base)
988 993 ]
989 994
990 995
991 996 def whyunstable(repo, ctx):
992 997 result = []
993 998 if ctx.orphan():
994 999 for parent in ctx.parents():
995 1000 kind = None
996 1001 if parent.orphan():
997 1002 kind = b'orphan'
998 1003 elif parent.obsolete():
999 1004 kind = b'obsolete'
1000 1005 if kind is not None:
1001 1006 result.append(
1002 1007 {
1003 1008 b'instability': b'orphan',
1004 1009 b'reason': b'%s parent' % kind,
1005 1010 b'node': parent.hex(),
1006 1011 }
1007 1012 )
1008 1013 if ctx.phasedivergent():
1009 1014 predecessors = allpredecessors(
1010 1015 repo.obsstore, [ctx.node()], ignoreflags=bumpedfix
1011 1016 )
1012 1017 immutable = [
1013 1018 repo[p] for p in predecessors if p in repo and not repo[p].mutable()
1014 1019 ]
1015 1020 for predecessor in immutable:
1016 1021 result.append(
1017 1022 {
1018 1023 b'instability': b'phase-divergent',
1019 1024 b'reason': b'immutable predecessor',
1020 1025 b'node': predecessor.hex(),
1021 1026 }
1022 1027 )
1023 1028 if ctx.contentdivergent():
1024 1029 dsets = divergentsets(repo, ctx)
1025 1030 for dset in dsets:
1026 1031 divnodes = [repo[n] for n in dset[b'divergentnodes']]
1027 1032 result.append(
1028 1033 {
1029 1034 b'instability': b'content-divergent',
1030 1035 b'divergentnodes': divnodes,
1031 1036 b'reason': b'predecessor',
1032 1037 b'node': nodemod.hex(dset[b'commonpredecessor']),
1033 1038 }
1034 1039 )
1035 1040 return result
General Comments 0
You need to be logged in to leave comments. Login now