##// END OF EJS Templates
pullbundles: use unfiltered repo for head/base matching...
Joerg Sonnenberger -
r45305:3d5fb6ca stable
parent child Browse files
Show More
@@ -1,748 +1,748 b''
1 1 # wireprotov1server.py - Wire protocol version 1 server functionality
2 2 #
3 3 # Copyright 2005-2010 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 binascii
11 11 import os
12 12
13 13 from .i18n import _
14 14 from .node import (
15 15 hex,
16 16 nullid,
17 17 )
18 18 from .pycompat import getattr
19 19
20 20 from . import (
21 21 bundle2,
22 22 changegroup as changegroupmod,
23 23 discovery,
24 24 encoding,
25 25 error,
26 26 exchange,
27 27 pushkey as pushkeymod,
28 28 pycompat,
29 29 streamclone,
30 30 util,
31 31 wireprototypes,
32 32 )
33 33
34 34 from .utils import (
35 35 procutil,
36 36 stringutil,
37 37 )
38 38
39 39 urlerr = util.urlerr
40 40 urlreq = util.urlreq
41 41
42 42 bundle2requiredmain = _(b'incompatible Mercurial client; bundle2 required')
43 43 bundle2requiredhint = _(
44 44 b'see https://www.mercurial-scm.org/wiki/IncompatibleClient'
45 45 )
46 46 bundle2required = b'%s\n(%s)\n' % (bundle2requiredmain, bundle2requiredhint)
47 47
48 48
49 49 def clientcompressionsupport(proto):
50 50 """Returns a list of compression methods supported by the client.
51 51
52 52 Returns a list of the compression methods supported by the client
53 53 according to the protocol capabilities. If no such capability has
54 54 been announced, fallback to the default of zlib and uncompressed.
55 55 """
56 56 for cap in proto.getprotocaps():
57 57 if cap.startswith(b'comp='):
58 58 return cap[5:].split(b',')
59 59 return [b'zlib', b'none']
60 60
61 61
62 62 # wire protocol command can either return a string or one of these classes.
63 63
64 64
65 65 def getdispatchrepo(repo, proto, command):
66 66 """Obtain the repo used for processing wire protocol commands.
67 67
68 68 The intent of this function is to serve as a monkeypatch point for
69 69 extensions that need commands to operate on different repo views under
70 70 specialized circumstances.
71 71 """
72 72 viewconfig = repo.ui.config(b'server', b'view')
73 73 return repo.filtered(viewconfig)
74 74
75 75
76 76 def dispatch(repo, proto, command):
77 77 repo = getdispatchrepo(repo, proto, command)
78 78
79 79 func, spec = commands[command]
80 80 args = proto.getargs(spec)
81 81
82 82 return func(repo, proto, *args)
83 83
84 84
85 85 def options(cmd, keys, others):
86 86 opts = {}
87 87 for k in keys:
88 88 if k in others:
89 89 opts[k] = others[k]
90 90 del others[k]
91 91 if others:
92 92 procutil.stderr.write(
93 93 b"warning: %s ignored unexpected arguments %s\n"
94 94 % (cmd, b",".join(others))
95 95 )
96 96 return opts
97 97
98 98
99 99 def bundle1allowed(repo, action):
100 100 """Whether a bundle1 operation is allowed from the server.
101 101
102 102 Priority is:
103 103
104 104 1. server.bundle1gd.<action> (if generaldelta active)
105 105 2. server.bundle1.<action>
106 106 3. server.bundle1gd (if generaldelta active)
107 107 4. server.bundle1
108 108 """
109 109 ui = repo.ui
110 110 gd = b'generaldelta' in repo.requirements
111 111
112 112 if gd:
113 113 v = ui.configbool(b'server', b'bundle1gd.%s' % action)
114 114 if v is not None:
115 115 return v
116 116
117 117 v = ui.configbool(b'server', b'bundle1.%s' % action)
118 118 if v is not None:
119 119 return v
120 120
121 121 if gd:
122 122 v = ui.configbool(b'server', b'bundle1gd')
123 123 if v is not None:
124 124 return v
125 125
126 126 return ui.configbool(b'server', b'bundle1')
127 127
128 128
129 129 commands = wireprototypes.commanddict()
130 130
131 131
132 132 def wireprotocommand(name, args=None, permission=b'push'):
133 133 """Decorator to declare a wire protocol command.
134 134
135 135 ``name`` is the name of the wire protocol command being provided.
136 136
137 137 ``args`` defines the named arguments accepted by the command. It is
138 138 a space-delimited list of argument names. ``*`` denotes a special value
139 139 that says to accept all named arguments.
140 140
141 141 ``permission`` defines the permission type needed to run this command.
142 142 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
143 143 respectively. Default is to assume command requires ``push`` permissions
144 144 because otherwise commands not declaring their permissions could modify
145 145 a repository that is supposed to be read-only.
146 146 """
147 147 transports = {
148 148 k for k, v in wireprototypes.TRANSPORTS.items() if v[b'version'] == 1
149 149 }
150 150
151 151 # Because SSHv2 is a mirror of SSHv1, we allow "batch" commands through to
152 152 # SSHv2.
153 153 # TODO undo this hack when SSH is using the unified frame protocol.
154 154 if name == b'batch':
155 155 transports.add(wireprototypes.SSHV2)
156 156
157 157 if permission not in (b'push', b'pull'):
158 158 raise error.ProgrammingError(
159 159 b'invalid wire protocol permission; '
160 160 b'got %s; expected "push" or "pull"' % permission
161 161 )
162 162
163 163 if args is None:
164 164 args = b''
165 165
166 166 if not isinstance(args, bytes):
167 167 raise error.ProgrammingError(
168 168 b'arguments for version 1 commands must be declared as bytes'
169 169 )
170 170
171 171 def register(func):
172 172 if name in commands:
173 173 raise error.ProgrammingError(
174 174 b'%s command already registered for version 1' % name
175 175 )
176 176 commands[name] = wireprototypes.commandentry(
177 177 func, args=args, transports=transports, permission=permission
178 178 )
179 179
180 180 return func
181 181
182 182 return register
183 183
184 184
185 185 # TODO define a more appropriate permissions type to use for this.
186 186 @wireprotocommand(b'batch', b'cmds *', permission=b'pull')
187 187 def batch(repo, proto, cmds, others):
188 188 unescapearg = wireprototypes.unescapebatcharg
189 189 res = []
190 190 for pair in cmds.split(b';'):
191 191 op, args = pair.split(b' ', 1)
192 192 vals = {}
193 193 for a in args.split(b','):
194 194 if a:
195 195 n, v = a.split(b'=')
196 196 vals[unescapearg(n)] = unescapearg(v)
197 197 func, spec = commands[op]
198 198
199 199 # Validate that client has permissions to perform this command.
200 200 perm = commands[op].permission
201 201 assert perm in (b'push', b'pull')
202 202 proto.checkperm(perm)
203 203
204 204 if spec:
205 205 keys = spec.split()
206 206 data = {}
207 207 for k in keys:
208 208 if k == b'*':
209 209 star = {}
210 210 for key in vals.keys():
211 211 if key not in keys:
212 212 star[key] = vals[key]
213 213 data[b'*'] = star
214 214 else:
215 215 data[k] = vals[k]
216 216 result = func(repo, proto, *[data[k] for k in keys])
217 217 else:
218 218 result = func(repo, proto)
219 219 if isinstance(result, wireprototypes.ooberror):
220 220 return result
221 221
222 222 # For now, all batchable commands must return bytesresponse or
223 223 # raw bytes (for backwards compatibility).
224 224 assert isinstance(result, (wireprototypes.bytesresponse, bytes))
225 225 if isinstance(result, wireprototypes.bytesresponse):
226 226 result = result.data
227 227 res.append(wireprototypes.escapebatcharg(result))
228 228
229 229 return wireprototypes.bytesresponse(b';'.join(res))
230 230
231 231
232 232 @wireprotocommand(b'between', b'pairs', permission=b'pull')
233 233 def between(repo, proto, pairs):
234 234 pairs = [wireprototypes.decodelist(p, b'-') for p in pairs.split(b" ")]
235 235 r = []
236 236 for b in repo.between(pairs):
237 237 r.append(wireprototypes.encodelist(b) + b"\n")
238 238
239 239 return wireprototypes.bytesresponse(b''.join(r))
240 240
241 241
242 242 @wireprotocommand(b'branchmap', permission=b'pull')
243 243 def branchmap(repo, proto):
244 244 branchmap = repo.branchmap()
245 245 heads = []
246 246 for branch, nodes in pycompat.iteritems(branchmap):
247 247 branchname = urlreq.quote(encoding.fromlocal(branch))
248 248 branchnodes = wireprototypes.encodelist(nodes)
249 249 heads.append(b'%s %s' % (branchname, branchnodes))
250 250
251 251 return wireprototypes.bytesresponse(b'\n'.join(heads))
252 252
253 253
254 254 @wireprotocommand(b'branches', b'nodes', permission=b'pull')
255 255 def branches(repo, proto, nodes):
256 256 nodes = wireprototypes.decodelist(nodes)
257 257 r = []
258 258 for b in repo.branches(nodes):
259 259 r.append(wireprototypes.encodelist(b) + b"\n")
260 260
261 261 return wireprototypes.bytesresponse(b''.join(r))
262 262
263 263
264 264 @wireprotocommand(b'clonebundles', b'', permission=b'pull')
265 265 def clonebundles(repo, proto):
266 266 """Server command for returning info for available bundles to seed clones.
267 267
268 268 Clients will parse this response and determine what bundle to fetch.
269 269
270 270 Extensions may wrap this command to filter or dynamically emit data
271 271 depending on the request. e.g. you could advertise URLs for the closest
272 272 data center given the client's IP address.
273 273 """
274 274 return wireprototypes.bytesresponse(
275 275 repo.vfs.tryread(b'clonebundles.manifest')
276 276 )
277 277
278 278
279 279 wireprotocaps = [
280 280 b'lookup',
281 281 b'branchmap',
282 282 b'pushkey',
283 283 b'known',
284 284 b'getbundle',
285 285 b'unbundlehash',
286 286 ]
287 287
288 288
289 289 def _capabilities(repo, proto):
290 290 """return a list of capabilities for a repo
291 291
292 292 This function exists to allow extensions to easily wrap capabilities
293 293 computation
294 294
295 295 - returns a lists: easy to alter
296 296 - change done here will be propagated to both `capabilities` and `hello`
297 297 command without any other action needed.
298 298 """
299 299 # copy to prevent modification of the global list
300 300 caps = list(wireprotocaps)
301 301
302 302 # Command of same name as capability isn't exposed to version 1 of
303 303 # transports. So conditionally add it.
304 304 if commands.commandavailable(b'changegroupsubset', proto):
305 305 caps.append(b'changegroupsubset')
306 306
307 307 if streamclone.allowservergeneration(repo):
308 308 if repo.ui.configbool(b'server', b'preferuncompressed'):
309 309 caps.append(b'stream-preferred')
310 310 requiredformats = repo.requirements & repo.supportedformats
311 311 # if our local revlogs are just revlogv1, add 'stream' cap
312 312 if not requiredformats - {b'revlogv1'}:
313 313 caps.append(b'stream')
314 314 # otherwise, add 'streamreqs' detailing our local revlog format
315 315 else:
316 316 caps.append(b'streamreqs=%s' % b','.join(sorted(requiredformats)))
317 317 if repo.ui.configbool(b'experimental', b'bundle2-advertise'):
318 318 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role=b'server'))
319 319 caps.append(b'bundle2=' + urlreq.quote(capsblob))
320 320 caps.append(b'unbundle=%s' % b','.join(bundle2.bundlepriority))
321 321
322 322 if repo.ui.configbool(b'experimental', b'narrow'):
323 323 caps.append(wireprototypes.NARROWCAP)
324 324 if repo.ui.configbool(b'experimental', b'narrowservebrokenellipses'):
325 325 caps.append(wireprototypes.ELLIPSESCAP)
326 326
327 327 return proto.addcapabilities(repo, caps)
328 328
329 329
330 330 # If you are writing an extension and consider wrapping this function. Wrap
331 331 # `_capabilities` instead.
332 332 @wireprotocommand(b'capabilities', permission=b'pull')
333 333 def capabilities(repo, proto):
334 334 caps = _capabilities(repo, proto)
335 335 return wireprototypes.bytesresponse(b' '.join(sorted(caps)))
336 336
337 337
338 338 @wireprotocommand(b'changegroup', b'roots', permission=b'pull')
339 339 def changegroup(repo, proto, roots):
340 340 nodes = wireprototypes.decodelist(roots)
341 341 outgoing = discovery.outgoing(
342 342 repo, missingroots=nodes, missingheads=repo.heads()
343 343 )
344 344 cg = changegroupmod.makechangegroup(repo, outgoing, b'01', b'serve')
345 345 gen = iter(lambda: cg.read(32768), b'')
346 346 return wireprototypes.streamres(gen=gen)
347 347
348 348
349 349 @wireprotocommand(b'changegroupsubset', b'bases heads', permission=b'pull')
350 350 def changegroupsubset(repo, proto, bases, heads):
351 351 bases = wireprototypes.decodelist(bases)
352 352 heads = wireprototypes.decodelist(heads)
353 353 outgoing = discovery.outgoing(repo, missingroots=bases, missingheads=heads)
354 354 cg = changegroupmod.makechangegroup(repo, outgoing, b'01', b'serve')
355 355 gen = iter(lambda: cg.read(32768), b'')
356 356 return wireprototypes.streamres(gen=gen)
357 357
358 358
359 359 @wireprotocommand(b'debugwireargs', b'one two *', permission=b'pull')
360 360 def debugwireargs(repo, proto, one, two, others):
361 361 # only accept optional args from the known set
362 362 opts = options(b'debugwireargs', [b'three', b'four'], others)
363 363 return wireprototypes.bytesresponse(
364 364 repo.debugwireargs(one, two, **pycompat.strkwargs(opts))
365 365 )
366 366
367 367
368 368 def find_pullbundle(repo, proto, opts, clheads, heads, common):
369 369 """Return a file object for the first matching pullbundle.
370 370
371 371 Pullbundles are specified in .hg/pullbundles.manifest similar to
372 372 clonebundles.
373 373 For each entry, the bundle specification is checked for compatibility:
374 374 - Client features vs the BUNDLESPEC.
375 375 - Revisions shared with the clients vs base revisions of the bundle.
376 376 A bundle can be applied only if all its base revisions are known by
377 377 the client.
378 378 - At least one leaf of the bundle's DAG is missing on the client.
379 379 - Every leaf of the bundle's DAG is part of node set the client wants.
380 380 E.g. do not send a bundle of all changes if the client wants only
381 381 one specific branch of many.
382 382 """
383 383
384 384 def decodehexstring(s):
385 385 return {binascii.unhexlify(h) for h in s.split(b';')}
386 386
387 387 manifest = repo.vfs.tryread(b'pullbundles.manifest')
388 388 if not manifest:
389 389 return None
390 390 res = exchange.parseclonebundlesmanifest(repo, manifest)
391 391 res = exchange.filterclonebundleentries(repo, res)
392 392 if not res:
393 393 return None
394 cl = repo.changelog
394 cl = repo.unfiltered().changelog
395 395 heads_anc = cl.ancestors([cl.rev(rev) for rev in heads], inclusive=True)
396 396 common_anc = cl.ancestors([cl.rev(rev) for rev in common], inclusive=True)
397 397 compformats = clientcompressionsupport(proto)
398 398 for entry in res:
399 399 comp = entry.get(b'COMPRESSION')
400 400 altcomp = util.compengines._bundlenames.get(comp)
401 401 if comp and comp not in compformats and altcomp not in compformats:
402 402 continue
403 403 # No test yet for VERSION, since V2 is supported by any client
404 404 # that advertises partial pulls
405 405 if b'heads' in entry:
406 406 try:
407 407 bundle_heads = decodehexstring(entry[b'heads'])
408 408 except TypeError:
409 409 # Bad heads entry
410 410 continue
411 411 if bundle_heads.issubset(common):
412 412 continue # Nothing new
413 413 if all(cl.rev(rev) in common_anc for rev in bundle_heads):
414 414 continue # Still nothing new
415 415 if any(
416 416 cl.rev(rev) not in heads_anc and cl.rev(rev) not in common_anc
417 417 for rev in bundle_heads
418 418 ):
419 419 continue
420 420 if b'bases' in entry:
421 421 try:
422 422 bundle_bases = decodehexstring(entry[b'bases'])
423 423 except TypeError:
424 424 # Bad bases entry
425 425 continue
426 426 if not all(cl.rev(rev) in common_anc for rev in bundle_bases):
427 427 continue
428 428 path = entry[b'URL']
429 429 repo.ui.debug(b'sending pullbundle "%s"\n' % path)
430 430 try:
431 431 return repo.vfs.open(path)
432 432 except IOError:
433 433 repo.ui.debug(b'pullbundle "%s" not accessible\n' % path)
434 434 continue
435 435 return None
436 436
437 437
438 438 @wireprotocommand(b'getbundle', b'*', permission=b'pull')
439 439 def getbundle(repo, proto, others):
440 440 opts = options(
441 441 b'getbundle', wireprototypes.GETBUNDLE_ARGUMENTS.keys(), others
442 442 )
443 443 for k, v in pycompat.iteritems(opts):
444 444 keytype = wireprototypes.GETBUNDLE_ARGUMENTS[k]
445 445 if keytype == b'nodes':
446 446 opts[k] = wireprototypes.decodelist(v)
447 447 elif keytype == b'csv':
448 448 opts[k] = list(v.split(b','))
449 449 elif keytype == b'scsv':
450 450 opts[k] = set(v.split(b','))
451 451 elif keytype == b'boolean':
452 452 # Client should serialize False as '0', which is a non-empty string
453 453 # so it evaluates as a True bool.
454 454 if v == b'0':
455 455 opts[k] = False
456 456 else:
457 457 opts[k] = bool(v)
458 458 elif keytype != b'plain':
459 459 raise KeyError(b'unknown getbundle option type %s' % keytype)
460 460
461 461 if not bundle1allowed(repo, b'pull'):
462 462 if not exchange.bundle2requested(opts.get(b'bundlecaps')):
463 463 if proto.name == b'http-v1':
464 464 return wireprototypes.ooberror(bundle2required)
465 465 raise error.Abort(bundle2requiredmain, hint=bundle2requiredhint)
466 466
467 467 try:
468 468 clheads = set(repo.changelog.heads())
469 469 heads = set(opts.get(b'heads', set()))
470 470 common = set(opts.get(b'common', set()))
471 471 common.discard(nullid)
472 472 if (
473 473 repo.ui.configbool(b'server', b'pullbundle')
474 474 and b'partial-pull' in proto.getprotocaps()
475 475 ):
476 476 # Check if a pre-built bundle covers this request.
477 477 bundle = find_pullbundle(repo, proto, opts, clheads, heads, common)
478 478 if bundle:
479 479 return wireprototypes.streamres(
480 480 gen=util.filechunkiter(bundle), prefer_uncompressed=True
481 481 )
482 482
483 483 if repo.ui.configbool(b'server', b'disablefullbundle'):
484 484 # Check to see if this is a full clone.
485 485 changegroup = opts.get(b'cg', True)
486 486 if changegroup and not common and clheads == heads:
487 487 raise error.Abort(
488 488 _(b'server has pull-based clones disabled'),
489 489 hint=_(b'remove --pull if specified or upgrade Mercurial'),
490 490 )
491 491
492 492 info, chunks = exchange.getbundlechunks(
493 493 repo, b'serve', **pycompat.strkwargs(opts)
494 494 )
495 495 prefercompressed = info.get(b'prefercompressed', True)
496 496 except error.Abort as exc:
497 497 # cleanly forward Abort error to the client
498 498 if not exchange.bundle2requested(opts.get(b'bundlecaps')):
499 499 if proto.name == b'http-v1':
500 500 return wireprototypes.ooberror(pycompat.bytestr(exc) + b'\n')
501 501 raise # cannot do better for bundle1 + ssh
502 502 # bundle2 request expect a bundle2 reply
503 503 bundler = bundle2.bundle20(repo.ui)
504 504 manargs = [(b'message', pycompat.bytestr(exc))]
505 505 advargs = []
506 506 if exc.hint is not None:
507 507 advargs.append((b'hint', exc.hint))
508 508 bundler.addpart(bundle2.bundlepart(b'error:abort', manargs, advargs))
509 509 chunks = bundler.getchunks()
510 510 prefercompressed = False
511 511
512 512 return wireprototypes.streamres(
513 513 gen=chunks, prefer_uncompressed=not prefercompressed
514 514 )
515 515
516 516
517 517 @wireprotocommand(b'heads', permission=b'pull')
518 518 def heads(repo, proto):
519 519 h = repo.heads()
520 520 return wireprototypes.bytesresponse(wireprototypes.encodelist(h) + b'\n')
521 521
522 522
523 523 @wireprotocommand(b'hello', permission=b'pull')
524 524 def hello(repo, proto):
525 525 """Called as part of SSH handshake to obtain server info.
526 526
527 527 Returns a list of lines describing interesting things about the
528 528 server, in an RFC822-like format.
529 529
530 530 Currently, the only one defined is ``capabilities``, which consists of a
531 531 line of space separated tokens describing server abilities:
532 532
533 533 capabilities: <token0> <token1> <token2>
534 534 """
535 535 caps = capabilities(repo, proto).data
536 536 return wireprototypes.bytesresponse(b'capabilities: %s\n' % caps)
537 537
538 538
539 539 @wireprotocommand(b'listkeys', b'namespace', permission=b'pull')
540 540 def listkeys(repo, proto, namespace):
541 541 d = sorted(repo.listkeys(encoding.tolocal(namespace)).items())
542 542 return wireprototypes.bytesresponse(pushkeymod.encodekeys(d))
543 543
544 544
545 545 @wireprotocommand(b'lookup', b'key', permission=b'pull')
546 546 def lookup(repo, proto, key):
547 547 try:
548 548 k = encoding.tolocal(key)
549 549 n = repo.lookup(k)
550 550 r = hex(n)
551 551 success = 1
552 552 except Exception as inst:
553 553 r = stringutil.forcebytestr(inst)
554 554 success = 0
555 555 return wireprototypes.bytesresponse(b'%d %s\n' % (success, r))
556 556
557 557
558 558 @wireprotocommand(b'known', b'nodes *', permission=b'pull')
559 559 def known(repo, proto, nodes, others):
560 560 v = b''.join(
561 561 b and b'1' or b'0' for b in repo.known(wireprototypes.decodelist(nodes))
562 562 )
563 563 return wireprototypes.bytesresponse(v)
564 564
565 565
566 566 @wireprotocommand(b'protocaps', b'caps', permission=b'pull')
567 567 def protocaps(repo, proto, caps):
568 568 if proto.name == wireprototypes.SSHV1:
569 569 proto._protocaps = set(caps.split(b' '))
570 570 return wireprototypes.bytesresponse(b'OK')
571 571
572 572
573 573 @wireprotocommand(b'pushkey', b'namespace key old new', permission=b'push')
574 574 def pushkey(repo, proto, namespace, key, old, new):
575 575 # compatibility with pre-1.8 clients which were accidentally
576 576 # sending raw binary nodes rather than utf-8-encoded hex
577 577 if len(new) == 20 and stringutil.escapestr(new) != new:
578 578 # looks like it could be a binary node
579 579 try:
580 580 new.decode('utf-8')
581 581 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
582 582 except UnicodeDecodeError:
583 583 pass # binary, leave unmodified
584 584 else:
585 585 new = encoding.tolocal(new) # normal path
586 586
587 587 with proto.mayberedirectstdio() as output:
588 588 r = (
589 589 repo.pushkey(
590 590 encoding.tolocal(namespace),
591 591 encoding.tolocal(key),
592 592 encoding.tolocal(old),
593 593 new,
594 594 )
595 595 or False
596 596 )
597 597
598 598 output = output.getvalue() if output else b''
599 599 return wireprototypes.bytesresponse(b'%d\n%s' % (int(r), output))
600 600
601 601
602 602 @wireprotocommand(b'stream_out', permission=b'pull')
603 603 def stream(repo, proto):
604 604 '''If the server supports streaming clone, it advertises the "stream"
605 605 capability with a value representing the version and flags of the repo
606 606 it is serving. Client checks to see if it understands the format.
607 607 '''
608 608 return wireprototypes.streamreslegacy(streamclone.generatev1wireproto(repo))
609 609
610 610
611 611 @wireprotocommand(b'unbundle', b'heads', permission=b'push')
612 612 def unbundle(repo, proto, heads):
613 613 their_heads = wireprototypes.decodelist(heads)
614 614
615 615 with proto.mayberedirectstdio() as output:
616 616 try:
617 617 exchange.check_heads(repo, their_heads, b'preparing changes')
618 618 cleanup = lambda: None
619 619 try:
620 620 payload = proto.getpayload()
621 621 if repo.ui.configbool(b'server', b'streamunbundle'):
622 622
623 623 def cleanup():
624 624 # Ensure that the full payload is consumed, so
625 625 # that the connection doesn't contain trailing garbage.
626 626 for p in payload:
627 627 pass
628 628
629 629 fp = util.chunkbuffer(payload)
630 630 else:
631 631 # write bundle data to temporary file as it can be big
632 632 fp, tempname = None, None
633 633
634 634 def cleanup():
635 635 if fp:
636 636 fp.close()
637 637 if tempname:
638 638 os.unlink(tempname)
639 639
640 640 fd, tempname = pycompat.mkstemp(prefix=b'hg-unbundle-')
641 641 repo.ui.debug(
642 642 b'redirecting incoming bundle to %s\n' % tempname
643 643 )
644 644 fp = os.fdopen(fd, pycompat.sysstr(b'wb+'))
645 645 for p in payload:
646 646 fp.write(p)
647 647 fp.seek(0)
648 648
649 649 gen = exchange.readbundle(repo.ui, fp, None)
650 650 if isinstance(
651 651 gen, changegroupmod.cg1unpacker
652 652 ) and not bundle1allowed(repo, b'push'):
653 653 if proto.name == b'http-v1':
654 654 # need to special case http because stderr do not get to
655 655 # the http client on failed push so we need to abuse
656 656 # some other error type to make sure the message get to
657 657 # the user.
658 658 return wireprototypes.ooberror(bundle2required)
659 659 raise error.Abort(
660 660 bundle2requiredmain, hint=bundle2requiredhint
661 661 )
662 662
663 663 r = exchange.unbundle(
664 664 repo, gen, their_heads, b'serve', proto.client()
665 665 )
666 666 if util.safehasattr(r, b'addpart'):
667 667 # The return looks streamable, we are in the bundle2 case
668 668 # and should return a stream.
669 669 return wireprototypes.streamreslegacy(gen=r.getchunks())
670 670 return wireprototypes.pushres(
671 671 r, output.getvalue() if output else b''
672 672 )
673 673
674 674 finally:
675 675 cleanup()
676 676
677 677 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
678 678 # handle non-bundle2 case first
679 679 if not getattr(exc, 'duringunbundle2', False):
680 680 try:
681 681 raise
682 682 except error.Abort as exc:
683 683 # The old code we moved used procutil.stderr directly.
684 684 # We did not change it to minimise code change.
685 685 # This need to be moved to something proper.
686 686 # Feel free to do it.
687 687 procutil.stderr.write(b"abort: %s\n" % exc)
688 688 if exc.hint is not None:
689 689 procutil.stderr.write(b"(%s)\n" % exc.hint)
690 690 procutil.stderr.flush()
691 691 return wireprototypes.pushres(
692 692 0, output.getvalue() if output else b''
693 693 )
694 694 except error.PushRaced:
695 695 return wireprototypes.pusherr(
696 696 pycompat.bytestr(exc),
697 697 output.getvalue() if output else b'',
698 698 )
699 699
700 700 bundler = bundle2.bundle20(repo.ui)
701 701 for out in getattr(exc, '_bundle2salvagedoutput', ()):
702 702 bundler.addpart(out)
703 703 try:
704 704 try:
705 705 raise
706 706 except error.PushkeyFailed as exc:
707 707 # check client caps
708 708 remotecaps = getattr(exc, '_replycaps', None)
709 709 if (
710 710 remotecaps is not None
711 711 and b'pushkey' not in remotecaps.get(b'error', ())
712 712 ):
713 713 # no support remote side, fallback to Abort handler.
714 714 raise
715 715 part = bundler.newpart(b'error:pushkey')
716 716 part.addparam(b'in-reply-to', exc.partid)
717 717 if exc.namespace is not None:
718 718 part.addparam(
719 719 b'namespace', exc.namespace, mandatory=False
720 720 )
721 721 if exc.key is not None:
722 722 part.addparam(b'key', exc.key, mandatory=False)
723 723 if exc.new is not None:
724 724 part.addparam(b'new', exc.new, mandatory=False)
725 725 if exc.old is not None:
726 726 part.addparam(b'old', exc.old, mandatory=False)
727 727 if exc.ret is not None:
728 728 part.addparam(b'ret', exc.ret, mandatory=False)
729 729 except error.BundleValueError as exc:
730 730 errpart = bundler.newpart(b'error:unsupportedcontent')
731 731 if exc.parttype is not None:
732 732 errpart.addparam(b'parttype', exc.parttype)
733 733 if exc.params:
734 734 errpart.addparam(b'params', b'\0'.join(exc.params))
735 735 except error.Abort as exc:
736 736 manargs = [(b'message', stringutil.forcebytestr(exc))]
737 737 advargs = []
738 738 if exc.hint is not None:
739 739 advargs.append((b'hint', exc.hint))
740 740 bundler.addpart(
741 741 bundle2.bundlepart(b'error:abort', manargs, advargs)
742 742 )
743 743 except error.PushRaced as exc:
744 744 bundler.newpart(
745 745 b'error:pushraced',
746 746 [(b'message', stringutil.forcebytestr(exc))],
747 747 )
748 748 return wireprototypes.streamreslegacy(gen=bundler.getchunks())
@@ -1,187 +1,210 b''
1 1 #require no-chg
2 2
3 3 $ hg init repo
4 4 $ cd repo
5 5 $ echo foo > foo
6 6 $ hg ci -qAm 'add foo'
7 7 $ echo >> foo
8 8 $ hg ci -m 'change foo'
9 9 $ hg up -qC 0
10 10 $ echo bar > bar
11 11 $ hg ci -qAm 'add bar'
12 12
13 13 $ hg log
14 14 changeset: 2:effea6de0384
15 15 tag: tip
16 16 parent: 0:bbd179dfa0a7
17 17 user: test
18 18 date: Thu Jan 01 00:00:00 1970 +0000
19 19 summary: add bar
20 20
21 21 changeset: 1:ed1b79f46b9a
22 22 user: test
23 23 date: Thu Jan 01 00:00:00 1970 +0000
24 24 summary: change foo
25 25
26 26 changeset: 0:bbd179dfa0a7
27 27 user: test
28 28 date: Thu Jan 01 00:00:00 1970 +0000
29 29 summary: add foo
30 30
31 31 $ cd ..
32 32
33 33 Test pullbundle functionality
34 34
35 35 $ cd repo
36 36 $ cat <<EOF > .hg/hgrc
37 37 > [server]
38 38 > pullbundle = True
39 > [experimental]
40 > evolution = True
39 41 > [extensions]
40 42 > blackbox =
41 43 > EOF
42 44 $ hg bundle --base null -r 0 .hg/0.hg
43 45 1 changesets found
44 46 $ hg bundle --base 0 -r 1 .hg/1.hg
45 47 1 changesets found
46 48 $ hg bundle --base 1 -r 2 .hg/2.hg
47 49 1 changesets found
48 50 $ cat <<EOF > .hg/pullbundles.manifest
49 51 > 2.hg BUNDLESPEC=none-v2 heads=effea6de0384e684f44435651cb7bd70b8735bd4 bases=bbd179dfa0a71671c253b3ae0aa1513b60d199fa
50 52 > 1.hg BUNDLESPEC=bzip2-v2 heads=ed1b79f46b9a29f5a6efa59cf12fcfca43bead5a bases=bbd179dfa0a71671c253b3ae0aa1513b60d199fa
51 53 > 0.hg BUNDLESPEC=gzip-v2 heads=bbd179dfa0a71671c253b3ae0aa1513b60d199fa
52 54 > EOF
53 55 $ hg --config blackbox.track=debug --debug serve -p $HGPORT2 -d --pid-file=../repo.pid
54 56 listening at http://*:$HGPORT2/ (bound to $LOCALIP:$HGPORT2) (glob) (?)
55 57 $ cat ../repo.pid >> $DAEMON_PIDS
56 58 $ cd ..
57 59 $ hg clone -r 0 http://localhost:$HGPORT2/ repo.pullbundle
58 60 adding changesets
59 61 adding manifests
60 62 adding file changes
61 63 added 1 changesets with 1 changes to 1 files
62 64 new changesets bbd179dfa0a7 (1 drafts)
63 65 updating to branch default
64 66 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
65 67 $ cd repo.pullbundle
66 68 $ hg pull -r 1
67 69 pulling from http://localhost:$HGPORT2/
68 70 searching for changes
69 71 adding changesets
70 72 adding manifests
71 73 adding file changes
72 74 added 1 changesets with 1 changes to 1 files
73 75 new changesets ed1b79f46b9a (1 drafts)
74 76 (run 'hg update' to get a working copy)
75 77 $ hg pull -r 2
76 78 pulling from http://localhost:$HGPORT2/
77 79 searching for changes
78 80 adding changesets
79 81 adding manifests
80 82 adding file changes
81 83 added 1 changesets with 1 changes to 1 files (+1 heads)
82 84 new changesets effea6de0384 (1 drafts)
83 85 (run 'hg heads' to see heads, 'hg merge' to merge)
84 86 $ cd ..
85 87 $ killdaemons.py
86 88 $ grep 'sending pullbundle ' repo/.hg/blackbox.log
87 89 * sending pullbundle "0.hg" (glob)
88 90 * sending pullbundle "1.hg" (glob)
89 91 * sending pullbundle "2.hg" (glob)
90 92 $ rm repo/.hg/blackbox.log
91 93
92 94 Test pullbundle functionality for incremental pulls
93 95
94 96 $ cd repo
95 97 $ hg --config blackbox.track=debug --debug serve -p $HGPORT2 -d --pid-file=../repo.pid
96 98 listening at http://*:$HGPORT2/ (bound to $LOCALIP:$HGPORT2) (glob) (?)
97 99 $ cat ../repo.pid >> $DAEMON_PIDS
98 100 $ cd ..
99 101 $ hg clone http://localhost:$HGPORT2/ repo.pullbundle2
100 102 requesting all changes
101 103 adding changesets
102 104 adding manifests
103 105 adding file changes
104 106 adding changesets
105 107 adding manifests
106 108 adding file changes
107 109 adding changesets
108 110 adding manifests
109 111 adding file changes
110 112 added 3 changesets with 3 changes to 3 files (+1 heads)
111 113 new changesets bbd179dfa0a7:ed1b79f46b9a (3 drafts)
112 114 updating to branch default
113 115 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
114 116 $ killdaemons.py
115 117 $ grep 'sending pullbundle ' repo/.hg/blackbox.log
116 118 * sending pullbundle "0.hg" (glob)
117 119 * sending pullbundle "2.hg" (glob)
118 120 * sending pullbundle "1.hg" (glob)
119 121 $ rm repo/.hg/blackbox.log
120 122
121 123 Test pullbundle functionality for incoming
122 124
123 125 $ cd repo
124 126 $ hg --config blackbox.track=debug --debug serve -p $HGPORT2 -d --pid-file=../repo.pid
125 127 listening at http://*:$HGPORT2/ (bound to $LOCALIP:$HGPORT2) (glob) (?)
126 128 $ cat ../repo.pid >> $DAEMON_PIDS
127 129 $ cd ..
128 130 $ hg clone http://localhost:$HGPORT2/ repo.pullbundle2a -r 0
129 131 adding changesets
130 132 adding manifests
131 133 adding file changes
132 134 added 1 changesets with 1 changes to 1 files
133 135 new changesets bbd179dfa0a7 (1 drafts)
134 136 updating to branch default
135 137 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
136 138 $ cd repo.pullbundle2a
137 139 $ hg incoming -r ed1b79f46b9a
138 140 comparing with http://localhost:$HGPORT2/
139 141 searching for changes
140 142 changeset: 1:ed1b79f46b9a
141 143 tag: tip
142 144 user: test
143 145 date: Thu Jan 01 00:00:00 1970 +0000
144 146 summary: change foo
145 147
146 148 $ cd ..
147 149 $ killdaemons.py
148 150 $ grep 'sending pullbundle ' repo/.hg/blackbox.log
149 151 * sending pullbundle "0.hg" (glob)
150 152 * sending pullbundle "1.hg" (glob)
151 153 $ rm repo/.hg/blackbox.log
152 154
153 155 Test recovery from misconfigured server sending no new data
154 156
155 157 $ cd repo
156 158 $ cat <<EOF > .hg/pullbundles.manifest
157 159 > 0.hg heads=ed1b79f46b9a29f5a6efa59cf12fcfca43bead5a bases=bbd179dfa0a71671c253b3ae0aa1513b60d199fa
158 160 > 0.hg heads=bbd179dfa0a71671c253b3ae0aa1513b60d199fa
159 161 > EOF
160 162 $ hg --config blackbox.track=debug --debug serve -p $HGPORT2 -d --pid-file=../repo.pid
161 163 listening at http://*:$HGPORT2/ (bound to $LOCALIP:$HGPORT2) (glob) (?)
162 164 $ cat ../repo.pid >> $DAEMON_PIDS
163 165 $ cd ..
164 166 $ hg clone -r 0 http://localhost:$HGPORT2/ repo.pullbundle3
165 167 adding changesets
166 168 adding manifests
167 169 adding file changes
168 170 added 1 changesets with 1 changes to 1 files
169 171 new changesets bbd179dfa0a7 (1 drafts)
170 172 updating to branch default
171 173 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
172 174 $ cd repo.pullbundle3
173 175 $ hg pull -r 1
174 176 pulling from http://localhost:$HGPORT2/
175 177 searching for changes
176 178 adding changesets
177 179 adding manifests
178 180 adding file changes
179 181 added 0 changesets with 0 changes to 1 files
180 182 abort: 00changelog.i@ed1b79f46b9a: no node!
181 183 [255]
182 184 $ cd ..
183 185 $ killdaemons.py
184 186 $ grep 'sending pullbundle ' repo/.hg/blackbox.log
185 187 * sending pullbundle "0.hg" (glob)
186 188 * sending pullbundle "0.hg" (glob)
187 189 $ rm repo/.hg/blackbox.log
190
191 Test processing when nodes used in the pullbundle.manifest end up being hidden
192
193 $ hg --repo repo debugobsolete ed1b79f46b9a29f5a6efa59cf12fcfca43bead5a
194 1 new obsolescence markers
195 obsoleted 1 changesets
196 $ hg serve --repo repo --config server.view=visible -p $HGPORT -d --pid-file=hg.pid -E errors.log
197 $ cat hg.pid >> $DAEMON_PIDS
198 $ hg clone http://localhost:$HGPORT repo-obs
199 requesting all changes
200 adding changesets
201 adding manifests
202 adding file changes
203 adding changesets
204 adding manifests
205 adding file changes
206 added 2 changesets with 2 changes to 2 files
207 new changesets bbd179dfa0a7:effea6de0384
208 updating to branch default
209 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
210 $ killdaemons.py
General Comments 0
You need to be logged in to leave comments. Login now