##// END OF EJS Templates
exchange: support declaring pull depth...
Gregory Szorc -
r40367:ac59de55 default
parent child Browse files
Show More
@@ -1,2650 +1,2657 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 bin,
16 16 hex,
17 17 nullid,
18 18 nullrev,
19 19 )
20 20 from .thirdparty import (
21 21 attr,
22 22 )
23 23 from . import (
24 24 bookmarks as bookmod,
25 25 bundle2,
26 26 changegroup,
27 27 discovery,
28 28 error,
29 29 exchangev2,
30 30 lock as lockmod,
31 31 logexchange,
32 32 narrowspec,
33 33 obsolete,
34 34 phases,
35 35 pushkey,
36 36 pycompat,
37 37 repository,
38 38 scmutil,
39 39 sslutil,
40 40 streamclone,
41 41 url as urlmod,
42 42 util,
43 43 )
44 44 from .utils import (
45 45 stringutil,
46 46 )
47 47
48 48 urlerr = util.urlerr
49 49 urlreq = util.urlreq
50 50
51 51 _NARROWACL_SECTION = 'narrowhgacl'
52 52
53 53 # Maps bundle version human names to changegroup versions.
54 54 _bundlespeccgversions = {'v1': '01',
55 55 'v2': '02',
56 56 'packed1': 's1',
57 57 'bundle2': '02', #legacy
58 58 }
59 59
60 60 # Maps bundle version with content opts to choose which part to bundle
61 61 _bundlespeccontentopts = {
62 62 'v1': {
63 63 'changegroup': True,
64 64 'cg.version': '01',
65 65 'obsolescence': False,
66 66 'phases': False,
67 67 'tagsfnodescache': False,
68 68 'revbranchcache': False
69 69 },
70 70 'v2': {
71 71 'changegroup': True,
72 72 'cg.version': '02',
73 73 'obsolescence': False,
74 74 'phases': False,
75 75 'tagsfnodescache': True,
76 76 'revbranchcache': True
77 77 },
78 78 'packed1' : {
79 79 'cg.version': 's1'
80 80 }
81 81 }
82 82 _bundlespeccontentopts['bundle2'] = _bundlespeccontentopts['v2']
83 83
84 84 _bundlespecvariants = {"streamv2": {"changegroup": False, "streamv2": True,
85 85 "tagsfnodescache": False,
86 86 "revbranchcache": False}}
87 87
88 88 # Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE.
89 89 _bundlespecv1compengines = {'gzip', 'bzip2', 'none'}
90 90
91 91 @attr.s
92 92 class bundlespec(object):
93 93 compression = attr.ib()
94 94 wirecompression = attr.ib()
95 95 version = attr.ib()
96 96 wireversion = attr.ib()
97 97 params = attr.ib()
98 98 contentopts = attr.ib()
99 99
100 100 def parsebundlespec(repo, spec, strict=True):
101 101 """Parse a bundle string specification into parts.
102 102
103 103 Bundle specifications denote a well-defined bundle/exchange format.
104 104 The content of a given specification should not change over time in
105 105 order to ensure that bundles produced by a newer version of Mercurial are
106 106 readable from an older version.
107 107
108 108 The string currently has the form:
109 109
110 110 <compression>-<type>[;<parameter0>[;<parameter1>]]
111 111
112 112 Where <compression> is one of the supported compression formats
113 113 and <type> is (currently) a version string. A ";" can follow the type and
114 114 all text afterwards is interpreted as URI encoded, ";" delimited key=value
115 115 pairs.
116 116
117 117 If ``strict`` is True (the default) <compression> is required. Otherwise,
118 118 it is optional.
119 119
120 120 Returns a bundlespec object of (compression, version, parameters).
121 121 Compression will be ``None`` if not in strict mode and a compression isn't
122 122 defined.
123 123
124 124 An ``InvalidBundleSpecification`` is raised when the specification is
125 125 not syntactically well formed.
126 126
127 127 An ``UnsupportedBundleSpecification`` is raised when the compression or
128 128 bundle type/version is not recognized.
129 129
130 130 Note: this function will likely eventually return a more complex data
131 131 structure, including bundle2 part information.
132 132 """
133 133 def parseparams(s):
134 134 if ';' not in s:
135 135 return s, {}
136 136
137 137 params = {}
138 138 version, paramstr = s.split(';', 1)
139 139
140 140 for p in paramstr.split(';'):
141 141 if '=' not in p:
142 142 raise error.InvalidBundleSpecification(
143 143 _('invalid bundle specification: '
144 144 'missing "=" in parameter: %s') % p)
145 145
146 146 key, value = p.split('=', 1)
147 147 key = urlreq.unquote(key)
148 148 value = urlreq.unquote(value)
149 149 params[key] = value
150 150
151 151 return version, params
152 152
153 153
154 154 if strict and '-' not in spec:
155 155 raise error.InvalidBundleSpecification(
156 156 _('invalid bundle specification; '
157 157 'must be prefixed with compression: %s') % spec)
158 158
159 159 if '-' in spec:
160 160 compression, version = spec.split('-', 1)
161 161
162 162 if compression not in util.compengines.supportedbundlenames:
163 163 raise error.UnsupportedBundleSpecification(
164 164 _('%s compression is not supported') % compression)
165 165
166 166 version, params = parseparams(version)
167 167
168 168 if version not in _bundlespeccgversions:
169 169 raise error.UnsupportedBundleSpecification(
170 170 _('%s is not a recognized bundle version') % version)
171 171 else:
172 172 # Value could be just the compression or just the version, in which
173 173 # case some defaults are assumed (but only when not in strict mode).
174 174 assert not strict
175 175
176 176 spec, params = parseparams(spec)
177 177
178 178 if spec in util.compengines.supportedbundlenames:
179 179 compression = spec
180 180 version = 'v1'
181 181 # Generaldelta repos require v2.
182 182 if 'generaldelta' in repo.requirements:
183 183 version = 'v2'
184 184 # Modern compression engines require v2.
185 185 if compression not in _bundlespecv1compengines:
186 186 version = 'v2'
187 187 elif spec in _bundlespeccgversions:
188 188 if spec == 'packed1':
189 189 compression = 'none'
190 190 else:
191 191 compression = 'bzip2'
192 192 version = spec
193 193 else:
194 194 raise error.UnsupportedBundleSpecification(
195 195 _('%s is not a recognized bundle specification') % spec)
196 196
197 197 # Bundle version 1 only supports a known set of compression engines.
198 198 if version == 'v1' and compression not in _bundlespecv1compengines:
199 199 raise error.UnsupportedBundleSpecification(
200 200 _('compression engine %s is not supported on v1 bundles') %
201 201 compression)
202 202
203 203 # The specification for packed1 can optionally declare the data formats
204 204 # required to apply it. If we see this metadata, compare against what the
205 205 # repo supports and error if the bundle isn't compatible.
206 206 if version == 'packed1' and 'requirements' in params:
207 207 requirements = set(params['requirements'].split(','))
208 208 missingreqs = requirements - repo.supportedformats
209 209 if missingreqs:
210 210 raise error.UnsupportedBundleSpecification(
211 211 _('missing support for repository features: %s') %
212 212 ', '.join(sorted(missingreqs)))
213 213
214 214 # Compute contentopts based on the version
215 215 contentopts = _bundlespeccontentopts.get(version, {}).copy()
216 216
217 217 # Process the variants
218 218 if "stream" in params and params["stream"] == "v2":
219 219 variant = _bundlespecvariants["streamv2"]
220 220 contentopts.update(variant)
221 221
222 222 engine = util.compengines.forbundlename(compression)
223 223 compression, wirecompression = engine.bundletype()
224 224 wireversion = _bundlespeccgversions[version]
225 225
226 226 return bundlespec(compression, wirecompression, version, wireversion,
227 227 params, contentopts)
228 228
229 229 def readbundle(ui, fh, fname, vfs=None):
230 230 header = changegroup.readexactly(fh, 4)
231 231
232 232 alg = None
233 233 if not fname:
234 234 fname = "stream"
235 235 if not header.startswith('HG') and header.startswith('\0'):
236 236 fh = changegroup.headerlessfixup(fh, header)
237 237 header = "HG10"
238 238 alg = 'UN'
239 239 elif vfs:
240 240 fname = vfs.join(fname)
241 241
242 242 magic, version = header[0:2], header[2:4]
243 243
244 244 if magic != 'HG':
245 245 raise error.Abort(_('%s: not a Mercurial bundle') % fname)
246 246 if version == '10':
247 247 if alg is None:
248 248 alg = changegroup.readexactly(fh, 2)
249 249 return changegroup.cg1unpacker(fh, alg)
250 250 elif version.startswith('2'):
251 251 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
252 252 elif version == 'S1':
253 253 return streamclone.streamcloneapplier(fh)
254 254 else:
255 255 raise error.Abort(_('%s: unknown bundle version %s') % (fname, version))
256 256
257 257 def getbundlespec(ui, fh):
258 258 """Infer the bundlespec from a bundle file handle.
259 259
260 260 The input file handle is seeked and the original seek position is not
261 261 restored.
262 262 """
263 263 def speccompression(alg):
264 264 try:
265 265 return util.compengines.forbundletype(alg).bundletype()[0]
266 266 except KeyError:
267 267 return None
268 268
269 269 b = readbundle(ui, fh, None)
270 270 if isinstance(b, changegroup.cg1unpacker):
271 271 alg = b._type
272 272 if alg == '_truncatedBZ':
273 273 alg = 'BZ'
274 274 comp = speccompression(alg)
275 275 if not comp:
276 276 raise error.Abort(_('unknown compression algorithm: %s') % alg)
277 277 return '%s-v1' % comp
278 278 elif isinstance(b, bundle2.unbundle20):
279 279 if 'Compression' in b.params:
280 280 comp = speccompression(b.params['Compression'])
281 281 if not comp:
282 282 raise error.Abort(_('unknown compression algorithm: %s') % comp)
283 283 else:
284 284 comp = 'none'
285 285
286 286 version = None
287 287 for part in b.iterparts():
288 288 if part.type == 'changegroup':
289 289 version = part.params['version']
290 290 if version in ('01', '02'):
291 291 version = 'v2'
292 292 else:
293 293 raise error.Abort(_('changegroup version %s does not have '
294 294 'a known bundlespec') % version,
295 295 hint=_('try upgrading your Mercurial '
296 296 'client'))
297 297 elif part.type == 'stream2' and version is None:
298 298 # A stream2 part requires to be part of a v2 bundle
299 299 version = "v2"
300 300 requirements = urlreq.unquote(part.params['requirements'])
301 301 splitted = requirements.split()
302 302 params = bundle2._formatrequirementsparams(splitted)
303 303 return 'none-v2;stream=v2;%s' % params
304 304
305 305 if not version:
306 306 raise error.Abort(_('could not identify changegroup version in '
307 307 'bundle'))
308 308
309 309 return '%s-%s' % (comp, version)
310 310 elif isinstance(b, streamclone.streamcloneapplier):
311 311 requirements = streamclone.readbundle1header(fh)[2]
312 312 formatted = bundle2._formatrequirementsparams(requirements)
313 313 return 'none-packed1;%s' % formatted
314 314 else:
315 315 raise error.Abort(_('unknown bundle type: %s') % b)
316 316
317 317 def _computeoutgoing(repo, heads, common):
318 318 """Computes which revs are outgoing given a set of common
319 319 and a set of heads.
320 320
321 321 This is a separate function so extensions can have access to
322 322 the logic.
323 323
324 324 Returns a discovery.outgoing object.
325 325 """
326 326 cl = repo.changelog
327 327 if common:
328 328 hasnode = cl.hasnode
329 329 common = [n for n in common if hasnode(n)]
330 330 else:
331 331 common = [nullid]
332 332 if not heads:
333 333 heads = cl.heads()
334 334 return discovery.outgoing(repo, common, heads)
335 335
336 336 def _forcebundle1(op):
337 337 """return true if a pull/push must use bundle1
338 338
339 339 This function is used to allow testing of the older bundle version"""
340 340 ui = op.repo.ui
341 341 # The goal is this config is to allow developer to choose the bundle
342 342 # version used during exchanged. This is especially handy during test.
343 343 # Value is a list of bundle version to be picked from, highest version
344 344 # should be used.
345 345 #
346 346 # developer config: devel.legacy.exchange
347 347 exchange = ui.configlist('devel', 'legacy.exchange')
348 348 forcebundle1 = 'bundle2' not in exchange and 'bundle1' in exchange
349 349 return forcebundle1 or not op.remote.capable('bundle2')
350 350
351 351 class pushoperation(object):
352 352 """A object that represent a single push operation
353 353
354 354 Its purpose is to carry push related state and very common operations.
355 355
356 356 A new pushoperation should be created at the beginning of each push and
357 357 discarded afterward.
358 358 """
359 359
360 360 def __init__(self, repo, remote, force=False, revs=None, newbranch=False,
361 361 bookmarks=(), pushvars=None):
362 362 # repo we push from
363 363 self.repo = repo
364 364 self.ui = repo.ui
365 365 # repo we push to
366 366 self.remote = remote
367 367 # force option provided
368 368 self.force = force
369 369 # revs to be pushed (None is "all")
370 370 self.revs = revs
371 371 # bookmark explicitly pushed
372 372 self.bookmarks = bookmarks
373 373 # allow push of new branch
374 374 self.newbranch = newbranch
375 375 # step already performed
376 376 # (used to check what steps have been already performed through bundle2)
377 377 self.stepsdone = set()
378 378 # Integer version of the changegroup push result
379 379 # - None means nothing to push
380 380 # - 0 means HTTP error
381 381 # - 1 means we pushed and remote head count is unchanged *or*
382 382 # we have outgoing changesets but refused to push
383 383 # - other values as described by addchangegroup()
384 384 self.cgresult = None
385 385 # Boolean value for the bookmark push
386 386 self.bkresult = None
387 387 # discover.outgoing object (contains common and outgoing data)
388 388 self.outgoing = None
389 389 # all remote topological heads before the push
390 390 self.remoteheads = None
391 391 # Details of the remote branch pre and post push
392 392 #
393 393 # mapping: {'branch': ([remoteheads],
394 394 # [newheads],
395 395 # [unsyncedheads],
396 396 # [discardedheads])}
397 397 # - branch: the branch name
398 398 # - remoteheads: the list of remote heads known locally
399 399 # None if the branch is new
400 400 # - newheads: the new remote heads (known locally) with outgoing pushed
401 401 # - unsyncedheads: the list of remote heads unknown locally.
402 402 # - discardedheads: the list of remote heads made obsolete by the push
403 403 self.pushbranchmap = None
404 404 # testable as a boolean indicating if any nodes are missing locally.
405 405 self.incoming = None
406 406 # summary of the remote phase situation
407 407 self.remotephases = None
408 408 # phases changes that must be pushed along side the changesets
409 409 self.outdatedphases = None
410 410 # phases changes that must be pushed if changeset push fails
411 411 self.fallbackoutdatedphases = None
412 412 # outgoing obsmarkers
413 413 self.outobsmarkers = set()
414 414 # outgoing bookmarks
415 415 self.outbookmarks = []
416 416 # transaction manager
417 417 self.trmanager = None
418 418 # map { pushkey partid -> callback handling failure}
419 419 # used to handle exception from mandatory pushkey part failure
420 420 self.pkfailcb = {}
421 421 # an iterable of pushvars or None
422 422 self.pushvars = pushvars
423 423
424 424 @util.propertycache
425 425 def futureheads(self):
426 426 """future remote heads if the changeset push succeeds"""
427 427 return self.outgoing.missingheads
428 428
429 429 @util.propertycache
430 430 def fallbackheads(self):
431 431 """future remote heads if the changeset push fails"""
432 432 if self.revs is None:
433 433 # not target to push, all common are relevant
434 434 return self.outgoing.commonheads
435 435 unfi = self.repo.unfiltered()
436 436 # I want cheads = heads(::missingheads and ::commonheads)
437 437 # (missingheads is revs with secret changeset filtered out)
438 438 #
439 439 # This can be expressed as:
440 440 # cheads = ( (missingheads and ::commonheads)
441 441 # + (commonheads and ::missingheads))"
442 442 # )
443 443 #
444 444 # while trying to push we already computed the following:
445 445 # common = (::commonheads)
446 446 # missing = ((commonheads::missingheads) - commonheads)
447 447 #
448 448 # We can pick:
449 449 # * missingheads part of common (::commonheads)
450 450 common = self.outgoing.common
451 451 nm = self.repo.changelog.nodemap
452 452 cheads = [node for node in self.revs if nm[node] in common]
453 453 # and
454 454 # * commonheads parents on missing
455 455 revset = unfi.set('%ln and parents(roots(%ln))',
456 456 self.outgoing.commonheads,
457 457 self.outgoing.missing)
458 458 cheads.extend(c.node() for c in revset)
459 459 return cheads
460 460
461 461 @property
462 462 def commonheads(self):
463 463 """set of all common heads after changeset bundle push"""
464 464 if self.cgresult:
465 465 return self.futureheads
466 466 else:
467 467 return self.fallbackheads
468 468
469 469 # mapping of message used when pushing bookmark
470 470 bookmsgmap = {'update': (_("updating bookmark %s\n"),
471 471 _('updating bookmark %s failed!\n')),
472 472 'export': (_("exporting bookmark %s\n"),
473 473 _('exporting bookmark %s failed!\n')),
474 474 'delete': (_("deleting remote bookmark %s\n"),
475 475 _('deleting remote bookmark %s failed!\n')),
476 476 }
477 477
478 478
479 479 def push(repo, remote, force=False, revs=None, newbranch=False, bookmarks=(),
480 480 opargs=None):
481 481 '''Push outgoing changesets (limited by revs) from a local
482 482 repository to remote. Return an integer:
483 483 - None means nothing to push
484 484 - 0 means HTTP error
485 485 - 1 means we pushed and remote head count is unchanged *or*
486 486 we have outgoing changesets but refused to push
487 487 - other values as described by addchangegroup()
488 488 '''
489 489 if opargs is None:
490 490 opargs = {}
491 491 pushop = pushoperation(repo, remote, force, revs, newbranch, bookmarks,
492 492 **pycompat.strkwargs(opargs))
493 493 if pushop.remote.local():
494 494 missing = (set(pushop.repo.requirements)
495 495 - pushop.remote.local().supported)
496 496 if missing:
497 497 msg = _("required features are not"
498 498 " supported in the destination:"
499 499 " %s") % (', '.join(sorted(missing)))
500 500 raise error.Abort(msg)
501 501
502 502 if not pushop.remote.canpush():
503 503 raise error.Abort(_("destination does not support push"))
504 504
505 505 if not pushop.remote.capable('unbundle'):
506 506 raise error.Abort(_('cannot push: destination does not support the '
507 507 'unbundle wire protocol command'))
508 508
509 509 # get lock as we might write phase data
510 510 wlock = lock = None
511 511 try:
512 512 # bundle2 push may receive a reply bundle touching bookmarks or other
513 513 # things requiring the wlock. Take it now to ensure proper ordering.
514 514 maypushback = pushop.ui.configbool('experimental', 'bundle2.pushback')
515 515 if (not _forcebundle1(pushop)) and maypushback:
516 516 wlock = pushop.repo.wlock()
517 517 lock = pushop.repo.lock()
518 518 pushop.trmanager = transactionmanager(pushop.repo,
519 519 'push-response',
520 520 pushop.remote.url())
521 521 except error.LockUnavailable as err:
522 522 # source repo cannot be locked.
523 523 # We do not abort the push, but just disable the local phase
524 524 # synchronisation.
525 525 msg = ('cannot lock source repository: %s\n'
526 526 % stringutil.forcebytestr(err))
527 527 pushop.ui.debug(msg)
528 528
529 529 with wlock or util.nullcontextmanager(), \
530 530 lock or util.nullcontextmanager(), \
531 531 pushop.trmanager or util.nullcontextmanager():
532 532 pushop.repo.checkpush(pushop)
533 533 _pushdiscovery(pushop)
534 534 if not _forcebundle1(pushop):
535 535 _pushbundle2(pushop)
536 536 _pushchangeset(pushop)
537 537 _pushsyncphase(pushop)
538 538 _pushobsolete(pushop)
539 539 _pushbookmark(pushop)
540 540
541 541 if repo.ui.configbool('experimental', 'remotenames'):
542 542 logexchange.pullremotenames(repo, remote)
543 543
544 544 return pushop
545 545
546 546 # list of steps to perform discovery before push
547 547 pushdiscoveryorder = []
548 548
549 549 # Mapping between step name and function
550 550 #
551 551 # This exists to help extensions wrap steps if necessary
552 552 pushdiscoverymapping = {}
553 553
554 554 def pushdiscovery(stepname):
555 555 """decorator for function performing discovery before push
556 556
557 557 The function is added to the step -> function mapping and appended to the
558 558 list of steps. Beware that decorated function will be added in order (this
559 559 may matter).
560 560
561 561 You can only use this decorator for a new step, if you want to wrap a step
562 562 from an extension, change the pushdiscovery dictionary directly."""
563 563 def dec(func):
564 564 assert stepname not in pushdiscoverymapping
565 565 pushdiscoverymapping[stepname] = func
566 566 pushdiscoveryorder.append(stepname)
567 567 return func
568 568 return dec
569 569
570 570 def _pushdiscovery(pushop):
571 571 """Run all discovery steps"""
572 572 for stepname in pushdiscoveryorder:
573 573 step = pushdiscoverymapping[stepname]
574 574 step(pushop)
575 575
576 576 @pushdiscovery('changeset')
577 577 def _pushdiscoverychangeset(pushop):
578 578 """discover the changeset that need to be pushed"""
579 579 fci = discovery.findcommonincoming
580 580 if pushop.revs:
581 581 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force,
582 582 ancestorsof=pushop.revs)
583 583 else:
584 584 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
585 585 common, inc, remoteheads = commoninc
586 586 fco = discovery.findcommonoutgoing
587 587 outgoing = fco(pushop.repo, pushop.remote, onlyheads=pushop.revs,
588 588 commoninc=commoninc, force=pushop.force)
589 589 pushop.outgoing = outgoing
590 590 pushop.remoteheads = remoteheads
591 591 pushop.incoming = inc
592 592
593 593 @pushdiscovery('phase')
594 594 def _pushdiscoveryphase(pushop):
595 595 """discover the phase that needs to be pushed
596 596
597 597 (computed for both success and failure case for changesets push)"""
598 598 outgoing = pushop.outgoing
599 599 unfi = pushop.repo.unfiltered()
600 600 remotephases = listkeys(pushop.remote, 'phases')
601 601
602 602 if (pushop.ui.configbool('ui', '_usedassubrepo')
603 603 and remotephases # server supports phases
604 604 and not pushop.outgoing.missing # no changesets to be pushed
605 605 and remotephases.get('publishing', False)):
606 606 # When:
607 607 # - this is a subrepo push
608 608 # - and remote support phase
609 609 # - and no changeset are to be pushed
610 610 # - and remote is publishing
611 611 # We may be in issue 3781 case!
612 612 # We drop the possible phase synchronisation done by
613 613 # courtesy to publish changesets possibly locally draft
614 614 # on the remote.
615 615 pushop.outdatedphases = []
616 616 pushop.fallbackoutdatedphases = []
617 617 return
618 618
619 619 pushop.remotephases = phases.remotephasessummary(pushop.repo,
620 620 pushop.fallbackheads,
621 621 remotephases)
622 622 droots = pushop.remotephases.draftroots
623 623
624 624 extracond = ''
625 625 if not pushop.remotephases.publishing:
626 626 extracond = ' and public()'
627 627 revset = 'heads((%%ln::%%ln) %s)' % extracond
628 628 # Get the list of all revs draft on remote by public here.
629 629 # XXX Beware that revset break if droots is not strictly
630 630 # XXX root we may want to ensure it is but it is costly
631 631 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
632 632 if not outgoing.missing:
633 633 future = fallback
634 634 else:
635 635 # adds changeset we are going to push as draft
636 636 #
637 637 # should not be necessary for publishing server, but because of an
638 638 # issue fixed in xxxxx we have to do it anyway.
639 639 fdroots = list(unfi.set('roots(%ln + %ln::)',
640 640 outgoing.missing, droots))
641 641 fdroots = [f.node() for f in fdroots]
642 642 future = list(unfi.set(revset, fdroots, pushop.futureheads))
643 643 pushop.outdatedphases = future
644 644 pushop.fallbackoutdatedphases = fallback
645 645
646 646 @pushdiscovery('obsmarker')
647 647 def _pushdiscoveryobsmarkers(pushop):
648 648 if not obsolete.isenabled(pushop.repo, obsolete.exchangeopt):
649 649 return
650 650
651 651 if not pushop.repo.obsstore:
652 652 return
653 653
654 654 if 'obsolete' not in listkeys(pushop.remote, 'namespaces'):
655 655 return
656 656
657 657 repo = pushop.repo
658 658 # very naive computation, that can be quite expensive on big repo.
659 659 # However: evolution is currently slow on them anyway.
660 660 nodes = (c.node() for c in repo.set('::%ln', pushop.futureheads))
661 661 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
662 662
663 663 @pushdiscovery('bookmarks')
664 664 def _pushdiscoverybookmarks(pushop):
665 665 ui = pushop.ui
666 666 repo = pushop.repo.unfiltered()
667 667 remote = pushop.remote
668 668 ui.debug("checking for updated bookmarks\n")
669 669 ancestors = ()
670 670 if pushop.revs:
671 671 revnums = pycompat.maplist(repo.changelog.rev, pushop.revs)
672 672 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
673 673
674 674 remotebookmark = listkeys(remote, 'bookmarks')
675 675
676 676 explicit = set([repo._bookmarks.expandname(bookmark)
677 677 for bookmark in pushop.bookmarks])
678 678
679 679 remotebookmark = bookmod.unhexlifybookmarks(remotebookmark)
680 680 comp = bookmod.comparebookmarks(repo, repo._bookmarks, remotebookmark)
681 681
682 682 def safehex(x):
683 683 if x is None:
684 684 return x
685 685 return hex(x)
686 686
687 687 def hexifycompbookmarks(bookmarks):
688 688 return [(b, safehex(scid), safehex(dcid))
689 689 for (b, scid, dcid) in bookmarks]
690 690
691 691 comp = [hexifycompbookmarks(marks) for marks in comp]
692 692 return _processcompared(pushop, ancestors, explicit, remotebookmark, comp)
693 693
694 694 def _processcompared(pushop, pushed, explicit, remotebms, comp):
695 695 """take decision on bookmark to pull from the remote bookmark
696 696
697 697 Exist to help extensions who want to alter this behavior.
698 698 """
699 699 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
700 700
701 701 repo = pushop.repo
702 702
703 703 for b, scid, dcid in advsrc:
704 704 if b in explicit:
705 705 explicit.remove(b)
706 706 if not pushed or repo[scid].rev() in pushed:
707 707 pushop.outbookmarks.append((b, dcid, scid))
708 708 # search added bookmark
709 709 for b, scid, dcid in addsrc:
710 710 if b in explicit:
711 711 explicit.remove(b)
712 712 pushop.outbookmarks.append((b, '', scid))
713 713 # search for overwritten bookmark
714 714 for b, scid, dcid in list(advdst) + list(diverge) + list(differ):
715 715 if b in explicit:
716 716 explicit.remove(b)
717 717 pushop.outbookmarks.append((b, dcid, scid))
718 718 # search for bookmark to delete
719 719 for b, scid, dcid in adddst:
720 720 if b in explicit:
721 721 explicit.remove(b)
722 722 # treat as "deleted locally"
723 723 pushop.outbookmarks.append((b, dcid, ''))
724 724 # identical bookmarks shouldn't get reported
725 725 for b, scid, dcid in same:
726 726 if b in explicit:
727 727 explicit.remove(b)
728 728
729 729 if explicit:
730 730 explicit = sorted(explicit)
731 731 # we should probably list all of them
732 732 pushop.ui.warn(_('bookmark %s does not exist on the local '
733 733 'or remote repository!\n') % explicit[0])
734 734 pushop.bkresult = 2
735 735
736 736 pushop.outbookmarks.sort()
737 737
738 738 def _pushcheckoutgoing(pushop):
739 739 outgoing = pushop.outgoing
740 740 unfi = pushop.repo.unfiltered()
741 741 if not outgoing.missing:
742 742 # nothing to push
743 743 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
744 744 return False
745 745 # something to push
746 746 if not pushop.force:
747 747 # if repo.obsstore == False --> no obsolete
748 748 # then, save the iteration
749 749 if unfi.obsstore:
750 750 # this message are here for 80 char limit reason
751 751 mso = _("push includes obsolete changeset: %s!")
752 752 mspd = _("push includes phase-divergent changeset: %s!")
753 753 mscd = _("push includes content-divergent changeset: %s!")
754 754 mst = {"orphan": _("push includes orphan changeset: %s!"),
755 755 "phase-divergent": mspd,
756 756 "content-divergent": mscd}
757 757 # If we are to push if there is at least one
758 758 # obsolete or unstable changeset in missing, at
759 759 # least one of the missinghead will be obsolete or
760 760 # unstable. So checking heads only is ok
761 761 for node in outgoing.missingheads:
762 762 ctx = unfi[node]
763 763 if ctx.obsolete():
764 764 raise error.Abort(mso % ctx)
765 765 elif ctx.isunstable():
766 766 # TODO print more than one instability in the abort
767 767 # message
768 768 raise error.Abort(mst[ctx.instabilities()[0]] % ctx)
769 769
770 770 discovery.checkheads(pushop)
771 771 return True
772 772
773 773 # List of names of steps to perform for an outgoing bundle2, order matters.
774 774 b2partsgenorder = []
775 775
776 776 # Mapping between step name and function
777 777 #
778 778 # This exists to help extensions wrap steps if necessary
779 779 b2partsgenmapping = {}
780 780
781 781 def b2partsgenerator(stepname, idx=None):
782 782 """decorator for function generating bundle2 part
783 783
784 784 The function is added to the step -> function mapping and appended to the
785 785 list of steps. Beware that decorated functions will be added in order
786 786 (this may matter).
787 787
788 788 You can only use this decorator for new steps, if you want to wrap a step
789 789 from an extension, attack the b2partsgenmapping dictionary directly."""
790 790 def dec(func):
791 791 assert stepname not in b2partsgenmapping
792 792 b2partsgenmapping[stepname] = func
793 793 if idx is None:
794 794 b2partsgenorder.append(stepname)
795 795 else:
796 796 b2partsgenorder.insert(idx, stepname)
797 797 return func
798 798 return dec
799 799
800 800 def _pushb2ctxcheckheads(pushop, bundler):
801 801 """Generate race condition checking parts
802 802
803 803 Exists as an independent function to aid extensions
804 804 """
805 805 # * 'force' do not check for push race,
806 806 # * if we don't push anything, there are nothing to check.
807 807 if not pushop.force and pushop.outgoing.missingheads:
808 808 allowunrelated = 'related' in bundler.capabilities.get('checkheads', ())
809 809 emptyremote = pushop.pushbranchmap is None
810 810 if not allowunrelated or emptyremote:
811 811 bundler.newpart('check:heads', data=iter(pushop.remoteheads))
812 812 else:
813 813 affected = set()
814 814 for branch, heads in pushop.pushbranchmap.iteritems():
815 815 remoteheads, newheads, unsyncedheads, discardedheads = heads
816 816 if remoteheads is not None:
817 817 remote = set(remoteheads)
818 818 affected |= set(discardedheads) & remote
819 819 affected |= remote - set(newheads)
820 820 if affected:
821 821 data = iter(sorted(affected))
822 822 bundler.newpart('check:updated-heads', data=data)
823 823
824 824 def _pushing(pushop):
825 825 """return True if we are pushing anything"""
826 826 return bool(pushop.outgoing.missing
827 827 or pushop.outdatedphases
828 828 or pushop.outobsmarkers
829 829 or pushop.outbookmarks)
830 830
831 831 @b2partsgenerator('check-bookmarks')
832 832 def _pushb2checkbookmarks(pushop, bundler):
833 833 """insert bookmark move checking"""
834 834 if not _pushing(pushop) or pushop.force:
835 835 return
836 836 b2caps = bundle2.bundle2caps(pushop.remote)
837 837 hasbookmarkcheck = 'bookmarks' in b2caps
838 838 if not (pushop.outbookmarks and hasbookmarkcheck):
839 839 return
840 840 data = []
841 841 for book, old, new in pushop.outbookmarks:
842 842 old = bin(old)
843 843 data.append((book, old))
844 844 checkdata = bookmod.binaryencode(data)
845 845 bundler.newpart('check:bookmarks', data=checkdata)
846 846
847 847 @b2partsgenerator('check-phases')
848 848 def _pushb2checkphases(pushop, bundler):
849 849 """insert phase move checking"""
850 850 if not _pushing(pushop) or pushop.force:
851 851 return
852 852 b2caps = bundle2.bundle2caps(pushop.remote)
853 853 hasphaseheads = 'heads' in b2caps.get('phases', ())
854 854 if pushop.remotephases is not None and hasphaseheads:
855 855 # check that the remote phase has not changed
856 856 checks = [[] for p in phases.allphases]
857 857 checks[phases.public].extend(pushop.remotephases.publicheads)
858 858 checks[phases.draft].extend(pushop.remotephases.draftroots)
859 859 if any(checks):
860 860 for nodes in checks:
861 861 nodes.sort()
862 862 checkdata = phases.binaryencode(checks)
863 863 bundler.newpart('check:phases', data=checkdata)
864 864
865 865 @b2partsgenerator('changeset')
866 866 def _pushb2ctx(pushop, bundler):
867 867 """handle changegroup push through bundle2
868 868
869 869 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
870 870 """
871 871 if 'changesets' in pushop.stepsdone:
872 872 return
873 873 pushop.stepsdone.add('changesets')
874 874 # Send known heads to the server for race detection.
875 875 if not _pushcheckoutgoing(pushop):
876 876 return
877 877 pushop.repo.prepushoutgoinghooks(pushop)
878 878
879 879 _pushb2ctxcheckheads(pushop, bundler)
880 880
881 881 b2caps = bundle2.bundle2caps(pushop.remote)
882 882 version = '01'
883 883 cgversions = b2caps.get('changegroup')
884 884 if cgversions: # 3.1 and 3.2 ship with an empty value
885 885 cgversions = [v for v in cgversions
886 886 if v in changegroup.supportedoutgoingversions(
887 887 pushop.repo)]
888 888 if not cgversions:
889 889 raise ValueError(_('no common changegroup version'))
890 890 version = max(cgversions)
891 891 cgstream = changegroup.makestream(pushop.repo, pushop.outgoing, version,
892 892 'push')
893 893 cgpart = bundler.newpart('changegroup', data=cgstream)
894 894 if cgversions:
895 895 cgpart.addparam('version', version)
896 896 if 'treemanifest' in pushop.repo.requirements:
897 897 cgpart.addparam('treemanifest', '1')
898 898 def handlereply(op):
899 899 """extract addchangegroup returns from server reply"""
900 900 cgreplies = op.records.getreplies(cgpart.id)
901 901 assert len(cgreplies['changegroup']) == 1
902 902 pushop.cgresult = cgreplies['changegroup'][0]['return']
903 903 return handlereply
904 904
905 905 @b2partsgenerator('phase')
906 906 def _pushb2phases(pushop, bundler):
907 907 """handle phase push through bundle2"""
908 908 if 'phases' in pushop.stepsdone:
909 909 return
910 910 b2caps = bundle2.bundle2caps(pushop.remote)
911 911 ui = pushop.repo.ui
912 912
913 913 legacyphase = 'phases' in ui.configlist('devel', 'legacy.exchange')
914 914 haspushkey = 'pushkey' in b2caps
915 915 hasphaseheads = 'heads' in b2caps.get('phases', ())
916 916
917 917 if hasphaseheads and not legacyphase:
918 918 return _pushb2phaseheads(pushop, bundler)
919 919 elif haspushkey:
920 920 return _pushb2phasespushkey(pushop, bundler)
921 921
922 922 def _pushb2phaseheads(pushop, bundler):
923 923 """push phase information through a bundle2 - binary part"""
924 924 pushop.stepsdone.add('phases')
925 925 if pushop.outdatedphases:
926 926 updates = [[] for p in phases.allphases]
927 927 updates[0].extend(h.node() for h in pushop.outdatedphases)
928 928 phasedata = phases.binaryencode(updates)
929 929 bundler.newpart('phase-heads', data=phasedata)
930 930
931 931 def _pushb2phasespushkey(pushop, bundler):
932 932 """push phase information through a bundle2 - pushkey part"""
933 933 pushop.stepsdone.add('phases')
934 934 part2node = []
935 935
936 936 def handlefailure(pushop, exc):
937 937 targetid = int(exc.partid)
938 938 for partid, node in part2node:
939 939 if partid == targetid:
940 940 raise error.Abort(_('updating %s to public failed') % node)
941 941
942 942 enc = pushkey.encode
943 943 for newremotehead in pushop.outdatedphases:
944 944 part = bundler.newpart('pushkey')
945 945 part.addparam('namespace', enc('phases'))
946 946 part.addparam('key', enc(newremotehead.hex()))
947 947 part.addparam('old', enc('%d' % phases.draft))
948 948 part.addparam('new', enc('%d' % phases.public))
949 949 part2node.append((part.id, newremotehead))
950 950 pushop.pkfailcb[part.id] = handlefailure
951 951
952 952 def handlereply(op):
953 953 for partid, node in part2node:
954 954 partrep = op.records.getreplies(partid)
955 955 results = partrep['pushkey']
956 956 assert len(results) <= 1
957 957 msg = None
958 958 if not results:
959 959 msg = _('server ignored update of %s to public!\n') % node
960 960 elif not int(results[0]['return']):
961 961 msg = _('updating %s to public failed!\n') % node
962 962 if msg is not None:
963 963 pushop.ui.warn(msg)
964 964 return handlereply
965 965
966 966 @b2partsgenerator('obsmarkers')
967 967 def _pushb2obsmarkers(pushop, bundler):
968 968 if 'obsmarkers' in pushop.stepsdone:
969 969 return
970 970 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
971 971 if obsolete.commonversion(remoteversions) is None:
972 972 return
973 973 pushop.stepsdone.add('obsmarkers')
974 974 if pushop.outobsmarkers:
975 975 markers = sorted(pushop.outobsmarkers)
976 976 bundle2.buildobsmarkerspart(bundler, markers)
977 977
978 978 @b2partsgenerator('bookmarks')
979 979 def _pushb2bookmarks(pushop, bundler):
980 980 """handle bookmark push through bundle2"""
981 981 if 'bookmarks' in pushop.stepsdone:
982 982 return
983 983 b2caps = bundle2.bundle2caps(pushop.remote)
984 984
985 985 legacy = pushop.repo.ui.configlist('devel', 'legacy.exchange')
986 986 legacybooks = 'bookmarks' in legacy
987 987
988 988 if not legacybooks and 'bookmarks' in b2caps:
989 989 return _pushb2bookmarkspart(pushop, bundler)
990 990 elif 'pushkey' in b2caps:
991 991 return _pushb2bookmarkspushkey(pushop, bundler)
992 992
993 993 def _bmaction(old, new):
994 994 """small utility for bookmark pushing"""
995 995 if not old:
996 996 return 'export'
997 997 elif not new:
998 998 return 'delete'
999 999 return 'update'
1000 1000
1001 1001 def _pushb2bookmarkspart(pushop, bundler):
1002 1002 pushop.stepsdone.add('bookmarks')
1003 1003 if not pushop.outbookmarks:
1004 1004 return
1005 1005
1006 1006 allactions = []
1007 1007 data = []
1008 1008 for book, old, new in pushop.outbookmarks:
1009 1009 new = bin(new)
1010 1010 data.append((book, new))
1011 1011 allactions.append((book, _bmaction(old, new)))
1012 1012 checkdata = bookmod.binaryencode(data)
1013 1013 bundler.newpart('bookmarks', data=checkdata)
1014 1014
1015 1015 def handlereply(op):
1016 1016 ui = pushop.ui
1017 1017 # if success
1018 1018 for book, action in allactions:
1019 1019 ui.status(bookmsgmap[action][0] % book)
1020 1020
1021 1021 return handlereply
1022 1022
1023 1023 def _pushb2bookmarkspushkey(pushop, bundler):
1024 1024 pushop.stepsdone.add('bookmarks')
1025 1025 part2book = []
1026 1026 enc = pushkey.encode
1027 1027
1028 1028 def handlefailure(pushop, exc):
1029 1029 targetid = int(exc.partid)
1030 1030 for partid, book, action in part2book:
1031 1031 if partid == targetid:
1032 1032 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
1033 1033 # we should not be called for part we did not generated
1034 1034 assert False
1035 1035
1036 1036 for book, old, new in pushop.outbookmarks:
1037 1037 part = bundler.newpart('pushkey')
1038 1038 part.addparam('namespace', enc('bookmarks'))
1039 1039 part.addparam('key', enc(book))
1040 1040 part.addparam('old', enc(old))
1041 1041 part.addparam('new', enc(new))
1042 1042 action = 'update'
1043 1043 if not old:
1044 1044 action = 'export'
1045 1045 elif not new:
1046 1046 action = 'delete'
1047 1047 part2book.append((part.id, book, action))
1048 1048 pushop.pkfailcb[part.id] = handlefailure
1049 1049
1050 1050 def handlereply(op):
1051 1051 ui = pushop.ui
1052 1052 for partid, book, action in part2book:
1053 1053 partrep = op.records.getreplies(partid)
1054 1054 results = partrep['pushkey']
1055 1055 assert len(results) <= 1
1056 1056 if not results:
1057 1057 pushop.ui.warn(_('server ignored bookmark %s update\n') % book)
1058 1058 else:
1059 1059 ret = int(results[0]['return'])
1060 1060 if ret:
1061 1061 ui.status(bookmsgmap[action][0] % book)
1062 1062 else:
1063 1063 ui.warn(bookmsgmap[action][1] % book)
1064 1064 if pushop.bkresult is not None:
1065 1065 pushop.bkresult = 1
1066 1066 return handlereply
1067 1067
1068 1068 @b2partsgenerator('pushvars', idx=0)
1069 1069 def _getbundlesendvars(pushop, bundler):
1070 1070 '''send shellvars via bundle2'''
1071 1071 pushvars = pushop.pushvars
1072 1072 if pushvars:
1073 1073 shellvars = {}
1074 1074 for raw in pushvars:
1075 1075 if '=' not in raw:
1076 1076 msg = ("unable to parse variable '%s', should follow "
1077 1077 "'KEY=VALUE' or 'KEY=' format")
1078 1078 raise error.Abort(msg % raw)
1079 1079 k, v = raw.split('=', 1)
1080 1080 shellvars[k] = v
1081 1081
1082 1082 part = bundler.newpart('pushvars')
1083 1083
1084 1084 for key, value in shellvars.iteritems():
1085 1085 part.addparam(key, value, mandatory=False)
1086 1086
1087 1087 def _pushbundle2(pushop):
1088 1088 """push data to the remote using bundle2
1089 1089
1090 1090 The only currently supported type of data is changegroup but this will
1091 1091 evolve in the future."""
1092 1092 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
1093 1093 pushback = (pushop.trmanager
1094 1094 and pushop.ui.configbool('experimental', 'bundle2.pushback'))
1095 1095
1096 1096 # create reply capability
1097 1097 capsblob = bundle2.encodecaps(bundle2.getrepocaps(pushop.repo,
1098 1098 allowpushback=pushback,
1099 1099 role='client'))
1100 1100 bundler.newpart('replycaps', data=capsblob)
1101 1101 replyhandlers = []
1102 1102 for partgenname in b2partsgenorder:
1103 1103 partgen = b2partsgenmapping[partgenname]
1104 1104 ret = partgen(pushop, bundler)
1105 1105 if callable(ret):
1106 1106 replyhandlers.append(ret)
1107 1107 # do not push if nothing to push
1108 1108 if bundler.nbparts <= 1:
1109 1109 return
1110 1110 stream = util.chunkbuffer(bundler.getchunks())
1111 1111 try:
1112 1112 try:
1113 1113 with pushop.remote.commandexecutor() as e:
1114 1114 reply = e.callcommand('unbundle', {
1115 1115 'bundle': stream,
1116 1116 'heads': ['force'],
1117 1117 'url': pushop.remote.url(),
1118 1118 }).result()
1119 1119 except error.BundleValueError as exc:
1120 1120 raise error.Abort(_('missing support for %s') % exc)
1121 1121 try:
1122 1122 trgetter = None
1123 1123 if pushback:
1124 1124 trgetter = pushop.trmanager.transaction
1125 1125 op = bundle2.processbundle(pushop.repo, reply, trgetter)
1126 1126 except error.BundleValueError as exc:
1127 1127 raise error.Abort(_('missing support for %s') % exc)
1128 1128 except bundle2.AbortFromPart as exc:
1129 1129 pushop.ui.status(_('remote: %s\n') % exc)
1130 1130 if exc.hint is not None:
1131 1131 pushop.ui.status(_('remote: %s\n') % ('(%s)' % exc.hint))
1132 1132 raise error.Abort(_('push failed on remote'))
1133 1133 except error.PushkeyFailed as exc:
1134 1134 partid = int(exc.partid)
1135 1135 if partid not in pushop.pkfailcb:
1136 1136 raise
1137 1137 pushop.pkfailcb[partid](pushop, exc)
1138 1138 for rephand in replyhandlers:
1139 1139 rephand(op)
1140 1140
1141 1141 def _pushchangeset(pushop):
1142 1142 """Make the actual push of changeset bundle to remote repo"""
1143 1143 if 'changesets' in pushop.stepsdone:
1144 1144 return
1145 1145 pushop.stepsdone.add('changesets')
1146 1146 if not _pushcheckoutgoing(pushop):
1147 1147 return
1148 1148
1149 1149 # Should have verified this in push().
1150 1150 assert pushop.remote.capable('unbundle')
1151 1151
1152 1152 pushop.repo.prepushoutgoinghooks(pushop)
1153 1153 outgoing = pushop.outgoing
1154 1154 # TODO: get bundlecaps from remote
1155 1155 bundlecaps = None
1156 1156 # create a changegroup from local
1157 1157 if pushop.revs is None and not (outgoing.excluded
1158 1158 or pushop.repo.changelog.filteredrevs):
1159 1159 # push everything,
1160 1160 # use the fast path, no race possible on push
1161 1161 cg = changegroup.makechangegroup(pushop.repo, outgoing, '01', 'push',
1162 1162 fastpath=True, bundlecaps=bundlecaps)
1163 1163 else:
1164 1164 cg = changegroup.makechangegroup(pushop.repo, outgoing, '01',
1165 1165 'push', bundlecaps=bundlecaps)
1166 1166
1167 1167 # apply changegroup to remote
1168 1168 # local repo finds heads on server, finds out what
1169 1169 # revs it must push. once revs transferred, if server
1170 1170 # finds it has different heads (someone else won
1171 1171 # commit/push race), server aborts.
1172 1172 if pushop.force:
1173 1173 remoteheads = ['force']
1174 1174 else:
1175 1175 remoteheads = pushop.remoteheads
1176 1176 # ssh: return remote's addchangegroup()
1177 1177 # http: return remote's addchangegroup() or 0 for error
1178 1178 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads,
1179 1179 pushop.repo.url())
1180 1180
1181 1181 def _pushsyncphase(pushop):
1182 1182 """synchronise phase information locally and remotely"""
1183 1183 cheads = pushop.commonheads
1184 1184 # even when we don't push, exchanging phase data is useful
1185 1185 remotephases = listkeys(pushop.remote, 'phases')
1186 1186 if (pushop.ui.configbool('ui', '_usedassubrepo')
1187 1187 and remotephases # server supports phases
1188 1188 and pushop.cgresult is None # nothing was pushed
1189 1189 and remotephases.get('publishing', False)):
1190 1190 # When:
1191 1191 # - this is a subrepo push
1192 1192 # - and remote support phase
1193 1193 # - and no changeset was pushed
1194 1194 # - and remote is publishing
1195 1195 # We may be in issue 3871 case!
1196 1196 # We drop the possible phase synchronisation done by
1197 1197 # courtesy to publish changesets possibly locally draft
1198 1198 # on the remote.
1199 1199 remotephases = {'publishing': 'True'}
1200 1200 if not remotephases: # old server or public only reply from non-publishing
1201 1201 _localphasemove(pushop, cheads)
1202 1202 # don't push any phase data as there is nothing to push
1203 1203 else:
1204 1204 ana = phases.analyzeremotephases(pushop.repo, cheads,
1205 1205 remotephases)
1206 1206 pheads, droots = ana
1207 1207 ### Apply remote phase on local
1208 1208 if remotephases.get('publishing', False):
1209 1209 _localphasemove(pushop, cheads)
1210 1210 else: # publish = False
1211 1211 _localphasemove(pushop, pheads)
1212 1212 _localphasemove(pushop, cheads, phases.draft)
1213 1213 ### Apply local phase on remote
1214 1214
1215 1215 if pushop.cgresult:
1216 1216 if 'phases' in pushop.stepsdone:
1217 1217 # phases already pushed though bundle2
1218 1218 return
1219 1219 outdated = pushop.outdatedphases
1220 1220 else:
1221 1221 outdated = pushop.fallbackoutdatedphases
1222 1222
1223 1223 pushop.stepsdone.add('phases')
1224 1224
1225 1225 # filter heads already turned public by the push
1226 1226 outdated = [c for c in outdated if c.node() not in pheads]
1227 1227 # fallback to independent pushkey command
1228 1228 for newremotehead in outdated:
1229 1229 with pushop.remote.commandexecutor() as e:
1230 1230 r = e.callcommand('pushkey', {
1231 1231 'namespace': 'phases',
1232 1232 'key': newremotehead.hex(),
1233 1233 'old': '%d' % phases.draft,
1234 1234 'new': '%d' % phases.public
1235 1235 }).result()
1236 1236
1237 1237 if not r:
1238 1238 pushop.ui.warn(_('updating %s to public failed!\n')
1239 1239 % newremotehead)
1240 1240
1241 1241 def _localphasemove(pushop, nodes, phase=phases.public):
1242 1242 """move <nodes> to <phase> in the local source repo"""
1243 1243 if pushop.trmanager:
1244 1244 phases.advanceboundary(pushop.repo,
1245 1245 pushop.trmanager.transaction(),
1246 1246 phase,
1247 1247 nodes)
1248 1248 else:
1249 1249 # repo is not locked, do not change any phases!
1250 1250 # Informs the user that phases should have been moved when
1251 1251 # applicable.
1252 1252 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
1253 1253 phasestr = phases.phasenames[phase]
1254 1254 if actualmoves:
1255 1255 pushop.ui.status(_('cannot lock source repo, skipping '
1256 1256 'local %s phase update\n') % phasestr)
1257 1257
1258 1258 def _pushobsolete(pushop):
1259 1259 """utility function to push obsolete markers to a remote"""
1260 1260 if 'obsmarkers' in pushop.stepsdone:
1261 1261 return
1262 1262 repo = pushop.repo
1263 1263 remote = pushop.remote
1264 1264 pushop.stepsdone.add('obsmarkers')
1265 1265 if pushop.outobsmarkers:
1266 1266 pushop.ui.debug('try to push obsolete markers to remote\n')
1267 1267 rslts = []
1268 1268 remotedata = obsolete._pushkeyescape(sorted(pushop.outobsmarkers))
1269 1269 for key in sorted(remotedata, reverse=True):
1270 1270 # reverse sort to ensure we end with dump0
1271 1271 data = remotedata[key]
1272 1272 rslts.append(remote.pushkey('obsolete', key, '', data))
1273 1273 if [r for r in rslts if not r]:
1274 1274 msg = _('failed to push some obsolete markers!\n')
1275 1275 repo.ui.warn(msg)
1276 1276
1277 1277 def _pushbookmark(pushop):
1278 1278 """Update bookmark position on remote"""
1279 1279 if pushop.cgresult == 0 or 'bookmarks' in pushop.stepsdone:
1280 1280 return
1281 1281 pushop.stepsdone.add('bookmarks')
1282 1282 ui = pushop.ui
1283 1283 remote = pushop.remote
1284 1284
1285 1285 for b, old, new in pushop.outbookmarks:
1286 1286 action = 'update'
1287 1287 if not old:
1288 1288 action = 'export'
1289 1289 elif not new:
1290 1290 action = 'delete'
1291 1291
1292 1292 with remote.commandexecutor() as e:
1293 1293 r = e.callcommand('pushkey', {
1294 1294 'namespace': 'bookmarks',
1295 1295 'key': b,
1296 1296 'old': old,
1297 1297 'new': new,
1298 1298 }).result()
1299 1299
1300 1300 if r:
1301 1301 ui.status(bookmsgmap[action][0] % b)
1302 1302 else:
1303 1303 ui.warn(bookmsgmap[action][1] % b)
1304 1304 # discovery can have set the value form invalid entry
1305 1305 if pushop.bkresult is not None:
1306 1306 pushop.bkresult = 1
1307 1307
1308 1308 class pulloperation(object):
1309 1309 """A object that represent a single pull operation
1310 1310
1311 1311 It purpose is to carry pull related state and very common operation.
1312 1312
1313 1313 A new should be created at the beginning of each pull and discarded
1314 1314 afterward.
1315 1315 """
1316 1316
1317 1317 def __init__(self, repo, remote, heads=None, force=False, bookmarks=(),
1318 1318 remotebookmarks=None, streamclonerequested=None,
1319 includepats=None, excludepats=None):
1319 includepats=None, excludepats=None, depth=None):
1320 1320 # repo we pull into
1321 1321 self.repo = repo
1322 1322 # repo we pull from
1323 1323 self.remote = remote
1324 1324 # revision we try to pull (None is "all")
1325 1325 self.heads = heads
1326 1326 # bookmark pulled explicitly
1327 1327 self.explicitbookmarks = [repo._bookmarks.expandname(bookmark)
1328 1328 for bookmark in bookmarks]
1329 1329 # do we force pull?
1330 1330 self.force = force
1331 1331 # whether a streaming clone was requested
1332 1332 self.streamclonerequested = streamclonerequested
1333 1333 # transaction manager
1334 1334 self.trmanager = None
1335 1335 # set of common changeset between local and remote before pull
1336 1336 self.common = None
1337 1337 # set of pulled head
1338 1338 self.rheads = None
1339 1339 # list of missing changeset to fetch remotely
1340 1340 self.fetch = None
1341 1341 # remote bookmarks data
1342 1342 self.remotebookmarks = remotebookmarks
1343 1343 # result of changegroup pulling (used as return code by pull)
1344 1344 self.cgresult = None
1345 1345 # list of step already done
1346 1346 self.stepsdone = set()
1347 1347 # Whether we attempted a clone from pre-generated bundles.
1348 1348 self.clonebundleattempted = False
1349 1349 # Set of file patterns to include.
1350 1350 self.includepats = includepats
1351 1351 # Set of file patterns to exclude.
1352 1352 self.excludepats = excludepats
1353 # Number of ancestor changesets to pull from each pulled head.
1354 self.depth = depth
1353 1355
1354 1356 @util.propertycache
1355 1357 def pulledsubset(self):
1356 1358 """heads of the set of changeset target by the pull"""
1357 1359 # compute target subset
1358 1360 if self.heads is None:
1359 1361 # We pulled every thing possible
1360 1362 # sync on everything common
1361 1363 c = set(self.common)
1362 1364 ret = list(self.common)
1363 1365 for n in self.rheads:
1364 1366 if n not in c:
1365 1367 ret.append(n)
1366 1368 return ret
1367 1369 else:
1368 1370 # We pulled a specific subset
1369 1371 # sync on this subset
1370 1372 return self.heads
1371 1373
1372 1374 @util.propertycache
1373 1375 def canusebundle2(self):
1374 1376 return not _forcebundle1(self)
1375 1377
1376 1378 @util.propertycache
1377 1379 def remotebundle2caps(self):
1378 1380 return bundle2.bundle2caps(self.remote)
1379 1381
1380 1382 def gettransaction(self):
1381 1383 # deprecated; talk to trmanager directly
1382 1384 return self.trmanager.transaction()
1383 1385
1384 1386 class transactionmanager(util.transactional):
1385 1387 """An object to manage the life cycle of a transaction
1386 1388
1387 1389 It creates the transaction on demand and calls the appropriate hooks when
1388 1390 closing the transaction."""
1389 1391 def __init__(self, repo, source, url):
1390 1392 self.repo = repo
1391 1393 self.source = source
1392 1394 self.url = url
1393 1395 self._tr = None
1394 1396
1395 1397 def transaction(self):
1396 1398 """Return an open transaction object, constructing if necessary"""
1397 1399 if not self._tr:
1398 1400 trname = '%s\n%s' % (self.source, util.hidepassword(self.url))
1399 1401 self._tr = self.repo.transaction(trname)
1400 1402 self._tr.hookargs['source'] = self.source
1401 1403 self._tr.hookargs['url'] = self.url
1402 1404 return self._tr
1403 1405
1404 1406 def close(self):
1405 1407 """close transaction if created"""
1406 1408 if self._tr is not None:
1407 1409 self._tr.close()
1408 1410
1409 1411 def release(self):
1410 1412 """release transaction if created"""
1411 1413 if self._tr is not None:
1412 1414 self._tr.release()
1413 1415
1414 1416 def listkeys(remote, namespace):
1415 1417 with remote.commandexecutor() as e:
1416 1418 return e.callcommand('listkeys', {'namespace': namespace}).result()
1417 1419
1418 1420 def _fullpullbundle2(repo, pullop):
1419 1421 # The server may send a partial reply, i.e. when inlining
1420 1422 # pre-computed bundles. In that case, update the common
1421 1423 # set based on the results and pull another bundle.
1422 1424 #
1423 1425 # There are two indicators that the process is finished:
1424 1426 # - no changeset has been added, or
1425 1427 # - all remote heads are known locally.
1426 1428 # The head check must use the unfiltered view as obsoletion
1427 1429 # markers can hide heads.
1428 1430 unfi = repo.unfiltered()
1429 1431 unficl = unfi.changelog
1430 1432 def headsofdiff(h1, h2):
1431 1433 """Returns heads(h1 % h2)"""
1432 1434 res = unfi.set('heads(%ln %% %ln)', h1, h2)
1433 1435 return set(ctx.node() for ctx in res)
1434 1436 def headsofunion(h1, h2):
1435 1437 """Returns heads((h1 + h2) - null)"""
1436 1438 res = unfi.set('heads((%ln + %ln - null))', h1, h2)
1437 1439 return set(ctx.node() for ctx in res)
1438 1440 while True:
1439 1441 old_heads = unficl.heads()
1440 1442 clstart = len(unficl)
1441 1443 _pullbundle2(pullop)
1442 1444 if repository.NARROW_REQUIREMENT in repo.requirements:
1443 1445 # XXX narrow clones filter the heads on the server side during
1444 1446 # XXX getbundle and result in partial replies as well.
1445 1447 # XXX Disable pull bundles in this case as band aid to avoid
1446 1448 # XXX extra round trips.
1447 1449 break
1448 1450 if clstart == len(unficl):
1449 1451 break
1450 1452 if all(unficl.hasnode(n) for n in pullop.rheads):
1451 1453 break
1452 1454 new_heads = headsofdiff(unficl.heads(), old_heads)
1453 1455 pullop.common = headsofunion(new_heads, pullop.common)
1454 1456 pullop.rheads = set(pullop.rheads) - pullop.common
1455 1457
1456 1458 def pull(repo, remote, heads=None, force=False, bookmarks=(), opargs=None,
1457 streamclonerequested=None, includepats=None, excludepats=None):
1459 streamclonerequested=None, includepats=None, excludepats=None,
1460 depth=None):
1458 1461 """Fetch repository data from a remote.
1459 1462
1460 1463 This is the main function used to retrieve data from a remote repository.
1461 1464
1462 1465 ``repo`` is the local repository to clone into.
1463 1466 ``remote`` is a peer instance.
1464 1467 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1465 1468 default) means to pull everything from the remote.
1466 1469 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1467 1470 default, all remote bookmarks are pulled.
1468 1471 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1469 1472 initialization.
1470 1473 ``streamclonerequested`` is a boolean indicating whether a "streaming
1471 1474 clone" is requested. A "streaming clone" is essentially a raw file copy
1472 1475 of revlogs from the server. This only works when the local repository is
1473 1476 empty. The default value of ``None`` means to respect the server
1474 1477 configuration for preferring stream clones.
1475 1478 ``includepats`` and ``excludepats`` define explicit file patterns to
1476 1479 include and exclude in storage, respectively. If not defined, narrow
1477 1480 patterns from the repo instance are used, if available.
1481 ``depth`` is an integer indicating the DAG depth of history we're
1482 interested in. If defined, for each revision specified in ``heads``, we
1483 will fetch up to this many of its ancestors and data associated with them.
1478 1484
1479 1485 Returns the ``pulloperation`` created for this pull.
1480 1486 """
1481 1487 if opargs is None:
1482 1488 opargs = {}
1483 1489
1484 1490 # We allow the narrow patterns to be passed in explicitly to provide more
1485 1491 # flexibility for API consumers.
1486 1492 if includepats or excludepats:
1487 1493 includepats = includepats or set()
1488 1494 excludepats = excludepats or set()
1489 1495 else:
1490 1496 includepats, excludepats = repo.narrowpats
1491 1497
1492 1498 narrowspec.validatepatterns(includepats)
1493 1499 narrowspec.validatepatterns(excludepats)
1494 1500
1495 1501 pullop = pulloperation(repo, remote, heads, force, bookmarks=bookmarks,
1496 1502 streamclonerequested=streamclonerequested,
1497 1503 includepats=includepats, excludepats=excludepats,
1504 depth=depth,
1498 1505 **pycompat.strkwargs(opargs))
1499 1506
1500 1507 peerlocal = pullop.remote.local()
1501 1508 if peerlocal:
1502 1509 missing = set(peerlocal.requirements) - pullop.repo.supported
1503 1510 if missing:
1504 1511 msg = _("required features are not"
1505 1512 " supported in the destination:"
1506 1513 " %s") % (', '.join(sorted(missing)))
1507 1514 raise error.Abort(msg)
1508 1515
1509 1516 pullop.trmanager = transactionmanager(repo, 'pull', remote.url())
1510 1517 with repo.wlock(), repo.lock(), pullop.trmanager:
1511 1518 # Use the modern wire protocol, if available.
1512 1519 if remote.capable('command-changesetdata'):
1513 1520 exchangev2.pull(pullop)
1514 1521 else:
1515 1522 # This should ideally be in _pullbundle2(). However, it needs to run
1516 1523 # before discovery to avoid extra work.
1517 1524 _maybeapplyclonebundle(pullop)
1518 1525 streamclone.maybeperformlegacystreamclone(pullop)
1519 1526 _pulldiscovery(pullop)
1520 1527 if pullop.canusebundle2:
1521 1528 _fullpullbundle2(repo, pullop)
1522 1529 _pullchangeset(pullop)
1523 1530 _pullphase(pullop)
1524 1531 _pullbookmarks(pullop)
1525 1532 _pullobsolete(pullop)
1526 1533
1527 1534 # storing remotenames
1528 1535 if repo.ui.configbool('experimental', 'remotenames'):
1529 1536 logexchange.pullremotenames(repo, remote)
1530 1537
1531 1538 return pullop
1532 1539
1533 1540 # list of steps to perform discovery before pull
1534 1541 pulldiscoveryorder = []
1535 1542
1536 1543 # Mapping between step name and function
1537 1544 #
1538 1545 # This exists to help extensions wrap steps if necessary
1539 1546 pulldiscoverymapping = {}
1540 1547
1541 1548 def pulldiscovery(stepname):
1542 1549 """decorator for function performing discovery before pull
1543 1550
1544 1551 The function is added to the step -> function mapping and appended to the
1545 1552 list of steps. Beware that decorated function will be added in order (this
1546 1553 may matter).
1547 1554
1548 1555 You can only use this decorator for a new step, if you want to wrap a step
1549 1556 from an extension, change the pulldiscovery dictionary directly."""
1550 1557 def dec(func):
1551 1558 assert stepname not in pulldiscoverymapping
1552 1559 pulldiscoverymapping[stepname] = func
1553 1560 pulldiscoveryorder.append(stepname)
1554 1561 return func
1555 1562 return dec
1556 1563
1557 1564 def _pulldiscovery(pullop):
1558 1565 """Run all discovery steps"""
1559 1566 for stepname in pulldiscoveryorder:
1560 1567 step = pulldiscoverymapping[stepname]
1561 1568 step(pullop)
1562 1569
1563 1570 @pulldiscovery('b1:bookmarks')
1564 1571 def _pullbookmarkbundle1(pullop):
1565 1572 """fetch bookmark data in bundle1 case
1566 1573
1567 1574 If not using bundle2, we have to fetch bookmarks before changeset
1568 1575 discovery to reduce the chance and impact of race conditions."""
1569 1576 if pullop.remotebookmarks is not None:
1570 1577 return
1571 1578 if pullop.canusebundle2 and 'listkeys' in pullop.remotebundle2caps:
1572 1579 # all known bundle2 servers now support listkeys, but lets be nice with
1573 1580 # new implementation.
1574 1581 return
1575 1582 books = listkeys(pullop.remote, 'bookmarks')
1576 1583 pullop.remotebookmarks = bookmod.unhexlifybookmarks(books)
1577 1584
1578 1585
1579 1586 @pulldiscovery('changegroup')
1580 1587 def _pulldiscoverychangegroup(pullop):
1581 1588 """discovery phase for the pull
1582 1589
1583 1590 Current handle changeset discovery only, will change handle all discovery
1584 1591 at some point."""
1585 1592 tmp = discovery.findcommonincoming(pullop.repo,
1586 1593 pullop.remote,
1587 1594 heads=pullop.heads,
1588 1595 force=pullop.force)
1589 1596 common, fetch, rheads = tmp
1590 1597 nm = pullop.repo.unfiltered().changelog.nodemap
1591 1598 if fetch and rheads:
1592 1599 # If a remote heads is filtered locally, put in back in common.
1593 1600 #
1594 1601 # This is a hackish solution to catch most of "common but locally
1595 1602 # hidden situation". We do not performs discovery on unfiltered
1596 1603 # repository because it end up doing a pathological amount of round
1597 1604 # trip for w huge amount of changeset we do not care about.
1598 1605 #
1599 1606 # If a set of such "common but filtered" changeset exist on the server
1600 1607 # but are not including a remote heads, we'll not be able to detect it,
1601 1608 scommon = set(common)
1602 1609 for n in rheads:
1603 1610 if n in nm:
1604 1611 if n not in scommon:
1605 1612 common.append(n)
1606 1613 if set(rheads).issubset(set(common)):
1607 1614 fetch = []
1608 1615 pullop.common = common
1609 1616 pullop.fetch = fetch
1610 1617 pullop.rheads = rheads
1611 1618
1612 1619 def _pullbundle2(pullop):
1613 1620 """pull data using bundle2
1614 1621
1615 1622 For now, the only supported data are changegroup."""
1616 1623 kwargs = {'bundlecaps': caps20to10(pullop.repo, role='client')}
1617 1624
1618 1625 # make ui easier to access
1619 1626 ui = pullop.repo.ui
1620 1627
1621 1628 # At the moment we don't do stream clones over bundle2. If that is
1622 1629 # implemented then here's where the check for that will go.
1623 1630 streaming = streamclone.canperformstreamclone(pullop, bundle2=True)[0]
1624 1631
1625 1632 # declare pull perimeters
1626 1633 kwargs['common'] = pullop.common
1627 1634 kwargs['heads'] = pullop.heads or pullop.rheads
1628 1635
1629 1636 if streaming:
1630 1637 kwargs['cg'] = False
1631 1638 kwargs['stream'] = True
1632 1639 pullop.stepsdone.add('changegroup')
1633 1640 pullop.stepsdone.add('phases')
1634 1641
1635 1642 else:
1636 1643 # pulling changegroup
1637 1644 pullop.stepsdone.add('changegroup')
1638 1645
1639 1646 kwargs['cg'] = pullop.fetch
1640 1647
1641 1648 legacyphase = 'phases' in ui.configlist('devel', 'legacy.exchange')
1642 1649 hasbinaryphase = 'heads' in pullop.remotebundle2caps.get('phases', ())
1643 1650 if (not legacyphase and hasbinaryphase):
1644 1651 kwargs['phases'] = True
1645 1652 pullop.stepsdone.add('phases')
1646 1653
1647 1654 if 'listkeys' in pullop.remotebundle2caps:
1648 1655 if 'phases' not in pullop.stepsdone:
1649 1656 kwargs['listkeys'] = ['phases']
1650 1657
1651 1658 bookmarksrequested = False
1652 1659 legacybookmark = 'bookmarks' in ui.configlist('devel', 'legacy.exchange')
1653 1660 hasbinarybook = 'bookmarks' in pullop.remotebundle2caps
1654 1661
1655 1662 if pullop.remotebookmarks is not None:
1656 1663 pullop.stepsdone.add('request-bookmarks')
1657 1664
1658 1665 if ('request-bookmarks' not in pullop.stepsdone
1659 1666 and pullop.remotebookmarks is None
1660 1667 and not legacybookmark and hasbinarybook):
1661 1668 kwargs['bookmarks'] = True
1662 1669 bookmarksrequested = True
1663 1670
1664 1671 if 'listkeys' in pullop.remotebundle2caps:
1665 1672 if 'request-bookmarks' not in pullop.stepsdone:
1666 1673 # make sure to always includes bookmark data when migrating
1667 1674 # `hg incoming --bundle` to using this function.
1668 1675 pullop.stepsdone.add('request-bookmarks')
1669 1676 kwargs.setdefault('listkeys', []).append('bookmarks')
1670 1677
1671 1678 # If this is a full pull / clone and the server supports the clone bundles
1672 1679 # feature, tell the server whether we attempted a clone bundle. The
1673 1680 # presence of this flag indicates the client supports clone bundles. This
1674 1681 # will enable the server to treat clients that support clone bundles
1675 1682 # differently from those that don't.
1676 1683 if (pullop.remote.capable('clonebundles')
1677 1684 and pullop.heads is None and list(pullop.common) == [nullid]):
1678 1685 kwargs['cbattempted'] = pullop.clonebundleattempted
1679 1686
1680 1687 if streaming:
1681 1688 pullop.repo.ui.status(_('streaming all changes\n'))
1682 1689 elif not pullop.fetch:
1683 1690 pullop.repo.ui.status(_("no changes found\n"))
1684 1691 pullop.cgresult = 0
1685 1692 else:
1686 1693 if pullop.heads is None and list(pullop.common) == [nullid]:
1687 1694 pullop.repo.ui.status(_("requesting all changes\n"))
1688 1695 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1689 1696 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1690 1697 if obsolete.commonversion(remoteversions) is not None:
1691 1698 kwargs['obsmarkers'] = True
1692 1699 pullop.stepsdone.add('obsmarkers')
1693 1700 _pullbundle2extraprepare(pullop, kwargs)
1694 1701
1695 1702 with pullop.remote.commandexecutor() as e:
1696 1703 args = dict(kwargs)
1697 1704 args['source'] = 'pull'
1698 1705 bundle = e.callcommand('getbundle', args).result()
1699 1706
1700 1707 try:
1701 1708 op = bundle2.bundleoperation(pullop.repo, pullop.gettransaction,
1702 1709 source='pull')
1703 1710 op.modes['bookmarks'] = 'records'
1704 1711 bundle2.processbundle(pullop.repo, bundle, op=op)
1705 1712 except bundle2.AbortFromPart as exc:
1706 1713 pullop.repo.ui.status(_('remote: abort: %s\n') % exc)
1707 1714 raise error.Abort(_('pull failed on remote'), hint=exc.hint)
1708 1715 except error.BundleValueError as exc:
1709 1716 raise error.Abort(_('missing support for %s') % exc)
1710 1717
1711 1718 if pullop.fetch:
1712 1719 pullop.cgresult = bundle2.combinechangegroupresults(op)
1713 1720
1714 1721 # processing phases change
1715 1722 for namespace, value in op.records['listkeys']:
1716 1723 if namespace == 'phases':
1717 1724 _pullapplyphases(pullop, value)
1718 1725
1719 1726 # processing bookmark update
1720 1727 if bookmarksrequested:
1721 1728 books = {}
1722 1729 for record in op.records['bookmarks']:
1723 1730 books[record['bookmark']] = record["node"]
1724 1731 pullop.remotebookmarks = books
1725 1732 else:
1726 1733 for namespace, value in op.records['listkeys']:
1727 1734 if namespace == 'bookmarks':
1728 1735 pullop.remotebookmarks = bookmod.unhexlifybookmarks(value)
1729 1736
1730 1737 # bookmark data were either already there or pulled in the bundle
1731 1738 if pullop.remotebookmarks is not None:
1732 1739 _pullbookmarks(pullop)
1733 1740
1734 1741 def _pullbundle2extraprepare(pullop, kwargs):
1735 1742 """hook function so that extensions can extend the getbundle call"""
1736 1743
1737 1744 def _pullchangeset(pullop):
1738 1745 """pull changeset from unbundle into the local repo"""
1739 1746 # We delay the open of the transaction as late as possible so we
1740 1747 # don't open transaction for nothing or you break future useful
1741 1748 # rollback call
1742 1749 if 'changegroup' in pullop.stepsdone:
1743 1750 return
1744 1751 pullop.stepsdone.add('changegroup')
1745 1752 if not pullop.fetch:
1746 1753 pullop.repo.ui.status(_("no changes found\n"))
1747 1754 pullop.cgresult = 0
1748 1755 return
1749 1756 tr = pullop.gettransaction()
1750 1757 if pullop.heads is None and list(pullop.common) == [nullid]:
1751 1758 pullop.repo.ui.status(_("requesting all changes\n"))
1752 1759 elif pullop.heads is None and pullop.remote.capable('changegroupsubset'):
1753 1760 # issue1320, avoid a race if remote changed after discovery
1754 1761 pullop.heads = pullop.rheads
1755 1762
1756 1763 if pullop.remote.capable('getbundle'):
1757 1764 # TODO: get bundlecaps from remote
1758 1765 cg = pullop.remote.getbundle('pull', common=pullop.common,
1759 1766 heads=pullop.heads or pullop.rheads)
1760 1767 elif pullop.heads is None:
1761 1768 with pullop.remote.commandexecutor() as e:
1762 1769 cg = e.callcommand('changegroup', {
1763 1770 'nodes': pullop.fetch,
1764 1771 'source': 'pull',
1765 1772 }).result()
1766 1773
1767 1774 elif not pullop.remote.capable('changegroupsubset'):
1768 1775 raise error.Abort(_("partial pull cannot be done because "
1769 1776 "other repository doesn't support "
1770 1777 "changegroupsubset."))
1771 1778 else:
1772 1779 with pullop.remote.commandexecutor() as e:
1773 1780 cg = e.callcommand('changegroupsubset', {
1774 1781 'bases': pullop.fetch,
1775 1782 'heads': pullop.heads,
1776 1783 'source': 'pull',
1777 1784 }).result()
1778 1785
1779 1786 bundleop = bundle2.applybundle(pullop.repo, cg, tr, 'pull',
1780 1787 pullop.remote.url())
1781 1788 pullop.cgresult = bundle2.combinechangegroupresults(bundleop)
1782 1789
1783 1790 def _pullphase(pullop):
1784 1791 # Get remote phases data from remote
1785 1792 if 'phases' in pullop.stepsdone:
1786 1793 return
1787 1794 remotephases = listkeys(pullop.remote, 'phases')
1788 1795 _pullapplyphases(pullop, remotephases)
1789 1796
1790 1797 def _pullapplyphases(pullop, remotephases):
1791 1798 """apply phase movement from observed remote state"""
1792 1799 if 'phases' in pullop.stepsdone:
1793 1800 return
1794 1801 pullop.stepsdone.add('phases')
1795 1802 publishing = bool(remotephases.get('publishing', False))
1796 1803 if remotephases and not publishing:
1797 1804 # remote is new and non-publishing
1798 1805 pheads, _dr = phases.analyzeremotephases(pullop.repo,
1799 1806 pullop.pulledsubset,
1800 1807 remotephases)
1801 1808 dheads = pullop.pulledsubset
1802 1809 else:
1803 1810 # Remote is old or publishing all common changesets
1804 1811 # should be seen as public
1805 1812 pheads = pullop.pulledsubset
1806 1813 dheads = []
1807 1814 unfi = pullop.repo.unfiltered()
1808 1815 phase = unfi._phasecache.phase
1809 1816 rev = unfi.changelog.nodemap.get
1810 1817 public = phases.public
1811 1818 draft = phases.draft
1812 1819
1813 1820 # exclude changesets already public locally and update the others
1814 1821 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
1815 1822 if pheads:
1816 1823 tr = pullop.gettransaction()
1817 1824 phases.advanceboundary(pullop.repo, tr, public, pheads)
1818 1825
1819 1826 # exclude changesets already draft locally and update the others
1820 1827 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
1821 1828 if dheads:
1822 1829 tr = pullop.gettransaction()
1823 1830 phases.advanceboundary(pullop.repo, tr, draft, dheads)
1824 1831
1825 1832 def _pullbookmarks(pullop):
1826 1833 """process the remote bookmark information to update the local one"""
1827 1834 if 'bookmarks' in pullop.stepsdone:
1828 1835 return
1829 1836 pullop.stepsdone.add('bookmarks')
1830 1837 repo = pullop.repo
1831 1838 remotebookmarks = pullop.remotebookmarks
1832 1839 bookmod.updatefromremote(repo.ui, repo, remotebookmarks,
1833 1840 pullop.remote.url(),
1834 1841 pullop.gettransaction,
1835 1842 explicit=pullop.explicitbookmarks)
1836 1843
1837 1844 def _pullobsolete(pullop):
1838 1845 """utility function to pull obsolete markers from a remote
1839 1846
1840 1847 The `gettransaction` is function that return the pull transaction, creating
1841 1848 one if necessary. We return the transaction to inform the calling code that
1842 1849 a new transaction have been created (when applicable).
1843 1850
1844 1851 Exists mostly to allow overriding for experimentation purpose"""
1845 1852 if 'obsmarkers' in pullop.stepsdone:
1846 1853 return
1847 1854 pullop.stepsdone.add('obsmarkers')
1848 1855 tr = None
1849 1856 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1850 1857 pullop.repo.ui.debug('fetching remote obsolete markers\n')
1851 1858 remoteobs = listkeys(pullop.remote, 'obsolete')
1852 1859 if 'dump0' in remoteobs:
1853 1860 tr = pullop.gettransaction()
1854 1861 markers = []
1855 1862 for key in sorted(remoteobs, reverse=True):
1856 1863 if key.startswith('dump'):
1857 1864 data = util.b85decode(remoteobs[key])
1858 1865 version, newmarks = obsolete._readmarkers(data)
1859 1866 markers += newmarks
1860 1867 if markers:
1861 1868 pullop.repo.obsstore.add(tr, markers)
1862 1869 pullop.repo.invalidatevolatilesets()
1863 1870 return tr
1864 1871
1865 1872 def applynarrowacl(repo, kwargs):
1866 1873 """Apply narrow fetch access control.
1867 1874
1868 1875 This massages the named arguments for getbundle wire protocol commands
1869 1876 so requested data is filtered through access control rules.
1870 1877 """
1871 1878 ui = repo.ui
1872 1879 # TODO this assumes existence of HTTP and is a layering violation.
1873 1880 username = ui.shortuser(ui.environ.get('REMOTE_USER') or ui.username())
1874 1881 user_includes = ui.configlist(
1875 1882 _NARROWACL_SECTION, username + '.includes',
1876 1883 ui.configlist(_NARROWACL_SECTION, 'default.includes'))
1877 1884 user_excludes = ui.configlist(
1878 1885 _NARROWACL_SECTION, username + '.excludes',
1879 1886 ui.configlist(_NARROWACL_SECTION, 'default.excludes'))
1880 1887 if not user_includes:
1881 1888 raise error.Abort(_("{} configuration for user {} is empty")
1882 1889 .format(_NARROWACL_SECTION, username))
1883 1890
1884 1891 user_includes = [
1885 1892 'path:.' if p == '*' else 'path:' + p for p in user_includes]
1886 1893 user_excludes = [
1887 1894 'path:.' if p == '*' else 'path:' + p for p in user_excludes]
1888 1895
1889 1896 req_includes = set(kwargs.get(r'includepats', []))
1890 1897 req_excludes = set(kwargs.get(r'excludepats', []))
1891 1898
1892 1899 req_includes, req_excludes, invalid_includes = narrowspec.restrictpatterns(
1893 1900 req_includes, req_excludes, user_includes, user_excludes)
1894 1901
1895 1902 if invalid_includes:
1896 1903 raise error.Abort(
1897 1904 _("The following includes are not accessible for {}: {}")
1898 1905 .format(username, invalid_includes))
1899 1906
1900 1907 new_args = {}
1901 1908 new_args.update(kwargs)
1902 1909 new_args[r'narrow'] = True
1903 1910 new_args[r'includepats'] = req_includes
1904 1911 if req_excludes:
1905 1912 new_args[r'excludepats'] = req_excludes
1906 1913
1907 1914 return new_args
1908 1915
1909 1916 def _computeellipsis(repo, common, heads, known, match, depth=None):
1910 1917 """Compute the shape of a narrowed DAG.
1911 1918
1912 1919 Args:
1913 1920 repo: The repository we're transferring.
1914 1921 common: The roots of the DAG range we're transferring.
1915 1922 May be just [nullid], which means all ancestors of heads.
1916 1923 heads: The heads of the DAG range we're transferring.
1917 1924 match: The narrowmatcher that allows us to identify relevant changes.
1918 1925 depth: If not None, only consider nodes to be full nodes if they are at
1919 1926 most depth changesets away from one of heads.
1920 1927
1921 1928 Returns:
1922 1929 A tuple of (visitnodes, relevant_nodes, ellipsisroots) where:
1923 1930
1924 1931 visitnodes: The list of nodes (either full or ellipsis) which
1925 1932 need to be sent to the client.
1926 1933 relevant_nodes: The set of changelog nodes which change a file inside
1927 1934 the narrowspec. The client needs these as non-ellipsis nodes.
1928 1935 ellipsisroots: A dict of {rev: parents} that is used in
1929 1936 narrowchangegroup to produce ellipsis nodes with the
1930 1937 correct parents.
1931 1938 """
1932 1939 cl = repo.changelog
1933 1940 mfl = repo.manifestlog
1934 1941
1935 1942 clrev = cl.rev
1936 1943
1937 1944 commonrevs = {clrev(n) for n in common} | {nullrev}
1938 1945 headsrevs = {clrev(n) for n in heads}
1939 1946
1940 1947 if depth:
1941 1948 revdepth = {h: 0 for h in headsrevs}
1942 1949
1943 1950 ellipsisheads = collections.defaultdict(set)
1944 1951 ellipsisroots = collections.defaultdict(set)
1945 1952
1946 1953 def addroot(head, curchange):
1947 1954 """Add a root to an ellipsis head, splitting heads with 3 roots."""
1948 1955 ellipsisroots[head].add(curchange)
1949 1956 # Recursively split ellipsis heads with 3 roots by finding the
1950 1957 # roots' youngest common descendant which is an elided merge commit.
1951 1958 # That descendant takes 2 of the 3 roots as its own, and becomes a
1952 1959 # root of the head.
1953 1960 while len(ellipsisroots[head]) > 2:
1954 1961 child, roots = splithead(head)
1955 1962 splitroots(head, child, roots)
1956 1963 head = child # Recurse in case we just added a 3rd root
1957 1964
1958 1965 def splitroots(head, child, roots):
1959 1966 ellipsisroots[head].difference_update(roots)
1960 1967 ellipsisroots[head].add(child)
1961 1968 ellipsisroots[child].update(roots)
1962 1969 ellipsisroots[child].discard(child)
1963 1970
1964 1971 def splithead(head):
1965 1972 r1, r2, r3 = sorted(ellipsisroots[head])
1966 1973 for nr1, nr2 in ((r2, r3), (r1, r3), (r1, r2)):
1967 1974 mid = repo.revs('sort(merge() & %d::%d & %d::%d, -rev)',
1968 1975 nr1, head, nr2, head)
1969 1976 for j in mid:
1970 1977 if j == nr2:
1971 1978 return nr2, (nr1, nr2)
1972 1979 if j not in ellipsisroots or len(ellipsisroots[j]) < 2:
1973 1980 return j, (nr1, nr2)
1974 1981 raise error.Abort(_('Failed to split up ellipsis node! head: %d, '
1975 1982 'roots: %d %d %d') % (head, r1, r2, r3))
1976 1983
1977 1984 missing = list(cl.findmissingrevs(common=commonrevs, heads=headsrevs))
1978 1985 visit = reversed(missing)
1979 1986 relevant_nodes = set()
1980 1987 visitnodes = [cl.node(m) for m in missing]
1981 1988 required = set(headsrevs) | known
1982 1989 for rev in visit:
1983 1990 clrev = cl.changelogrevision(rev)
1984 1991 ps = [prev for prev in cl.parentrevs(rev) if prev != nullrev]
1985 1992 if depth is not None:
1986 1993 curdepth = revdepth[rev]
1987 1994 for p in ps:
1988 1995 revdepth[p] = min(curdepth + 1, revdepth.get(p, depth + 1))
1989 1996 needed = False
1990 1997 shallow_enough = depth is None or revdepth[rev] <= depth
1991 1998 if shallow_enough:
1992 1999 curmf = mfl[clrev.manifest].read()
1993 2000 if ps:
1994 2001 # We choose to not trust the changed files list in
1995 2002 # changesets because it's not always correct. TODO: could
1996 2003 # we trust it for the non-merge case?
1997 2004 p1mf = mfl[cl.changelogrevision(ps[0]).manifest].read()
1998 2005 needed = bool(curmf.diff(p1mf, match))
1999 2006 if not needed and len(ps) > 1:
2000 2007 # For merge changes, the list of changed files is not
2001 2008 # helpful, since we need to emit the merge if a file
2002 2009 # in the narrow spec has changed on either side of the
2003 2010 # merge. As a result, we do a manifest diff to check.
2004 2011 p2mf = mfl[cl.changelogrevision(ps[1]).manifest].read()
2005 2012 needed = bool(curmf.diff(p2mf, match))
2006 2013 else:
2007 2014 # For a root node, we need to include the node if any
2008 2015 # files in the node match the narrowspec.
2009 2016 needed = any(curmf.walk(match))
2010 2017
2011 2018 if needed:
2012 2019 for head in ellipsisheads[rev]:
2013 2020 addroot(head, rev)
2014 2021 for p in ps:
2015 2022 required.add(p)
2016 2023 relevant_nodes.add(cl.node(rev))
2017 2024 else:
2018 2025 if not ps:
2019 2026 ps = [nullrev]
2020 2027 if rev in required:
2021 2028 for head in ellipsisheads[rev]:
2022 2029 addroot(head, rev)
2023 2030 for p in ps:
2024 2031 ellipsisheads[p].add(rev)
2025 2032 else:
2026 2033 for p in ps:
2027 2034 ellipsisheads[p] |= ellipsisheads[rev]
2028 2035
2029 2036 # add common changesets as roots of their reachable ellipsis heads
2030 2037 for c in commonrevs:
2031 2038 for head in ellipsisheads[c]:
2032 2039 addroot(head, c)
2033 2040 return visitnodes, relevant_nodes, ellipsisroots
2034 2041
2035 2042 def caps20to10(repo, role):
2036 2043 """return a set with appropriate options to use bundle20 during getbundle"""
2037 2044 caps = {'HG20'}
2038 2045 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role=role))
2039 2046 caps.add('bundle2=' + urlreq.quote(capsblob))
2040 2047 return caps
2041 2048
2042 2049 # List of names of steps to perform for a bundle2 for getbundle, order matters.
2043 2050 getbundle2partsorder = []
2044 2051
2045 2052 # Mapping between step name and function
2046 2053 #
2047 2054 # This exists to help extensions wrap steps if necessary
2048 2055 getbundle2partsmapping = {}
2049 2056
2050 2057 def getbundle2partsgenerator(stepname, idx=None):
2051 2058 """decorator for function generating bundle2 part for getbundle
2052 2059
2053 2060 The function is added to the step -> function mapping and appended to the
2054 2061 list of steps. Beware that decorated functions will be added in order
2055 2062 (this may matter).
2056 2063
2057 2064 You can only use this decorator for new steps, if you want to wrap a step
2058 2065 from an extension, attack the getbundle2partsmapping dictionary directly."""
2059 2066 def dec(func):
2060 2067 assert stepname not in getbundle2partsmapping
2061 2068 getbundle2partsmapping[stepname] = func
2062 2069 if idx is None:
2063 2070 getbundle2partsorder.append(stepname)
2064 2071 else:
2065 2072 getbundle2partsorder.insert(idx, stepname)
2066 2073 return func
2067 2074 return dec
2068 2075
2069 2076 def bundle2requested(bundlecaps):
2070 2077 if bundlecaps is not None:
2071 2078 return any(cap.startswith('HG2') for cap in bundlecaps)
2072 2079 return False
2073 2080
2074 2081 def getbundlechunks(repo, source, heads=None, common=None, bundlecaps=None,
2075 2082 **kwargs):
2076 2083 """Return chunks constituting a bundle's raw data.
2077 2084
2078 2085 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
2079 2086 passed.
2080 2087
2081 2088 Returns a 2-tuple of a dict with metadata about the generated bundle
2082 2089 and an iterator over raw chunks (of varying sizes).
2083 2090 """
2084 2091 kwargs = pycompat.byteskwargs(kwargs)
2085 2092 info = {}
2086 2093 usebundle2 = bundle2requested(bundlecaps)
2087 2094 # bundle10 case
2088 2095 if not usebundle2:
2089 2096 if bundlecaps and not kwargs.get('cg', True):
2090 2097 raise ValueError(_('request for bundle10 must include changegroup'))
2091 2098
2092 2099 if kwargs:
2093 2100 raise ValueError(_('unsupported getbundle arguments: %s')
2094 2101 % ', '.join(sorted(kwargs.keys())))
2095 2102 outgoing = _computeoutgoing(repo, heads, common)
2096 2103 info['bundleversion'] = 1
2097 2104 return info, changegroup.makestream(repo, outgoing, '01', source,
2098 2105 bundlecaps=bundlecaps)
2099 2106
2100 2107 # bundle20 case
2101 2108 info['bundleversion'] = 2
2102 2109 b2caps = {}
2103 2110 for bcaps in bundlecaps:
2104 2111 if bcaps.startswith('bundle2='):
2105 2112 blob = urlreq.unquote(bcaps[len('bundle2='):])
2106 2113 b2caps.update(bundle2.decodecaps(blob))
2107 2114 bundler = bundle2.bundle20(repo.ui, b2caps)
2108 2115
2109 2116 kwargs['heads'] = heads
2110 2117 kwargs['common'] = common
2111 2118
2112 2119 for name in getbundle2partsorder:
2113 2120 func = getbundle2partsmapping[name]
2114 2121 func(bundler, repo, source, bundlecaps=bundlecaps, b2caps=b2caps,
2115 2122 **pycompat.strkwargs(kwargs))
2116 2123
2117 2124 info['prefercompressed'] = bundler.prefercompressed
2118 2125
2119 2126 return info, bundler.getchunks()
2120 2127
2121 2128 @getbundle2partsgenerator('stream2')
2122 2129 def _getbundlestream2(bundler, repo, *args, **kwargs):
2123 2130 return bundle2.addpartbundlestream2(bundler, repo, **kwargs)
2124 2131
2125 2132 @getbundle2partsgenerator('changegroup')
2126 2133 def _getbundlechangegrouppart(bundler, repo, source, bundlecaps=None,
2127 2134 b2caps=None, heads=None, common=None, **kwargs):
2128 2135 """add a changegroup part to the requested bundle"""
2129 2136 if not kwargs.get(r'cg', True):
2130 2137 return
2131 2138
2132 2139 version = '01'
2133 2140 cgversions = b2caps.get('changegroup')
2134 2141 if cgversions: # 3.1 and 3.2 ship with an empty value
2135 2142 cgversions = [v for v in cgversions
2136 2143 if v in changegroup.supportedoutgoingversions(repo)]
2137 2144 if not cgversions:
2138 2145 raise ValueError(_('no common changegroup version'))
2139 2146 version = max(cgversions)
2140 2147
2141 2148 outgoing = _computeoutgoing(repo, heads, common)
2142 2149 if not outgoing.missing:
2143 2150 return
2144 2151
2145 2152 if kwargs.get(r'narrow', False):
2146 2153 include = sorted(filter(bool, kwargs.get(r'includepats', [])))
2147 2154 exclude = sorted(filter(bool, kwargs.get(r'excludepats', [])))
2148 2155 filematcher = narrowspec.match(repo.root, include=include,
2149 2156 exclude=exclude)
2150 2157 else:
2151 2158 filematcher = None
2152 2159
2153 2160 cgstream = changegroup.makestream(repo, outgoing, version, source,
2154 2161 bundlecaps=bundlecaps,
2155 2162 filematcher=filematcher)
2156 2163
2157 2164 part = bundler.newpart('changegroup', data=cgstream)
2158 2165 if cgversions:
2159 2166 part.addparam('version', version)
2160 2167
2161 2168 part.addparam('nbchanges', '%d' % len(outgoing.missing),
2162 2169 mandatory=False)
2163 2170
2164 2171 if 'treemanifest' in repo.requirements:
2165 2172 part.addparam('treemanifest', '1')
2166 2173
2167 2174 if kwargs.get(r'narrow', False) and (include or exclude):
2168 2175 narrowspecpart = bundler.newpart('narrow:spec')
2169 2176 if include:
2170 2177 narrowspecpart.addparam(
2171 2178 'include', '\n'.join(include), mandatory=True)
2172 2179 if exclude:
2173 2180 narrowspecpart.addparam(
2174 2181 'exclude', '\n'.join(exclude), mandatory=True)
2175 2182
2176 2183 @getbundle2partsgenerator('bookmarks')
2177 2184 def _getbundlebookmarkpart(bundler, repo, source, bundlecaps=None,
2178 2185 b2caps=None, **kwargs):
2179 2186 """add a bookmark part to the requested bundle"""
2180 2187 if not kwargs.get(r'bookmarks', False):
2181 2188 return
2182 2189 if 'bookmarks' not in b2caps:
2183 2190 raise ValueError(_('no common bookmarks exchange method'))
2184 2191 books = bookmod.listbinbookmarks(repo)
2185 2192 data = bookmod.binaryencode(books)
2186 2193 if data:
2187 2194 bundler.newpart('bookmarks', data=data)
2188 2195
2189 2196 @getbundle2partsgenerator('listkeys')
2190 2197 def _getbundlelistkeysparts(bundler, repo, source, bundlecaps=None,
2191 2198 b2caps=None, **kwargs):
2192 2199 """add parts containing listkeys namespaces to the requested bundle"""
2193 2200 listkeys = kwargs.get(r'listkeys', ())
2194 2201 for namespace in listkeys:
2195 2202 part = bundler.newpart('listkeys')
2196 2203 part.addparam('namespace', namespace)
2197 2204 keys = repo.listkeys(namespace).items()
2198 2205 part.data = pushkey.encodekeys(keys)
2199 2206
2200 2207 @getbundle2partsgenerator('obsmarkers')
2201 2208 def _getbundleobsmarkerpart(bundler, repo, source, bundlecaps=None,
2202 2209 b2caps=None, heads=None, **kwargs):
2203 2210 """add an obsolescence markers part to the requested bundle"""
2204 2211 if kwargs.get(r'obsmarkers', False):
2205 2212 if heads is None:
2206 2213 heads = repo.heads()
2207 2214 subset = [c.node() for c in repo.set('::%ln', heads)]
2208 2215 markers = repo.obsstore.relevantmarkers(subset)
2209 2216 markers = sorted(markers)
2210 2217 bundle2.buildobsmarkerspart(bundler, markers)
2211 2218
2212 2219 @getbundle2partsgenerator('phases')
2213 2220 def _getbundlephasespart(bundler, repo, source, bundlecaps=None,
2214 2221 b2caps=None, heads=None, **kwargs):
2215 2222 """add phase heads part to the requested bundle"""
2216 2223 if kwargs.get(r'phases', False):
2217 2224 if not 'heads' in b2caps.get('phases'):
2218 2225 raise ValueError(_('no common phases exchange method'))
2219 2226 if heads is None:
2220 2227 heads = repo.heads()
2221 2228
2222 2229 headsbyphase = collections.defaultdict(set)
2223 2230 if repo.publishing():
2224 2231 headsbyphase[phases.public] = heads
2225 2232 else:
2226 2233 # find the appropriate heads to move
2227 2234
2228 2235 phase = repo._phasecache.phase
2229 2236 node = repo.changelog.node
2230 2237 rev = repo.changelog.rev
2231 2238 for h in heads:
2232 2239 headsbyphase[phase(repo, rev(h))].add(h)
2233 2240 seenphases = list(headsbyphase.keys())
2234 2241
2235 2242 # We do not handle anything but public and draft phase for now)
2236 2243 if seenphases:
2237 2244 assert max(seenphases) <= phases.draft
2238 2245
2239 2246 # if client is pulling non-public changesets, we need to find
2240 2247 # intermediate public heads.
2241 2248 draftheads = headsbyphase.get(phases.draft, set())
2242 2249 if draftheads:
2243 2250 publicheads = headsbyphase.get(phases.public, set())
2244 2251
2245 2252 revset = 'heads(only(%ln, %ln) and public())'
2246 2253 extraheads = repo.revs(revset, draftheads, publicheads)
2247 2254 for r in extraheads:
2248 2255 headsbyphase[phases.public].add(node(r))
2249 2256
2250 2257 # transform data in a format used by the encoding function
2251 2258 phasemapping = []
2252 2259 for phase in phases.allphases:
2253 2260 phasemapping.append(sorted(headsbyphase[phase]))
2254 2261
2255 2262 # generate the actual part
2256 2263 phasedata = phases.binaryencode(phasemapping)
2257 2264 bundler.newpart('phase-heads', data=phasedata)
2258 2265
2259 2266 @getbundle2partsgenerator('hgtagsfnodes')
2260 2267 def _getbundletagsfnodes(bundler, repo, source, bundlecaps=None,
2261 2268 b2caps=None, heads=None, common=None,
2262 2269 **kwargs):
2263 2270 """Transfer the .hgtags filenodes mapping.
2264 2271
2265 2272 Only values for heads in this bundle will be transferred.
2266 2273
2267 2274 The part data consists of pairs of 20 byte changeset node and .hgtags
2268 2275 filenodes raw values.
2269 2276 """
2270 2277 # Don't send unless:
2271 2278 # - changeset are being exchanged,
2272 2279 # - the client supports it.
2273 2280 if not (kwargs.get(r'cg', True) and 'hgtagsfnodes' in b2caps):
2274 2281 return
2275 2282
2276 2283 outgoing = _computeoutgoing(repo, heads, common)
2277 2284 bundle2.addparttagsfnodescache(repo, bundler, outgoing)
2278 2285
2279 2286 @getbundle2partsgenerator('cache:rev-branch-cache')
2280 2287 def _getbundlerevbranchcache(bundler, repo, source, bundlecaps=None,
2281 2288 b2caps=None, heads=None, common=None,
2282 2289 **kwargs):
2283 2290 """Transfer the rev-branch-cache mapping
2284 2291
2285 2292 The payload is a series of data related to each branch
2286 2293
2287 2294 1) branch name length
2288 2295 2) number of open heads
2289 2296 3) number of closed heads
2290 2297 4) open heads nodes
2291 2298 5) closed heads nodes
2292 2299 """
2293 2300 # Don't send unless:
2294 2301 # - changeset are being exchanged,
2295 2302 # - the client supports it.
2296 2303 # - narrow bundle isn't in play (not currently compatible).
2297 2304 if (not kwargs.get(r'cg', True)
2298 2305 or 'rev-branch-cache' not in b2caps
2299 2306 or kwargs.get(r'narrow', False)
2300 2307 or repo.ui.has_section(_NARROWACL_SECTION)):
2301 2308 return
2302 2309
2303 2310 outgoing = _computeoutgoing(repo, heads, common)
2304 2311 bundle2.addpartrevbranchcache(repo, bundler, outgoing)
2305 2312
2306 2313 def check_heads(repo, their_heads, context):
2307 2314 """check if the heads of a repo have been modified
2308 2315
2309 2316 Used by peer for unbundling.
2310 2317 """
2311 2318 heads = repo.heads()
2312 2319 heads_hash = hashlib.sha1(''.join(sorted(heads))).digest()
2313 2320 if not (their_heads == ['force'] or their_heads == heads or
2314 2321 their_heads == ['hashed', heads_hash]):
2315 2322 # someone else committed/pushed/unbundled while we
2316 2323 # were transferring data
2317 2324 raise error.PushRaced('repository changed while %s - '
2318 2325 'please try again' % context)
2319 2326
2320 2327 def unbundle(repo, cg, heads, source, url):
2321 2328 """Apply a bundle to a repo.
2322 2329
2323 2330 this function makes sure the repo is locked during the application and have
2324 2331 mechanism to check that no push race occurred between the creation of the
2325 2332 bundle and its application.
2326 2333
2327 2334 If the push was raced as PushRaced exception is raised."""
2328 2335 r = 0
2329 2336 # need a transaction when processing a bundle2 stream
2330 2337 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
2331 2338 lockandtr = [None, None, None]
2332 2339 recordout = None
2333 2340 # quick fix for output mismatch with bundle2 in 3.4
2334 2341 captureoutput = repo.ui.configbool('experimental', 'bundle2-output-capture')
2335 2342 if url.startswith('remote:http:') or url.startswith('remote:https:'):
2336 2343 captureoutput = True
2337 2344 try:
2338 2345 # note: outside bundle1, 'heads' is expected to be empty and this
2339 2346 # 'check_heads' call wil be a no-op
2340 2347 check_heads(repo, heads, 'uploading changes')
2341 2348 # push can proceed
2342 2349 if not isinstance(cg, bundle2.unbundle20):
2343 2350 # legacy case: bundle1 (changegroup 01)
2344 2351 txnname = "\n".join([source, util.hidepassword(url)])
2345 2352 with repo.lock(), repo.transaction(txnname) as tr:
2346 2353 op = bundle2.applybundle(repo, cg, tr, source, url)
2347 2354 r = bundle2.combinechangegroupresults(op)
2348 2355 else:
2349 2356 r = None
2350 2357 try:
2351 2358 def gettransaction():
2352 2359 if not lockandtr[2]:
2353 2360 lockandtr[0] = repo.wlock()
2354 2361 lockandtr[1] = repo.lock()
2355 2362 lockandtr[2] = repo.transaction(source)
2356 2363 lockandtr[2].hookargs['source'] = source
2357 2364 lockandtr[2].hookargs['url'] = url
2358 2365 lockandtr[2].hookargs['bundle2'] = '1'
2359 2366 return lockandtr[2]
2360 2367
2361 2368 # Do greedy locking by default until we're satisfied with lazy
2362 2369 # locking.
2363 2370 if not repo.ui.configbool('experimental', 'bundle2lazylocking'):
2364 2371 gettransaction()
2365 2372
2366 2373 op = bundle2.bundleoperation(repo, gettransaction,
2367 2374 captureoutput=captureoutput,
2368 2375 source='push')
2369 2376 try:
2370 2377 op = bundle2.processbundle(repo, cg, op=op)
2371 2378 finally:
2372 2379 r = op.reply
2373 2380 if captureoutput and r is not None:
2374 2381 repo.ui.pushbuffer(error=True, subproc=True)
2375 2382 def recordout(output):
2376 2383 r.newpart('output', data=output, mandatory=False)
2377 2384 if lockandtr[2] is not None:
2378 2385 lockandtr[2].close()
2379 2386 except BaseException as exc:
2380 2387 exc.duringunbundle2 = True
2381 2388 if captureoutput and r is not None:
2382 2389 parts = exc._bundle2salvagedoutput = r.salvageoutput()
2383 2390 def recordout(output):
2384 2391 part = bundle2.bundlepart('output', data=output,
2385 2392 mandatory=False)
2386 2393 parts.append(part)
2387 2394 raise
2388 2395 finally:
2389 2396 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
2390 2397 if recordout is not None:
2391 2398 recordout(repo.ui.popbuffer())
2392 2399 return r
2393 2400
2394 2401 def _maybeapplyclonebundle(pullop):
2395 2402 """Apply a clone bundle from a remote, if possible."""
2396 2403
2397 2404 repo = pullop.repo
2398 2405 remote = pullop.remote
2399 2406
2400 2407 if not repo.ui.configbool('ui', 'clonebundles'):
2401 2408 return
2402 2409
2403 2410 # Only run if local repo is empty.
2404 2411 if len(repo):
2405 2412 return
2406 2413
2407 2414 if pullop.heads:
2408 2415 return
2409 2416
2410 2417 if not remote.capable('clonebundles'):
2411 2418 return
2412 2419
2413 2420 with remote.commandexecutor() as e:
2414 2421 res = e.callcommand('clonebundles', {}).result()
2415 2422
2416 2423 # If we call the wire protocol command, that's good enough to record the
2417 2424 # attempt.
2418 2425 pullop.clonebundleattempted = True
2419 2426
2420 2427 entries = parseclonebundlesmanifest(repo, res)
2421 2428 if not entries:
2422 2429 repo.ui.note(_('no clone bundles available on remote; '
2423 2430 'falling back to regular clone\n'))
2424 2431 return
2425 2432
2426 2433 entries = filterclonebundleentries(
2427 2434 repo, entries, streamclonerequested=pullop.streamclonerequested)
2428 2435
2429 2436 if not entries:
2430 2437 # There is a thundering herd concern here. However, if a server
2431 2438 # operator doesn't advertise bundles appropriate for its clients,
2432 2439 # they deserve what's coming. Furthermore, from a client's
2433 2440 # perspective, no automatic fallback would mean not being able to
2434 2441 # clone!
2435 2442 repo.ui.warn(_('no compatible clone bundles available on server; '
2436 2443 'falling back to regular clone\n'))
2437 2444 repo.ui.warn(_('(you may want to report this to the server '
2438 2445 'operator)\n'))
2439 2446 return
2440 2447
2441 2448 entries = sortclonebundleentries(repo.ui, entries)
2442 2449
2443 2450 url = entries[0]['URL']
2444 2451 repo.ui.status(_('applying clone bundle from %s\n') % url)
2445 2452 if trypullbundlefromurl(repo.ui, repo, url):
2446 2453 repo.ui.status(_('finished applying clone bundle\n'))
2447 2454 # Bundle failed.
2448 2455 #
2449 2456 # We abort by default to avoid the thundering herd of
2450 2457 # clients flooding a server that was expecting expensive
2451 2458 # clone load to be offloaded.
2452 2459 elif repo.ui.configbool('ui', 'clonebundlefallback'):
2453 2460 repo.ui.warn(_('falling back to normal clone\n'))
2454 2461 else:
2455 2462 raise error.Abort(_('error applying bundle'),
2456 2463 hint=_('if this error persists, consider contacting '
2457 2464 'the server operator or disable clone '
2458 2465 'bundles via '
2459 2466 '"--config ui.clonebundles=false"'))
2460 2467
2461 2468 def parseclonebundlesmanifest(repo, s):
2462 2469 """Parses the raw text of a clone bundles manifest.
2463 2470
2464 2471 Returns a list of dicts. The dicts have a ``URL`` key corresponding
2465 2472 to the URL and other keys are the attributes for the entry.
2466 2473 """
2467 2474 m = []
2468 2475 for line in s.splitlines():
2469 2476 fields = line.split()
2470 2477 if not fields:
2471 2478 continue
2472 2479 attrs = {'URL': fields[0]}
2473 2480 for rawattr in fields[1:]:
2474 2481 key, value = rawattr.split('=', 1)
2475 2482 key = urlreq.unquote(key)
2476 2483 value = urlreq.unquote(value)
2477 2484 attrs[key] = value
2478 2485
2479 2486 # Parse BUNDLESPEC into components. This makes client-side
2480 2487 # preferences easier to specify since you can prefer a single
2481 2488 # component of the BUNDLESPEC.
2482 2489 if key == 'BUNDLESPEC':
2483 2490 try:
2484 2491 bundlespec = parsebundlespec(repo, value)
2485 2492 attrs['COMPRESSION'] = bundlespec.compression
2486 2493 attrs['VERSION'] = bundlespec.version
2487 2494 except error.InvalidBundleSpecification:
2488 2495 pass
2489 2496 except error.UnsupportedBundleSpecification:
2490 2497 pass
2491 2498
2492 2499 m.append(attrs)
2493 2500
2494 2501 return m
2495 2502
2496 2503 def isstreamclonespec(bundlespec):
2497 2504 # Stream clone v1
2498 2505 if (bundlespec.wirecompression == 'UN' and bundlespec.wireversion == 's1'):
2499 2506 return True
2500 2507
2501 2508 # Stream clone v2
2502 2509 if (bundlespec.wirecompression == 'UN' and \
2503 2510 bundlespec.wireversion == '02' and \
2504 2511 bundlespec.contentopts.get('streamv2')):
2505 2512 return True
2506 2513
2507 2514 return False
2508 2515
2509 2516 def filterclonebundleentries(repo, entries, streamclonerequested=False):
2510 2517 """Remove incompatible clone bundle manifest entries.
2511 2518
2512 2519 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
2513 2520 and returns a new list consisting of only the entries that this client
2514 2521 should be able to apply.
2515 2522
2516 2523 There is no guarantee we'll be able to apply all returned entries because
2517 2524 the metadata we use to filter on may be missing or wrong.
2518 2525 """
2519 2526 newentries = []
2520 2527 for entry in entries:
2521 2528 spec = entry.get('BUNDLESPEC')
2522 2529 if spec:
2523 2530 try:
2524 2531 bundlespec = parsebundlespec(repo, spec, strict=True)
2525 2532
2526 2533 # If a stream clone was requested, filter out non-streamclone
2527 2534 # entries.
2528 2535 if streamclonerequested and not isstreamclonespec(bundlespec):
2529 2536 repo.ui.debug('filtering %s because not a stream clone\n' %
2530 2537 entry['URL'])
2531 2538 continue
2532 2539
2533 2540 except error.InvalidBundleSpecification as e:
2534 2541 repo.ui.debug(stringutil.forcebytestr(e) + '\n')
2535 2542 continue
2536 2543 except error.UnsupportedBundleSpecification as e:
2537 2544 repo.ui.debug('filtering %s because unsupported bundle '
2538 2545 'spec: %s\n' % (
2539 2546 entry['URL'], stringutil.forcebytestr(e)))
2540 2547 continue
2541 2548 # If we don't have a spec and requested a stream clone, we don't know
2542 2549 # what the entry is so don't attempt to apply it.
2543 2550 elif streamclonerequested:
2544 2551 repo.ui.debug('filtering %s because cannot determine if a stream '
2545 2552 'clone bundle\n' % entry['URL'])
2546 2553 continue
2547 2554
2548 2555 if 'REQUIRESNI' in entry and not sslutil.hassni:
2549 2556 repo.ui.debug('filtering %s because SNI not supported\n' %
2550 2557 entry['URL'])
2551 2558 continue
2552 2559
2553 2560 newentries.append(entry)
2554 2561
2555 2562 return newentries
2556 2563
2557 2564 class clonebundleentry(object):
2558 2565 """Represents an item in a clone bundles manifest.
2559 2566
2560 2567 This rich class is needed to support sorting since sorted() in Python 3
2561 2568 doesn't support ``cmp`` and our comparison is complex enough that ``key=``
2562 2569 won't work.
2563 2570 """
2564 2571
2565 2572 def __init__(self, value, prefers):
2566 2573 self.value = value
2567 2574 self.prefers = prefers
2568 2575
2569 2576 def _cmp(self, other):
2570 2577 for prefkey, prefvalue in self.prefers:
2571 2578 avalue = self.value.get(prefkey)
2572 2579 bvalue = other.value.get(prefkey)
2573 2580
2574 2581 # Special case for b missing attribute and a matches exactly.
2575 2582 if avalue is not None and bvalue is None and avalue == prefvalue:
2576 2583 return -1
2577 2584
2578 2585 # Special case for a missing attribute and b matches exactly.
2579 2586 if bvalue is not None and avalue is None and bvalue == prefvalue:
2580 2587 return 1
2581 2588
2582 2589 # We can't compare unless attribute present on both.
2583 2590 if avalue is None or bvalue is None:
2584 2591 continue
2585 2592
2586 2593 # Same values should fall back to next attribute.
2587 2594 if avalue == bvalue:
2588 2595 continue
2589 2596
2590 2597 # Exact matches come first.
2591 2598 if avalue == prefvalue:
2592 2599 return -1
2593 2600 if bvalue == prefvalue:
2594 2601 return 1
2595 2602
2596 2603 # Fall back to next attribute.
2597 2604 continue
2598 2605
2599 2606 # If we got here we couldn't sort by attributes and prefers. Fall
2600 2607 # back to index order.
2601 2608 return 0
2602 2609
2603 2610 def __lt__(self, other):
2604 2611 return self._cmp(other) < 0
2605 2612
2606 2613 def __gt__(self, other):
2607 2614 return self._cmp(other) > 0
2608 2615
2609 2616 def __eq__(self, other):
2610 2617 return self._cmp(other) == 0
2611 2618
2612 2619 def __le__(self, other):
2613 2620 return self._cmp(other) <= 0
2614 2621
2615 2622 def __ge__(self, other):
2616 2623 return self._cmp(other) >= 0
2617 2624
2618 2625 def __ne__(self, other):
2619 2626 return self._cmp(other) != 0
2620 2627
2621 2628 def sortclonebundleentries(ui, entries):
2622 2629 prefers = ui.configlist('ui', 'clonebundleprefers')
2623 2630 if not prefers:
2624 2631 return list(entries)
2625 2632
2626 2633 prefers = [p.split('=', 1) for p in prefers]
2627 2634
2628 2635 items = sorted(clonebundleentry(v, prefers) for v in entries)
2629 2636 return [i.value for i in items]
2630 2637
2631 2638 def trypullbundlefromurl(ui, repo, url):
2632 2639 """Attempt to apply a bundle from a URL."""
2633 2640 with repo.lock(), repo.transaction('bundleurl') as tr:
2634 2641 try:
2635 2642 fh = urlmod.open(ui, url)
2636 2643 cg = readbundle(ui, fh, 'stream')
2637 2644
2638 2645 if isinstance(cg, streamclone.streamcloneapplier):
2639 2646 cg.apply(repo)
2640 2647 else:
2641 2648 bundle2.applybundle(repo, cg, tr, 'clonebundles', url)
2642 2649 return True
2643 2650 except urlerr.httperror as e:
2644 2651 ui.warn(_('HTTP error fetching bundle: %s\n') %
2645 2652 stringutil.forcebytestr(e))
2646 2653 except urlerr.urlerror as e:
2647 2654 ui.warn(_('error fetching bundle: %s\n') %
2648 2655 stringutil.forcebytestr(e.reason))
2649 2656
2650 2657 return False
@@ -1,1225 +1,1226 b''
1 1 # hg.py - repository classes for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import errno
12 12 import hashlib
13 13 import os
14 14 import shutil
15 15 import stat
16 16
17 17 from .i18n import _
18 18 from .node import (
19 19 nullid,
20 20 )
21 21
22 22 from . import (
23 23 bookmarks,
24 24 bundlerepo,
25 25 cacheutil,
26 26 cmdutil,
27 27 destutil,
28 28 discovery,
29 29 error,
30 30 exchange,
31 31 extensions,
32 32 httppeer,
33 33 localrepo,
34 34 lock,
35 35 logcmdutil,
36 36 logexchange,
37 37 merge as mergemod,
38 38 narrowspec,
39 39 node,
40 40 phases,
41 41 scmutil,
42 42 sshpeer,
43 43 statichttprepo,
44 44 ui as uimod,
45 45 unionrepo,
46 46 url,
47 47 util,
48 48 verify as verifymod,
49 49 vfs as vfsmod,
50 50 )
51 51
52 52 release = lock.release
53 53
54 54 # shared features
55 55 sharedbookmarks = 'bookmarks'
56 56
57 57 def _local(path):
58 58 path = util.expandpath(util.urllocalpath(path))
59 59 return (os.path.isfile(path) and bundlerepo or localrepo)
60 60
61 61 def addbranchrevs(lrepo, other, branches, revs):
62 62 peer = other.peer() # a courtesy to callers using a localrepo for other
63 63 hashbranch, branches = branches
64 64 if not hashbranch and not branches:
65 65 x = revs or None
66 66 if revs:
67 67 y = revs[0]
68 68 else:
69 69 y = None
70 70 return x, y
71 71 if revs:
72 72 revs = list(revs)
73 73 else:
74 74 revs = []
75 75
76 76 if not peer.capable('branchmap'):
77 77 if branches:
78 78 raise error.Abort(_("remote branch lookup not supported"))
79 79 revs.append(hashbranch)
80 80 return revs, revs[0]
81 81
82 82 with peer.commandexecutor() as e:
83 83 branchmap = e.callcommand('branchmap', {}).result()
84 84
85 85 def primary(branch):
86 86 if branch == '.':
87 87 if not lrepo:
88 88 raise error.Abort(_("dirstate branch not accessible"))
89 89 branch = lrepo.dirstate.branch()
90 90 if branch in branchmap:
91 91 revs.extend(node.hex(r) for r in reversed(branchmap[branch]))
92 92 return True
93 93 else:
94 94 return False
95 95
96 96 for branch in branches:
97 97 if not primary(branch):
98 98 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
99 99 if hashbranch:
100 100 if not primary(hashbranch):
101 101 revs.append(hashbranch)
102 102 return revs, revs[0]
103 103
104 104 def parseurl(path, branches=None):
105 105 '''parse url#branch, returning (url, (branch, branches))'''
106 106
107 107 u = util.url(path)
108 108 branch = None
109 109 if u.fragment:
110 110 branch = u.fragment
111 111 u.fragment = None
112 112 return bytes(u), (branch, branches or [])
113 113
114 114 schemes = {
115 115 'bundle': bundlerepo,
116 116 'union': unionrepo,
117 117 'file': _local,
118 118 'http': httppeer,
119 119 'https': httppeer,
120 120 'ssh': sshpeer,
121 121 'static-http': statichttprepo,
122 122 }
123 123
124 124 def _peerlookup(path):
125 125 u = util.url(path)
126 126 scheme = u.scheme or 'file'
127 127 thing = schemes.get(scheme) or schemes['file']
128 128 try:
129 129 return thing(path)
130 130 except TypeError:
131 131 # we can't test callable(thing) because 'thing' can be an unloaded
132 132 # module that implements __call__
133 133 if not util.safehasattr(thing, 'instance'):
134 134 raise
135 135 return thing
136 136
137 137 def islocal(repo):
138 138 '''return true if repo (or path pointing to repo) is local'''
139 139 if isinstance(repo, bytes):
140 140 try:
141 141 return _peerlookup(repo).islocal(repo)
142 142 except AttributeError:
143 143 return False
144 144 return repo.local()
145 145
146 146 def openpath(ui, path):
147 147 '''open path with open if local, url.open if remote'''
148 148 pathurl = util.url(path, parsequery=False, parsefragment=False)
149 149 if pathurl.islocal():
150 150 return util.posixfile(pathurl.localpath(), 'rb')
151 151 else:
152 152 return url.open(ui, path)
153 153
154 154 # a list of (ui, repo) functions called for wire peer initialization
155 155 wirepeersetupfuncs = []
156 156
157 157 def _peerorrepo(ui, path, create=False, presetupfuncs=None,
158 158 intents=None, createopts=None):
159 159 """return a repository object for the specified path"""
160 160 obj = _peerlookup(path).instance(ui, path, create, intents=intents,
161 161 createopts=createopts)
162 162 ui = getattr(obj, "ui", ui)
163 163 if ui.configbool('devel', 'debug.extensions'):
164 164 log = lambda msg, *values: ui.debug('debug.extensions: ',
165 165 msg % values, label='debug.extensions')
166 166 else:
167 167 log = lambda *a, **kw: None
168 168 for f in presetupfuncs or []:
169 169 f(ui, obj)
170 170 log('- executing reposetup hooks\n')
171 171 with util.timedcm('all reposetup') as allreposetupstats:
172 172 for name, module in extensions.extensions(ui):
173 173 log(' - running reposetup for %s\n' % (name,))
174 174 hook = getattr(module, 'reposetup', None)
175 175 if hook:
176 176 with util.timedcm('reposetup %r', name) as stats:
177 177 hook(ui, obj)
178 178 log(' > reposetup for %r took %s\n', name, stats)
179 179 log('> all reposetup took %s\n', allreposetupstats)
180 180 if not obj.local():
181 181 for f in wirepeersetupfuncs:
182 182 f(ui, obj)
183 183 return obj
184 184
185 185 def repository(ui, path='', create=False, presetupfuncs=None, intents=None,
186 186 createopts=None):
187 187 """return a repository object for the specified path"""
188 188 peer = _peerorrepo(ui, path, create, presetupfuncs=presetupfuncs,
189 189 intents=intents, createopts=createopts)
190 190 repo = peer.local()
191 191 if not repo:
192 192 raise error.Abort(_("repository '%s' is not local") %
193 193 (path or peer.url()))
194 194 return repo.filtered('visible')
195 195
196 196 def peer(uiorrepo, opts, path, create=False, intents=None, createopts=None):
197 197 '''return a repository peer for the specified path'''
198 198 rui = remoteui(uiorrepo, opts)
199 199 return _peerorrepo(rui, path, create, intents=intents,
200 200 createopts=createopts).peer()
201 201
202 202 def defaultdest(source):
203 203 '''return default destination of clone if none is given
204 204
205 205 >>> defaultdest(b'foo')
206 206 'foo'
207 207 >>> defaultdest(b'/foo/bar')
208 208 'bar'
209 209 >>> defaultdest(b'/')
210 210 ''
211 211 >>> defaultdest(b'')
212 212 ''
213 213 >>> defaultdest(b'http://example.org/')
214 214 ''
215 215 >>> defaultdest(b'http://example.org/foo/')
216 216 'foo'
217 217 '''
218 218 path = util.url(source).path
219 219 if not path:
220 220 return ''
221 221 return os.path.basename(os.path.normpath(path))
222 222
223 223 def sharedreposource(repo):
224 224 """Returns repository object for source repository of a shared repo.
225 225
226 226 If repo is not a shared repository, returns None.
227 227 """
228 228 if repo.sharedpath == repo.path:
229 229 return None
230 230
231 231 if util.safehasattr(repo, 'srcrepo') and repo.srcrepo:
232 232 return repo.srcrepo
233 233
234 234 # the sharedpath always ends in the .hg; we want the path to the repo
235 235 source = repo.vfs.split(repo.sharedpath)[0]
236 236 srcurl, branches = parseurl(source)
237 237 srcrepo = repository(repo.ui, srcurl)
238 238 repo.srcrepo = srcrepo
239 239 return srcrepo
240 240
241 241 def share(ui, source, dest=None, update=True, bookmarks=True, defaultpath=None,
242 242 relative=False):
243 243 '''create a shared repository'''
244 244
245 245 if not islocal(source):
246 246 raise error.Abort(_('can only share local repositories'))
247 247
248 248 if not dest:
249 249 dest = defaultdest(source)
250 250 else:
251 251 dest = ui.expandpath(dest)
252 252
253 253 if isinstance(source, bytes):
254 254 origsource = ui.expandpath(source)
255 255 source, branches = parseurl(origsource)
256 256 srcrepo = repository(ui, source)
257 257 rev, checkout = addbranchrevs(srcrepo, srcrepo, branches, None)
258 258 else:
259 259 srcrepo = source.local()
260 260 checkout = None
261 261
262 262 shareditems = set()
263 263 if bookmarks:
264 264 shareditems.add(sharedbookmarks)
265 265
266 266 r = repository(ui, dest, create=True, createopts={
267 267 'sharedrepo': srcrepo,
268 268 'sharedrelative': relative,
269 269 'shareditems': shareditems,
270 270 })
271 271
272 272 postshare(srcrepo, r, defaultpath=defaultpath)
273 273 _postshareupdate(r, update, checkout=checkout)
274 274 return r
275 275
276 276 def unshare(ui, repo):
277 277 """convert a shared repository to a normal one
278 278
279 279 Copy the store data to the repo and remove the sharedpath data.
280 280
281 281 Returns a new repository object representing the unshared repository.
282 282
283 283 The passed repository object is not usable after this function is
284 284 called.
285 285 """
286 286
287 287 destlock = lock = None
288 288 lock = repo.lock()
289 289 try:
290 290 # we use locks here because if we race with commit, we
291 291 # can end up with extra data in the cloned revlogs that's
292 292 # not pointed to by changesets, thus causing verify to
293 293 # fail
294 294
295 295 destlock = copystore(ui, repo, repo.path)
296 296
297 297 sharefile = repo.vfs.join('sharedpath')
298 298 util.rename(sharefile, sharefile + '.old')
299 299
300 300 repo.requirements.discard('shared')
301 301 repo.requirements.discard('relshared')
302 302 repo._writerequirements()
303 303 finally:
304 304 destlock and destlock.release()
305 305 lock and lock.release()
306 306
307 307 # Removing share changes some fundamental properties of the repo instance.
308 308 # So we instantiate a new repo object and operate on it rather than
309 309 # try to keep the existing repo usable.
310 310 newrepo = repository(repo.baseui, repo.root, create=False)
311 311
312 312 # TODO: figure out how to access subrepos that exist, but were previously
313 313 # removed from .hgsub
314 314 c = newrepo['.']
315 315 subs = c.substate
316 316 for s in sorted(subs):
317 317 c.sub(s).unshare()
318 318
319 319 localrepo.poisonrepository(repo)
320 320
321 321 return newrepo
322 322
323 323 def postshare(sourcerepo, destrepo, defaultpath=None):
324 324 """Called after a new shared repo is created.
325 325
326 326 The new repo only has a requirements file and pointer to the source.
327 327 This function configures additional shared data.
328 328
329 329 Extensions can wrap this function and write additional entries to
330 330 destrepo/.hg/shared to indicate additional pieces of data to be shared.
331 331 """
332 332 default = defaultpath or sourcerepo.ui.config('paths', 'default')
333 333 if default:
334 334 template = ('[paths]\n'
335 335 'default = %s\n')
336 336 destrepo.vfs.write('hgrc', util.tonativeeol(template % default))
337 337
338 338 def _postshareupdate(repo, update, checkout=None):
339 339 """Maybe perform a working directory update after a shared repo is created.
340 340
341 341 ``update`` can be a boolean or a revision to update to.
342 342 """
343 343 if not update:
344 344 return
345 345
346 346 repo.ui.status(_("updating working directory\n"))
347 347 if update is not True:
348 348 checkout = update
349 349 for test in (checkout, 'default', 'tip'):
350 350 if test is None:
351 351 continue
352 352 try:
353 353 uprev = repo.lookup(test)
354 354 break
355 355 except error.RepoLookupError:
356 356 continue
357 357 _update(repo, uprev)
358 358
359 359 def copystore(ui, srcrepo, destpath):
360 360 '''copy files from store of srcrepo in destpath
361 361
362 362 returns destlock
363 363 '''
364 364 destlock = None
365 365 try:
366 366 hardlink = None
367 367 topic = _('linking') if hardlink else _('copying')
368 368 with ui.makeprogress(topic) as progress:
369 369 num = 0
370 370 srcpublishing = srcrepo.publishing()
371 371 srcvfs = vfsmod.vfs(srcrepo.sharedpath)
372 372 dstvfs = vfsmod.vfs(destpath)
373 373 for f in srcrepo.store.copylist():
374 374 if srcpublishing and f.endswith('phaseroots'):
375 375 continue
376 376 dstbase = os.path.dirname(f)
377 377 if dstbase and not dstvfs.exists(dstbase):
378 378 dstvfs.mkdir(dstbase)
379 379 if srcvfs.exists(f):
380 380 if f.endswith('data'):
381 381 # 'dstbase' may be empty (e.g. revlog format 0)
382 382 lockfile = os.path.join(dstbase, "lock")
383 383 # lock to avoid premature writing to the target
384 384 destlock = lock.lock(dstvfs, lockfile)
385 385 hardlink, n = util.copyfiles(srcvfs.join(f), dstvfs.join(f),
386 386 hardlink, progress)
387 387 num += n
388 388 if hardlink:
389 389 ui.debug("linked %d files\n" % num)
390 390 else:
391 391 ui.debug("copied %d files\n" % num)
392 392 return destlock
393 393 except: # re-raises
394 394 release(destlock)
395 395 raise
396 396
397 397 def clonewithshare(ui, peeropts, sharepath, source, srcpeer, dest, pull=False,
398 398 rev=None, update=True, stream=False):
399 399 """Perform a clone using a shared repo.
400 400
401 401 The store for the repository will be located at <sharepath>/.hg. The
402 402 specified revisions will be cloned or pulled from "source". A shared repo
403 403 will be created at "dest" and a working copy will be created if "update" is
404 404 True.
405 405 """
406 406 revs = None
407 407 if rev:
408 408 if not srcpeer.capable('lookup'):
409 409 raise error.Abort(_("src repository does not support "
410 410 "revision lookup and so doesn't "
411 411 "support clone by revision"))
412 412
413 413 # TODO this is batchable.
414 414 remoterevs = []
415 415 for r in rev:
416 416 with srcpeer.commandexecutor() as e:
417 417 remoterevs.append(e.callcommand('lookup', {
418 418 'key': r,
419 419 }).result())
420 420 revs = remoterevs
421 421
422 422 # Obtain a lock before checking for or cloning the pooled repo otherwise
423 423 # 2 clients may race creating or populating it.
424 424 pooldir = os.path.dirname(sharepath)
425 425 # lock class requires the directory to exist.
426 426 try:
427 427 util.makedir(pooldir, False)
428 428 except OSError as e:
429 429 if e.errno != errno.EEXIST:
430 430 raise
431 431
432 432 poolvfs = vfsmod.vfs(pooldir)
433 433 basename = os.path.basename(sharepath)
434 434
435 435 with lock.lock(poolvfs, '%s.lock' % basename):
436 436 if os.path.exists(sharepath):
437 437 ui.status(_('(sharing from existing pooled repository %s)\n') %
438 438 basename)
439 439 else:
440 440 ui.status(_('(sharing from new pooled repository %s)\n') % basename)
441 441 # Always use pull mode because hardlinks in share mode don't work
442 442 # well. Never update because working copies aren't necessary in
443 443 # share mode.
444 444 clone(ui, peeropts, source, dest=sharepath, pull=True,
445 445 revs=rev, update=False, stream=stream)
446 446
447 447 # Resolve the value to put in [paths] section for the source.
448 448 if islocal(source):
449 449 defaultpath = os.path.abspath(util.urllocalpath(source))
450 450 else:
451 451 defaultpath = source
452 452
453 453 sharerepo = repository(ui, path=sharepath)
454 454 share(ui, sharerepo, dest=dest, update=False, bookmarks=False,
455 455 defaultpath=defaultpath)
456 456
457 457 # We need to perform a pull against the dest repo to fetch bookmarks
458 458 # and other non-store data that isn't shared by default. In the case of
459 459 # non-existing shared repo, this means we pull from the remote twice. This
460 460 # is a bit weird. But at the time it was implemented, there wasn't an easy
461 461 # way to pull just non-changegroup data.
462 462 destrepo = repository(ui, path=dest)
463 463 exchange.pull(destrepo, srcpeer, heads=revs)
464 464
465 465 _postshareupdate(destrepo, update)
466 466
467 467 return srcpeer, peer(ui, peeropts, dest)
468 468
469 469 # Recomputing branch cache might be slow on big repos,
470 470 # so just copy it
471 471 def _copycache(srcrepo, dstcachedir, fname):
472 472 """copy a cache from srcrepo to destcachedir (if it exists)"""
473 473 srcbranchcache = srcrepo.vfs.join('cache/%s' % fname)
474 474 dstbranchcache = os.path.join(dstcachedir, fname)
475 475 if os.path.exists(srcbranchcache):
476 476 if not os.path.exists(dstcachedir):
477 477 os.mkdir(dstcachedir)
478 478 util.copyfile(srcbranchcache, dstbranchcache)
479 479
480 480 def clone(ui, peeropts, source, dest=None, pull=False, revs=None,
481 481 update=True, stream=False, branch=None, shareopts=None,
482 storeincludepats=None, storeexcludepats=None):
482 storeincludepats=None, storeexcludepats=None, depth=None):
483 483 """Make a copy of an existing repository.
484 484
485 485 Create a copy of an existing repository in a new directory. The
486 486 source and destination are URLs, as passed to the repository
487 487 function. Returns a pair of repository peers, the source and
488 488 newly created destination.
489 489
490 490 The location of the source is added to the new repository's
491 491 .hg/hgrc file, as the default to be used for future pulls and
492 492 pushes.
493 493
494 494 If an exception is raised, the partly cloned/updated destination
495 495 repository will be deleted.
496 496
497 497 Arguments:
498 498
499 499 source: repository object or URL
500 500
501 501 dest: URL of destination repository to create (defaults to base
502 502 name of source repository)
503 503
504 504 pull: always pull from source repository, even in local case or if the
505 505 server prefers streaming
506 506
507 507 stream: stream raw data uncompressed from repository (fast over
508 508 LAN, slow over WAN)
509 509
510 510 revs: revision to clone up to (implies pull=True)
511 511
512 512 update: update working directory after clone completes, if
513 513 destination is local repository (True means update to default rev,
514 514 anything else is treated as a revision)
515 515
516 516 branch: branches to clone
517 517
518 518 shareopts: dict of options to control auto sharing behavior. The "pool" key
519 519 activates auto sharing mode and defines the directory for stores. The
520 520 "mode" key determines how to construct the directory name of the shared
521 521 repository. "identity" means the name is derived from the node of the first
522 522 changeset in the repository. "remote" means the name is derived from the
523 523 remote's path/URL. Defaults to "identity."
524 524
525 525 storeincludepats and storeexcludepats: sets of file patterns to include and
526 526 exclude in the repository copy, respectively. If not defined, all files
527 527 will be included (a "full" clone). Otherwise a "narrow" clone containing
528 528 only the requested files will be performed. If ``storeincludepats`` is not
529 529 defined but ``storeexcludepats`` is, ``storeincludepats`` is assumed to be
530 530 ``path:.``. If both are empty sets, no files will be cloned.
531 531 """
532 532
533 533 if isinstance(source, bytes):
534 534 origsource = ui.expandpath(source)
535 535 source, branches = parseurl(origsource, branch)
536 536 srcpeer = peer(ui, peeropts, source)
537 537 else:
538 538 srcpeer = source.peer() # in case we were called with a localrepo
539 539 branches = (None, branch or [])
540 540 origsource = source = srcpeer.url()
541 541 revs, checkout = addbranchrevs(srcpeer, srcpeer, branches, revs)
542 542
543 543 if dest is None:
544 544 dest = defaultdest(source)
545 545 if dest:
546 546 ui.status(_("destination directory: %s\n") % dest)
547 547 else:
548 548 dest = ui.expandpath(dest)
549 549
550 550 dest = util.urllocalpath(dest)
551 551 source = util.urllocalpath(source)
552 552
553 553 if not dest:
554 554 raise error.Abort(_("empty destination path is not valid"))
555 555
556 556 destvfs = vfsmod.vfs(dest, expandpath=True)
557 557 if destvfs.lexists():
558 558 if not destvfs.isdir():
559 559 raise error.Abort(_("destination '%s' already exists") % dest)
560 560 elif destvfs.listdir():
561 561 raise error.Abort(_("destination '%s' is not empty") % dest)
562 562
563 563 createopts = {}
564 564 narrow = False
565 565
566 566 if storeincludepats is not None:
567 567 narrowspec.validatepatterns(storeincludepats)
568 568 narrow = True
569 569
570 570 if storeexcludepats is not None:
571 571 narrowspec.validatepatterns(storeexcludepats)
572 572 narrow = True
573 573
574 574 if narrow:
575 575 # Include everything by default if only exclusion patterns defined.
576 576 if storeexcludepats and not storeincludepats:
577 577 storeincludepats = {'path:.'}
578 578
579 579 createopts['narrowfiles'] = True
580 580
581 581 if srcpeer.capable(b'lfs-serve'):
582 582 # Repository creation honors the config if it disabled the extension, so
583 583 # we can't just announce that lfs will be enabled. This check avoids
584 584 # saying that lfs will be enabled, and then saying it's an unknown
585 585 # feature. The lfs creation option is set in either case so that a
586 586 # requirement is added. If the extension is explicitly disabled but the
587 587 # requirement is set, the clone aborts early, before transferring any
588 588 # data.
589 589 createopts['lfs'] = True
590 590
591 591 if extensions.disabledext('lfs'):
592 592 ui.status(_('(remote is using large file support (lfs), but it is '
593 593 'explicitly disabled in the local configuration)\n'))
594 594 else:
595 595 ui.status(_('(remote is using large file support (lfs); lfs will '
596 596 'be enabled for this repository)\n'))
597 597
598 598 shareopts = shareopts or {}
599 599 sharepool = shareopts.get('pool')
600 600 sharenamemode = shareopts.get('mode')
601 601 if sharepool and islocal(dest):
602 602 sharepath = None
603 603 if sharenamemode == 'identity':
604 604 # Resolve the name from the initial changeset in the remote
605 605 # repository. This returns nullid when the remote is empty. It
606 606 # raises RepoLookupError if revision 0 is filtered or otherwise
607 607 # not available. If we fail to resolve, sharing is not enabled.
608 608 try:
609 609 with srcpeer.commandexecutor() as e:
610 610 rootnode = e.callcommand('lookup', {
611 611 'key': '0',
612 612 }).result()
613 613
614 614 if rootnode != node.nullid:
615 615 sharepath = os.path.join(sharepool, node.hex(rootnode))
616 616 else:
617 617 ui.status(_('(not using pooled storage: '
618 618 'remote appears to be empty)\n'))
619 619 except error.RepoLookupError:
620 620 ui.status(_('(not using pooled storage: '
621 621 'unable to resolve identity of remote)\n'))
622 622 elif sharenamemode == 'remote':
623 623 sharepath = os.path.join(
624 624 sharepool, node.hex(hashlib.sha1(source).digest()))
625 625 else:
626 626 raise error.Abort(_('unknown share naming mode: %s') %
627 627 sharenamemode)
628 628
629 629 # TODO this is a somewhat arbitrary restriction.
630 630 if narrow:
631 631 ui.status(_('(pooled storage not supported for narrow clones)\n'))
632 632 sharepath = None
633 633
634 634 if sharepath:
635 635 return clonewithshare(ui, peeropts, sharepath, source, srcpeer,
636 636 dest, pull=pull, rev=revs, update=update,
637 637 stream=stream)
638 638
639 639 srclock = destlock = cleandir = None
640 640 srcrepo = srcpeer.local()
641 641 try:
642 642 abspath = origsource
643 643 if islocal(origsource):
644 644 abspath = os.path.abspath(util.urllocalpath(origsource))
645 645
646 646 if islocal(dest):
647 647 cleandir = dest
648 648
649 649 copy = False
650 650 if (srcrepo and srcrepo.cancopy() and islocal(dest)
651 651 and not phases.hassecret(srcrepo)):
652 652 copy = not pull and not revs
653 653
654 654 # TODO this is a somewhat arbitrary restriction.
655 655 if narrow:
656 656 copy = False
657 657
658 658 if copy:
659 659 try:
660 660 # we use a lock here because if we race with commit, we
661 661 # can end up with extra data in the cloned revlogs that's
662 662 # not pointed to by changesets, thus causing verify to
663 663 # fail
664 664 srclock = srcrepo.lock(wait=False)
665 665 except error.LockError:
666 666 copy = False
667 667
668 668 if copy:
669 669 srcrepo.hook('preoutgoing', throw=True, source='clone')
670 670 hgdir = os.path.realpath(os.path.join(dest, ".hg"))
671 671 if not os.path.exists(dest):
672 672 util.makedirs(dest)
673 673 else:
674 674 # only clean up directories we create ourselves
675 675 cleandir = hgdir
676 676 try:
677 677 destpath = hgdir
678 678 util.makedir(destpath, notindexed=True)
679 679 except OSError as inst:
680 680 if inst.errno == errno.EEXIST:
681 681 cleandir = None
682 682 raise error.Abort(_("destination '%s' already exists")
683 683 % dest)
684 684 raise
685 685
686 686 destlock = copystore(ui, srcrepo, destpath)
687 687 # copy bookmarks over
688 688 srcbookmarks = srcrepo.vfs.join('bookmarks')
689 689 dstbookmarks = os.path.join(destpath, 'bookmarks')
690 690 if os.path.exists(srcbookmarks):
691 691 util.copyfile(srcbookmarks, dstbookmarks)
692 692
693 693 dstcachedir = os.path.join(destpath, 'cache')
694 694 for cache in cacheutil.cachetocopy(srcrepo):
695 695 _copycache(srcrepo, dstcachedir, cache)
696 696
697 697 # we need to re-init the repo after manually copying the data
698 698 # into it
699 699 destpeer = peer(srcrepo, peeropts, dest)
700 700 srcrepo.hook('outgoing', source='clone',
701 701 node=node.hex(node.nullid))
702 702 else:
703 703 try:
704 704 # only pass ui when no srcrepo
705 705 destpeer = peer(srcrepo or ui, peeropts, dest, create=True,
706 706 createopts=createopts)
707 707 except OSError as inst:
708 708 if inst.errno == errno.EEXIST:
709 709 cleandir = None
710 710 raise error.Abort(_("destination '%s' already exists")
711 711 % dest)
712 712 raise
713 713
714 714 if revs:
715 715 if not srcpeer.capable('lookup'):
716 716 raise error.Abort(_("src repository does not support "
717 717 "revision lookup and so doesn't "
718 718 "support clone by revision"))
719 719
720 720 # TODO this is batchable.
721 721 remoterevs = []
722 722 for rev in revs:
723 723 with srcpeer.commandexecutor() as e:
724 724 remoterevs.append(e.callcommand('lookup', {
725 725 'key': rev,
726 726 }).result())
727 727 revs = remoterevs
728 728
729 729 checkout = revs[0]
730 730 else:
731 731 revs = None
732 732 local = destpeer.local()
733 733 if local:
734 734 if narrow:
735 735 with local.lock():
736 736 local.setnarrowpats(storeincludepats, storeexcludepats)
737 737
738 738 u = util.url(abspath)
739 739 defaulturl = bytes(u)
740 740 local.ui.setconfig('paths', 'default', defaulturl, 'clone')
741 741 if not stream:
742 742 if pull:
743 743 stream = False
744 744 else:
745 745 stream = None
746 746 # internal config: ui.quietbookmarkmove
747 747 overrides = {('ui', 'quietbookmarkmove'): True}
748 748 with local.ui.configoverride(overrides, 'clone'):
749 749 exchange.pull(local, srcpeer, revs,
750 750 streamclonerequested=stream,
751 751 includepats=storeincludepats,
752 excludepats=storeexcludepats)
752 excludepats=storeexcludepats,
753 depth=depth)
753 754 elif srcrepo:
754 755 # TODO lift restriction once exchange.push() accepts narrow
755 756 # push.
756 757 if narrow:
757 758 raise error.Abort(_('narrow clone not available for '
758 759 'remote destinations'))
759 760
760 761 exchange.push(srcrepo, destpeer, revs=revs,
761 762 bookmarks=srcrepo._bookmarks.keys())
762 763 else:
763 764 raise error.Abort(_("clone from remote to remote not supported")
764 765 )
765 766
766 767 cleandir = None
767 768
768 769 destrepo = destpeer.local()
769 770 if destrepo:
770 771 template = uimod.samplehgrcs['cloned']
771 772 u = util.url(abspath)
772 773 u.passwd = None
773 774 defaulturl = bytes(u)
774 775 destrepo.vfs.write('hgrc', util.tonativeeol(template % defaulturl))
775 776 destrepo.ui.setconfig('paths', 'default', defaulturl, 'clone')
776 777
777 778 if ui.configbool('experimental', 'remotenames'):
778 779 logexchange.pullremotenames(destrepo, srcpeer)
779 780
780 781 if update:
781 782 if update is not True:
782 783 with srcpeer.commandexecutor() as e:
783 784 checkout = e.callcommand('lookup', {
784 785 'key': update,
785 786 }).result()
786 787
787 788 uprev = None
788 789 status = None
789 790 if checkout is not None:
790 791 # Some extensions (at least hg-git and hg-subversion) have
791 792 # a peer.lookup() implementation that returns a name instead
792 793 # of a nodeid. We work around it here until we've figured
793 794 # out a better solution.
794 795 if len(checkout) == 20 and checkout in destrepo:
795 796 uprev = checkout
796 797 elif scmutil.isrevsymbol(destrepo, checkout):
797 798 uprev = scmutil.revsymbol(destrepo, checkout).node()
798 799 else:
799 800 if update is not True:
800 801 try:
801 802 uprev = destrepo.lookup(update)
802 803 except error.RepoLookupError:
803 804 pass
804 805 if uprev is None:
805 806 try:
806 807 uprev = destrepo._bookmarks['@']
807 808 update = '@'
808 809 bn = destrepo[uprev].branch()
809 810 if bn == 'default':
810 811 status = _("updating to bookmark @\n")
811 812 else:
812 813 status = (_("updating to bookmark @ on branch %s\n")
813 814 % bn)
814 815 except KeyError:
815 816 try:
816 817 uprev = destrepo.branchtip('default')
817 818 except error.RepoLookupError:
818 819 uprev = destrepo.lookup('tip')
819 820 if not status:
820 821 bn = destrepo[uprev].branch()
821 822 status = _("updating to branch %s\n") % bn
822 823 destrepo.ui.status(status)
823 824 _update(destrepo, uprev)
824 825 if update in destrepo._bookmarks:
825 826 bookmarks.activate(destrepo, update)
826 827 finally:
827 828 release(srclock, destlock)
828 829 if cleandir is not None:
829 830 shutil.rmtree(cleandir, True)
830 831 if srcpeer is not None:
831 832 srcpeer.close()
832 833 return srcpeer, destpeer
833 834
834 835 def _showstats(repo, stats, quietempty=False):
835 836 if quietempty and stats.isempty():
836 837 return
837 838 repo.ui.status(_("%d files updated, %d files merged, "
838 839 "%d files removed, %d files unresolved\n") % (
839 840 stats.updatedcount, stats.mergedcount,
840 841 stats.removedcount, stats.unresolvedcount))
841 842
842 843 def updaterepo(repo, node, overwrite, updatecheck=None):
843 844 """Update the working directory to node.
844 845
845 846 When overwrite is set, changes are clobbered, merged else
846 847
847 848 returns stats (see pydoc mercurial.merge.applyupdates)"""
848 849 return mergemod.update(repo, node, False, overwrite,
849 850 labels=['working copy', 'destination'],
850 851 updatecheck=updatecheck)
851 852
852 853 def update(repo, node, quietempty=False, updatecheck=None):
853 854 """update the working directory to node"""
854 855 stats = updaterepo(repo, node, False, updatecheck=updatecheck)
855 856 _showstats(repo, stats, quietempty)
856 857 if stats.unresolvedcount:
857 858 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
858 859 return stats.unresolvedcount > 0
859 860
860 861 # naming conflict in clone()
861 862 _update = update
862 863
863 864 def clean(repo, node, show_stats=True, quietempty=False):
864 865 """forcibly switch the working directory to node, clobbering changes"""
865 866 stats = updaterepo(repo, node, True)
866 867 repo.vfs.unlinkpath('graftstate', ignoremissing=True)
867 868 if show_stats:
868 869 _showstats(repo, stats, quietempty)
869 870 return stats.unresolvedcount > 0
870 871
871 872 # naming conflict in updatetotally()
872 873 _clean = clean
873 874
874 875 def updatetotally(ui, repo, checkout, brev, clean=False, updatecheck=None):
875 876 """Update the working directory with extra care for non-file components
876 877
877 878 This takes care of non-file components below:
878 879
879 880 :bookmark: might be advanced or (in)activated
880 881
881 882 This takes arguments below:
882 883
883 884 :checkout: to which revision the working directory is updated
884 885 :brev: a name, which might be a bookmark to be activated after updating
885 886 :clean: whether changes in the working directory can be discarded
886 887 :updatecheck: how to deal with a dirty working directory
887 888
888 889 Valid values for updatecheck are (None => linear):
889 890
890 891 * abort: abort if the working directory is dirty
891 892 * none: don't check (merge working directory changes into destination)
892 893 * linear: check that update is linear before merging working directory
893 894 changes into destination
894 895 * noconflict: check that the update does not result in file merges
895 896
896 897 This returns whether conflict is detected at updating or not.
897 898 """
898 899 if updatecheck is None:
899 900 updatecheck = ui.config('commands', 'update.check')
900 901 if updatecheck not in ('abort', 'none', 'linear', 'noconflict'):
901 902 # If not configured, or invalid value configured
902 903 updatecheck = 'linear'
903 904 with repo.wlock():
904 905 movemarkfrom = None
905 906 warndest = False
906 907 if checkout is None:
907 908 updata = destutil.destupdate(repo, clean=clean)
908 909 checkout, movemarkfrom, brev = updata
909 910 warndest = True
910 911
911 912 if clean:
912 913 ret = _clean(repo, checkout)
913 914 else:
914 915 if updatecheck == 'abort':
915 916 cmdutil.bailifchanged(repo, merge=False)
916 917 updatecheck = 'none'
917 918 ret = _update(repo, checkout, updatecheck=updatecheck)
918 919
919 920 if not ret and movemarkfrom:
920 921 if movemarkfrom == repo['.'].node():
921 922 pass # no-op update
922 923 elif bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
923 924 b = ui.label(repo._activebookmark, 'bookmarks.active')
924 925 ui.status(_("updating bookmark %s\n") % b)
925 926 else:
926 927 # this can happen with a non-linear update
927 928 b = ui.label(repo._activebookmark, 'bookmarks')
928 929 ui.status(_("(leaving bookmark %s)\n") % b)
929 930 bookmarks.deactivate(repo)
930 931 elif brev in repo._bookmarks:
931 932 if brev != repo._activebookmark:
932 933 b = ui.label(brev, 'bookmarks.active')
933 934 ui.status(_("(activating bookmark %s)\n") % b)
934 935 bookmarks.activate(repo, brev)
935 936 elif brev:
936 937 if repo._activebookmark:
937 938 b = ui.label(repo._activebookmark, 'bookmarks')
938 939 ui.status(_("(leaving bookmark %s)\n") % b)
939 940 bookmarks.deactivate(repo)
940 941
941 942 if warndest:
942 943 destutil.statusotherdests(ui, repo)
943 944
944 945 return ret
945 946
946 947 def merge(repo, node, force=None, remind=True, mergeforce=False, labels=None,
947 948 abort=False):
948 949 """Branch merge with node, resolving changes. Return true if any
949 950 unresolved conflicts."""
950 951 if not abort:
951 952 stats = mergemod.update(repo, node, True, force, mergeforce=mergeforce,
952 953 labels=labels)
953 954 else:
954 955 ms = mergemod.mergestate.read(repo)
955 956 if ms.active():
956 957 # there were conflicts
957 958 node = ms.localctx.hex()
958 959 else:
959 960 # there were no conficts, mergestate was not stored
960 961 node = repo['.'].hex()
961 962
962 963 repo.ui.status(_("aborting the merge, updating back to"
963 964 " %s\n") % node[:12])
964 965 stats = mergemod.update(repo, node, branchmerge=False, force=True,
965 966 labels=labels)
966 967
967 968 _showstats(repo, stats)
968 969 if stats.unresolvedcount:
969 970 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
970 971 "or 'hg merge --abort' to abandon\n"))
971 972 elif remind and not abort:
972 973 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
973 974 return stats.unresolvedcount > 0
974 975
975 976 def _incoming(displaychlist, subreporecurse, ui, repo, source,
976 977 opts, buffered=False):
977 978 """
978 979 Helper for incoming / gincoming.
979 980 displaychlist gets called with
980 981 (remoterepo, incomingchangesetlist, displayer) parameters,
981 982 and is supposed to contain only code that can't be unified.
982 983 """
983 984 source, branches = parseurl(ui.expandpath(source), opts.get('branch'))
984 985 other = peer(repo, opts, source)
985 986 ui.status(_('comparing with %s\n') % util.hidepassword(source))
986 987 revs, checkout = addbranchrevs(repo, other, branches, opts.get('rev'))
987 988
988 989 if revs:
989 990 revs = [other.lookup(rev) for rev in revs]
990 991 other, chlist, cleanupfn = bundlerepo.getremotechanges(ui, repo, other,
991 992 revs, opts["bundle"], opts["force"])
992 993 try:
993 994 if not chlist:
994 995 ui.status(_("no changes found\n"))
995 996 return subreporecurse()
996 997 ui.pager('incoming')
997 998 displayer = logcmdutil.changesetdisplayer(ui, other, opts,
998 999 buffered=buffered)
999 1000 displaychlist(other, chlist, displayer)
1000 1001 displayer.close()
1001 1002 finally:
1002 1003 cleanupfn()
1003 1004 subreporecurse()
1004 1005 return 0 # exit code is zero since we found incoming changes
1005 1006
1006 1007 def incoming(ui, repo, source, opts):
1007 1008 def subreporecurse():
1008 1009 ret = 1
1009 1010 if opts.get('subrepos'):
1010 1011 ctx = repo[None]
1011 1012 for subpath in sorted(ctx.substate):
1012 1013 sub = ctx.sub(subpath)
1013 1014 ret = min(ret, sub.incoming(ui, source, opts))
1014 1015 return ret
1015 1016
1016 1017 def display(other, chlist, displayer):
1017 1018 limit = logcmdutil.getlimit(opts)
1018 1019 if opts.get('newest_first'):
1019 1020 chlist.reverse()
1020 1021 count = 0
1021 1022 for n in chlist:
1022 1023 if limit is not None and count >= limit:
1023 1024 break
1024 1025 parents = [p for p in other.changelog.parents(n) if p != nullid]
1025 1026 if opts.get('no_merges') and len(parents) == 2:
1026 1027 continue
1027 1028 count += 1
1028 1029 displayer.show(other[n])
1029 1030 return _incoming(display, subreporecurse, ui, repo, source, opts)
1030 1031
1031 1032 def _outgoing(ui, repo, dest, opts):
1032 1033 path = ui.paths.getpath(dest, default=('default-push', 'default'))
1033 1034 if not path:
1034 1035 raise error.Abort(_('default repository not configured!'),
1035 1036 hint=_("see 'hg help config.paths'"))
1036 1037 dest = path.pushloc or path.loc
1037 1038 branches = path.branch, opts.get('branch') or []
1038 1039
1039 1040 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
1040 1041 revs, checkout = addbranchrevs(repo, repo, branches, opts.get('rev'))
1041 1042 if revs:
1042 1043 revs = [repo[rev].node() for rev in scmutil.revrange(repo, revs)]
1043 1044
1044 1045 other = peer(repo, opts, dest)
1045 1046 outgoing = discovery.findcommonoutgoing(repo, other, revs,
1046 1047 force=opts.get('force'))
1047 1048 o = outgoing.missing
1048 1049 if not o:
1049 1050 scmutil.nochangesfound(repo.ui, repo, outgoing.excluded)
1050 1051 return o, other
1051 1052
1052 1053 def outgoing(ui, repo, dest, opts):
1053 1054 def recurse():
1054 1055 ret = 1
1055 1056 if opts.get('subrepos'):
1056 1057 ctx = repo[None]
1057 1058 for subpath in sorted(ctx.substate):
1058 1059 sub = ctx.sub(subpath)
1059 1060 ret = min(ret, sub.outgoing(ui, dest, opts))
1060 1061 return ret
1061 1062
1062 1063 limit = logcmdutil.getlimit(opts)
1063 1064 o, other = _outgoing(ui, repo, dest, opts)
1064 1065 if not o:
1065 1066 cmdutil.outgoinghooks(ui, repo, other, opts, o)
1066 1067 return recurse()
1067 1068
1068 1069 if opts.get('newest_first'):
1069 1070 o.reverse()
1070 1071 ui.pager('outgoing')
1071 1072 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
1072 1073 count = 0
1073 1074 for n in o:
1074 1075 if limit is not None and count >= limit:
1075 1076 break
1076 1077 parents = [p for p in repo.changelog.parents(n) if p != nullid]
1077 1078 if opts.get('no_merges') and len(parents) == 2:
1078 1079 continue
1079 1080 count += 1
1080 1081 displayer.show(repo[n])
1081 1082 displayer.close()
1082 1083 cmdutil.outgoinghooks(ui, repo, other, opts, o)
1083 1084 recurse()
1084 1085 return 0 # exit code is zero since we found outgoing changes
1085 1086
1086 1087 def verify(repo):
1087 1088 """verify the consistency of a repository"""
1088 1089 ret = verifymod.verify(repo)
1089 1090
1090 1091 # Broken subrepo references in hidden csets don't seem worth worrying about,
1091 1092 # since they can't be pushed/pulled, and --hidden can be used if they are a
1092 1093 # concern.
1093 1094
1094 1095 # pathto() is needed for -R case
1095 1096 revs = repo.revs("filelog(%s)",
1096 1097 util.pathto(repo.root, repo.getcwd(), '.hgsubstate'))
1097 1098
1098 1099 if revs:
1099 1100 repo.ui.status(_('checking subrepo links\n'))
1100 1101 for rev in revs:
1101 1102 ctx = repo[rev]
1102 1103 try:
1103 1104 for subpath in ctx.substate:
1104 1105 try:
1105 1106 ret = (ctx.sub(subpath, allowcreate=False).verify()
1106 1107 or ret)
1107 1108 except error.RepoError as e:
1108 1109 repo.ui.warn(('%d: %s\n') % (rev, e))
1109 1110 except Exception:
1110 1111 repo.ui.warn(_('.hgsubstate is corrupt in revision %s\n') %
1111 1112 node.short(ctx.node()))
1112 1113
1113 1114 return ret
1114 1115
1115 1116 def remoteui(src, opts):
1116 1117 'build a remote ui from ui or repo and opts'
1117 1118 if util.safehasattr(src, 'baseui'): # looks like a repository
1118 1119 dst = src.baseui.copy() # drop repo-specific config
1119 1120 src = src.ui # copy target options from repo
1120 1121 else: # assume it's a global ui object
1121 1122 dst = src.copy() # keep all global options
1122 1123
1123 1124 # copy ssh-specific options
1124 1125 for o in 'ssh', 'remotecmd':
1125 1126 v = opts.get(o) or src.config('ui', o)
1126 1127 if v:
1127 1128 dst.setconfig("ui", o, v, 'copied')
1128 1129
1129 1130 # copy bundle-specific options
1130 1131 r = src.config('bundle', 'mainreporoot')
1131 1132 if r:
1132 1133 dst.setconfig('bundle', 'mainreporoot', r, 'copied')
1133 1134
1134 1135 # copy selected local settings to the remote ui
1135 1136 for sect in ('auth', 'hostfingerprints', 'hostsecurity', 'http_proxy'):
1136 1137 for key, val in src.configitems(sect):
1137 1138 dst.setconfig(sect, key, val, 'copied')
1138 1139 v = src.config('web', 'cacerts')
1139 1140 if v:
1140 1141 dst.setconfig('web', 'cacerts', util.expandpath(v), 'copied')
1141 1142
1142 1143 return dst
1143 1144
1144 1145 # Files of interest
1145 1146 # Used to check if the repository has changed looking at mtime and size of
1146 1147 # these files.
1147 1148 foi = [('spath', '00changelog.i'),
1148 1149 ('spath', 'phaseroots'), # ! phase can change content at the same size
1149 1150 ('spath', 'obsstore'),
1150 1151 ('path', 'bookmarks'), # ! bookmark can change content at the same size
1151 1152 ]
1152 1153
1153 1154 class cachedlocalrepo(object):
1154 1155 """Holds a localrepository that can be cached and reused."""
1155 1156
1156 1157 def __init__(self, repo):
1157 1158 """Create a new cached repo from an existing repo.
1158 1159
1159 1160 We assume the passed in repo was recently created. If the
1160 1161 repo has changed between when it was created and when it was
1161 1162 turned into a cache, it may not refresh properly.
1162 1163 """
1163 1164 assert isinstance(repo, localrepo.localrepository)
1164 1165 self._repo = repo
1165 1166 self._state, self.mtime = self._repostate()
1166 1167 self._filtername = repo.filtername
1167 1168
1168 1169 def fetch(self):
1169 1170 """Refresh (if necessary) and return a repository.
1170 1171
1171 1172 If the cached instance is out of date, it will be recreated
1172 1173 automatically and returned.
1173 1174
1174 1175 Returns a tuple of the repo and a boolean indicating whether a new
1175 1176 repo instance was created.
1176 1177 """
1177 1178 # We compare the mtimes and sizes of some well-known files to
1178 1179 # determine if the repo changed. This is not precise, as mtimes
1179 1180 # are susceptible to clock skew and imprecise filesystems and
1180 1181 # file content can change while maintaining the same size.
1181 1182
1182 1183 state, mtime = self._repostate()
1183 1184 if state == self._state:
1184 1185 return self._repo, False
1185 1186
1186 1187 repo = repository(self._repo.baseui, self._repo.url())
1187 1188 if self._filtername:
1188 1189 self._repo = repo.filtered(self._filtername)
1189 1190 else:
1190 1191 self._repo = repo.unfiltered()
1191 1192 self._state = state
1192 1193 self.mtime = mtime
1193 1194
1194 1195 return self._repo, True
1195 1196
1196 1197 def _repostate(self):
1197 1198 state = []
1198 1199 maxmtime = -1
1199 1200 for attr, fname in foi:
1200 1201 prefix = getattr(self._repo, attr)
1201 1202 p = os.path.join(prefix, fname)
1202 1203 try:
1203 1204 st = os.stat(p)
1204 1205 except OSError:
1205 1206 st = os.stat(prefix)
1206 1207 state.append((st[stat.ST_MTIME], st.st_size))
1207 1208 maxmtime = max(maxmtime, st[stat.ST_MTIME])
1208 1209
1209 1210 return tuple(state), maxmtime
1210 1211
1211 1212 def copy(self):
1212 1213 """Obtain a copy of this class instance.
1213 1214
1214 1215 A new localrepository instance is obtained. The new instance should be
1215 1216 completely independent of the original.
1216 1217 """
1217 1218 repo = repository(self._repo.baseui, self._repo.origroot)
1218 1219 if self._filtername:
1219 1220 repo = repo.filtered(self._filtername)
1220 1221 else:
1221 1222 repo = repo.unfiltered()
1222 1223 c = cachedlocalrepo(repo)
1223 1224 c._state = self._state
1224 1225 c.mtime = self.mtime
1225 1226 return c
General Comments 0
You need to be logged in to leave comments. Login now