##// END OF EJS Templates
wireproto: reimplement dispatch() for version 2 server...
Gregory Szorc -
r37800:99accae4 default
parent child Browse files
Show More
@@ -1,730 +1,720 b''
1 1 # wireproto.py - generic wire protocol support functions
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 os
11 11 import tempfile
12 12
13 13 from .i18n import _
14 14 from .node import (
15 15 hex,
16 16 nullid,
17 17 )
18 18
19 19 from . import (
20 20 bundle2,
21 21 changegroup as changegroupmod,
22 22 discovery,
23 23 encoding,
24 24 error,
25 25 exchange,
26 26 pushkey as pushkeymod,
27 27 pycompat,
28 28 streamclone,
29 29 util,
30 30 wireprototypes,
31 31 )
32 32
33 33 from .utils import (
34 34 procutil,
35 35 stringutil,
36 36 )
37 37
38 38 urlerr = util.urlerr
39 39 urlreq = util.urlreq
40 40
41 41 bundle2requiredmain = _('incompatible Mercurial client; bundle2 required')
42 42 bundle2requiredhint = _('see https://www.mercurial-scm.org/wiki/'
43 43 'IncompatibleClient')
44 44 bundle2required = '%s\n(%s)\n' % (bundle2requiredmain, bundle2requiredhint)
45 45
46 46 def clientcompressionsupport(proto):
47 47 """Returns a list of compression methods supported by the client.
48 48
49 49 Returns a list of the compression methods supported by the client
50 50 according to the protocol capabilities. If no such capability has
51 51 been announced, fallback to the default of zlib and uncompressed.
52 52 """
53 53 for cap in proto.getprotocaps():
54 54 if cap.startswith('comp='):
55 55 return cap[5:].split(',')
56 56 return ['zlib', 'none']
57 57
58 58 # wire protocol command can either return a string or one of these classes.
59 59
60 60 def getdispatchrepo(repo, proto, command):
61 61 """Obtain the repo used for processing wire protocol commands.
62 62
63 63 The intent of this function is to serve as a monkeypatch point for
64 64 extensions that need commands to operate on different repo views under
65 65 specialized circumstances.
66 66 """
67 67 return repo.filtered('served')
68 68
69 69 def dispatch(repo, proto, command):
70 70 repo = getdispatchrepo(repo, proto, command)
71 71
72 transportversion = wireprototypes.TRANSPORTS[proto.name]['version']
73 commandtable = commandsv2 if transportversion == 2 else commands
74 func, spec = commandtable[command]
75
72 func, spec = commands[command]
76 73 args = proto.getargs(spec)
77 74
78 # Version 1 protocols define arguments as a list. Version 2 uses a dict.
79 if isinstance(args, list):
80 return func(repo, proto, *args)
81 elif isinstance(args, dict):
82 return func(repo, proto, **args)
83 else:
84 raise error.ProgrammingError('unexpected type returned from '
85 'proto.getargs(): %s' % type(args))
75 return func(repo, proto, *args)
86 76
87 77 def options(cmd, keys, others):
88 78 opts = {}
89 79 for k in keys:
90 80 if k in others:
91 81 opts[k] = others[k]
92 82 del others[k]
93 83 if others:
94 84 procutil.stderr.write("warning: %s ignored unexpected arguments %s\n"
95 85 % (cmd, ",".join(others)))
96 86 return opts
97 87
98 88 def bundle1allowed(repo, action):
99 89 """Whether a bundle1 operation is allowed from the server.
100 90
101 91 Priority is:
102 92
103 93 1. server.bundle1gd.<action> (if generaldelta active)
104 94 2. server.bundle1.<action>
105 95 3. server.bundle1gd (if generaldelta active)
106 96 4. server.bundle1
107 97 """
108 98 ui = repo.ui
109 99 gd = 'generaldelta' in repo.requirements
110 100
111 101 if gd:
112 102 v = ui.configbool('server', 'bundle1gd.%s' % action)
113 103 if v is not None:
114 104 return v
115 105
116 106 v = ui.configbool('server', 'bundle1.%s' % action)
117 107 if v is not None:
118 108 return v
119 109
120 110 if gd:
121 111 v = ui.configbool('server', 'bundle1gd')
122 112 if v is not None:
123 113 return v
124 114
125 115 return ui.configbool('server', 'bundle1')
126 116
127 117 def supportedcompengines(ui, role):
128 118 """Obtain the list of supported compression engines for a request."""
129 119 assert role in (util.CLIENTROLE, util.SERVERROLE)
130 120
131 121 compengines = util.compengines.supportedwireengines(role)
132 122
133 123 # Allow config to override default list and ordering.
134 124 if role == util.SERVERROLE:
135 125 configengines = ui.configlist('server', 'compressionengines')
136 126 config = 'server.compressionengines'
137 127 else:
138 128 # This is currently implemented mainly to facilitate testing. In most
139 129 # cases, the server should be in charge of choosing a compression engine
140 130 # because a server has the most to lose from a sub-optimal choice. (e.g.
141 131 # CPU DoS due to an expensive engine or a network DoS due to poor
142 132 # compression ratio).
143 133 configengines = ui.configlist('experimental',
144 134 'clientcompressionengines')
145 135 config = 'experimental.clientcompressionengines'
146 136
147 137 # No explicit config. Filter out the ones that aren't supposed to be
148 138 # advertised and return default ordering.
149 139 if not configengines:
150 140 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
151 141 return [e for e in compengines
152 142 if getattr(e.wireprotosupport(), attr) > 0]
153 143
154 144 # If compression engines are listed in the config, assume there is a good
155 145 # reason for it (like server operators wanting to achieve specific
156 146 # performance characteristics). So fail fast if the config references
157 147 # unusable compression engines.
158 148 validnames = set(e.name() for e in compengines)
159 149 invalidnames = set(e for e in configengines if e not in validnames)
160 150 if invalidnames:
161 151 raise error.Abort(_('invalid compression engine defined in %s: %s') %
162 152 (config, ', '.join(sorted(invalidnames))))
163 153
164 154 compengines = [e for e in compengines if e.name() in configengines]
165 155 compengines = sorted(compengines,
166 156 key=lambda e: configengines.index(e.name()))
167 157
168 158 if not compengines:
169 159 raise error.Abort(_('%s config option does not specify any known '
170 160 'compression engines') % config,
171 161 hint=_('usable compression engines: %s') %
172 162 ', '.sorted(validnames))
173 163
174 164 return compengines
175 165
176 166 # For version 1 transports.
177 167 commands = wireprototypes.commanddict()
178 168
179 169 # For version 2 transports.
180 170 commandsv2 = wireprototypes.commanddict()
181 171
182 172 def wireprotocommand(name, args=None, permission='push'):
183 173 """Decorator to declare a wire protocol command.
184 174
185 175 ``name`` is the name of the wire protocol command being provided.
186 176
187 177 ``args`` defines the named arguments accepted by the command. It is
188 178 a space-delimited list of argument names. ``*`` denotes a special value
189 179 that says to accept all named arguments.
190 180
191 181 ``permission`` defines the permission type needed to run this command.
192 182 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
193 183 respectively. Default is to assume command requires ``push`` permissions
194 184 because otherwise commands not declaring their permissions could modify
195 185 a repository that is supposed to be read-only.
196 186 """
197 187 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
198 188 if v['version'] == 1}
199 189
200 190 # Because SSHv2 is a mirror of SSHv1, we allow "batch" commands through to
201 191 # SSHv2.
202 192 # TODO undo this hack when SSH is using the unified frame protocol.
203 193 if name == b'batch':
204 194 transports.add(wireprototypes.SSHV2)
205 195
206 196 if permission not in ('push', 'pull'):
207 197 raise error.ProgrammingError('invalid wire protocol permission; '
208 198 'got %s; expected "push" or "pull"' %
209 199 permission)
210 200
211 201 if args is None:
212 202 args = ''
213 203
214 204 if not isinstance(args, bytes):
215 205 raise error.ProgrammingError('arguments for version 1 commands '
216 206 'must be declared as bytes')
217 207
218 208 def register(func):
219 209 if name in commands:
220 210 raise error.ProgrammingError('%s command already registered '
221 211 'for version 1' % name)
222 212 commands[name] = wireprototypes.commandentry(
223 213 func, args=args, transports=transports, permission=permission)
224 214
225 215 return func
226 216 return register
227 217
228 218 # TODO define a more appropriate permissions type to use for this.
229 219 @wireprotocommand('batch', 'cmds *', permission='pull')
230 220 def batch(repo, proto, cmds, others):
231 221 unescapearg = wireprototypes.unescapebatcharg
232 222 repo = repo.filtered("served")
233 223 res = []
234 224 for pair in cmds.split(';'):
235 225 op, args = pair.split(' ', 1)
236 226 vals = {}
237 227 for a in args.split(','):
238 228 if a:
239 229 n, v = a.split('=')
240 230 vals[unescapearg(n)] = unescapearg(v)
241 231 func, spec = commands[op]
242 232
243 233 # Validate that client has permissions to perform this command.
244 234 perm = commands[op].permission
245 235 assert perm in ('push', 'pull')
246 236 proto.checkperm(perm)
247 237
248 238 if spec:
249 239 keys = spec.split()
250 240 data = {}
251 241 for k in keys:
252 242 if k == '*':
253 243 star = {}
254 244 for key in vals.keys():
255 245 if key not in keys:
256 246 star[key] = vals[key]
257 247 data['*'] = star
258 248 else:
259 249 data[k] = vals[k]
260 250 result = func(repo, proto, *[data[k] for k in keys])
261 251 else:
262 252 result = func(repo, proto)
263 253 if isinstance(result, wireprototypes.ooberror):
264 254 return result
265 255
266 256 # For now, all batchable commands must return bytesresponse or
267 257 # raw bytes (for backwards compatibility).
268 258 assert isinstance(result, (wireprototypes.bytesresponse, bytes))
269 259 if isinstance(result, wireprototypes.bytesresponse):
270 260 result = result.data
271 261 res.append(wireprototypes.escapebatcharg(result))
272 262
273 263 return wireprototypes.bytesresponse(';'.join(res))
274 264
275 265 @wireprotocommand('between', 'pairs', permission='pull')
276 266 def between(repo, proto, pairs):
277 267 pairs = [wireprototypes.decodelist(p, '-') for p in pairs.split(" ")]
278 268 r = []
279 269 for b in repo.between(pairs):
280 270 r.append(wireprototypes.encodelist(b) + "\n")
281 271
282 272 return wireprototypes.bytesresponse(''.join(r))
283 273
284 274 @wireprotocommand('branchmap', permission='pull')
285 275 def branchmap(repo, proto):
286 276 branchmap = repo.branchmap()
287 277 heads = []
288 278 for branch, nodes in branchmap.iteritems():
289 279 branchname = urlreq.quote(encoding.fromlocal(branch))
290 280 branchnodes = wireprototypes.encodelist(nodes)
291 281 heads.append('%s %s' % (branchname, branchnodes))
292 282
293 283 return wireprototypes.bytesresponse('\n'.join(heads))
294 284
295 285 @wireprotocommand('branches', 'nodes', permission='pull')
296 286 def branches(repo, proto, nodes):
297 287 nodes = wireprototypes.decodelist(nodes)
298 288 r = []
299 289 for b in repo.branches(nodes):
300 290 r.append(wireprototypes.encodelist(b) + "\n")
301 291
302 292 return wireprototypes.bytesresponse(''.join(r))
303 293
304 294 @wireprotocommand('clonebundles', '', permission='pull')
305 295 def clonebundles(repo, proto):
306 296 """Server command for returning info for available bundles to seed clones.
307 297
308 298 Clients will parse this response and determine what bundle to fetch.
309 299
310 300 Extensions may wrap this command to filter or dynamically emit data
311 301 depending on the request. e.g. you could advertise URLs for the closest
312 302 data center given the client's IP address.
313 303 """
314 304 return wireprototypes.bytesresponse(
315 305 repo.vfs.tryread('clonebundles.manifest'))
316 306
317 307 wireprotocaps = ['lookup', 'branchmap', 'pushkey',
318 308 'known', 'getbundle', 'unbundlehash']
319 309
320 310 def _capabilities(repo, proto):
321 311 """return a list of capabilities for a repo
322 312
323 313 This function exists to allow extensions to easily wrap capabilities
324 314 computation
325 315
326 316 - returns a lists: easy to alter
327 317 - change done here will be propagated to both `capabilities` and `hello`
328 318 command without any other action needed.
329 319 """
330 320 # copy to prevent modification of the global list
331 321 caps = list(wireprotocaps)
332 322
333 323 # Command of same name as capability isn't exposed to version 1 of
334 324 # transports. So conditionally add it.
335 325 if commands.commandavailable('changegroupsubset', proto):
336 326 caps.append('changegroupsubset')
337 327
338 328 if streamclone.allowservergeneration(repo):
339 329 if repo.ui.configbool('server', 'preferuncompressed'):
340 330 caps.append('stream-preferred')
341 331 requiredformats = repo.requirements & repo.supportedformats
342 332 # if our local revlogs are just revlogv1, add 'stream' cap
343 333 if not requiredformats - {'revlogv1'}:
344 334 caps.append('stream')
345 335 # otherwise, add 'streamreqs' detailing our local revlog format
346 336 else:
347 337 caps.append('streamreqs=%s' % ','.join(sorted(requiredformats)))
348 338 if repo.ui.configbool('experimental', 'bundle2-advertise'):
349 339 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role='server'))
350 340 caps.append('bundle2=' + urlreq.quote(capsblob))
351 341 caps.append('unbundle=%s' % ','.join(bundle2.bundlepriority))
352 342
353 343 return proto.addcapabilities(repo, caps)
354 344
355 345 # If you are writing an extension and consider wrapping this function. Wrap
356 346 # `_capabilities` instead.
357 347 @wireprotocommand('capabilities', permission='pull')
358 348 def capabilities(repo, proto):
359 349 caps = _capabilities(repo, proto)
360 350 return wireprototypes.bytesresponse(' '.join(sorted(caps)))
361 351
362 352 @wireprotocommand('changegroup', 'roots', permission='pull')
363 353 def changegroup(repo, proto, roots):
364 354 nodes = wireprototypes.decodelist(roots)
365 355 outgoing = discovery.outgoing(repo, missingroots=nodes,
366 356 missingheads=repo.heads())
367 357 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
368 358 gen = iter(lambda: cg.read(32768), '')
369 359 return wireprototypes.streamres(gen=gen)
370 360
371 361 @wireprotocommand('changegroupsubset', 'bases heads',
372 362 permission='pull')
373 363 def changegroupsubset(repo, proto, bases, heads):
374 364 bases = wireprototypes.decodelist(bases)
375 365 heads = wireprototypes.decodelist(heads)
376 366 outgoing = discovery.outgoing(repo, missingroots=bases,
377 367 missingheads=heads)
378 368 cg = changegroupmod.makechangegroup(repo, outgoing, '01', 'serve')
379 369 gen = iter(lambda: cg.read(32768), '')
380 370 return wireprototypes.streamres(gen=gen)
381 371
382 372 @wireprotocommand('debugwireargs', 'one two *',
383 373 permission='pull')
384 374 def debugwireargs(repo, proto, one, two, others):
385 375 # only accept optional args from the known set
386 376 opts = options('debugwireargs', ['three', 'four'], others)
387 377 return wireprototypes.bytesresponse(repo.debugwireargs(
388 378 one, two, **pycompat.strkwargs(opts)))
389 379
390 380 def find_pullbundle(repo, proto, opts, clheads, heads, common):
391 381 """Return a file object for the first matching pullbundle.
392 382
393 383 Pullbundles are specified in .hg/pullbundles.manifest similar to
394 384 clonebundles.
395 385 For each entry, the bundle specification is checked for compatibility:
396 386 - Client features vs the BUNDLESPEC.
397 387 - Revisions shared with the clients vs base revisions of the bundle.
398 388 A bundle can be applied only if all its base revisions are known by
399 389 the client.
400 390 - At least one leaf of the bundle's DAG is missing on the client.
401 391 - Every leaf of the bundle's DAG is part of node set the client wants.
402 392 E.g. do not send a bundle of all changes if the client wants only
403 393 one specific branch of many.
404 394 """
405 395 def decodehexstring(s):
406 396 return set([h.decode('hex') for h in s.split(';')])
407 397
408 398 manifest = repo.vfs.tryread('pullbundles.manifest')
409 399 if not manifest:
410 400 return None
411 401 res = exchange.parseclonebundlesmanifest(repo, manifest)
412 402 res = exchange.filterclonebundleentries(repo, res)
413 403 if not res:
414 404 return None
415 405 cl = repo.changelog
416 406 heads_anc = cl.ancestors([cl.rev(rev) for rev in heads], inclusive=True)
417 407 common_anc = cl.ancestors([cl.rev(rev) for rev in common], inclusive=True)
418 408 compformats = clientcompressionsupport(proto)
419 409 for entry in res:
420 410 if 'COMPRESSION' in entry and entry['COMPRESSION'] not in compformats:
421 411 continue
422 412 # No test yet for VERSION, since V2 is supported by any client
423 413 # that advertises partial pulls
424 414 if 'heads' in entry:
425 415 try:
426 416 bundle_heads = decodehexstring(entry['heads'])
427 417 except TypeError:
428 418 # Bad heads entry
429 419 continue
430 420 if bundle_heads.issubset(common):
431 421 continue # Nothing new
432 422 if all(cl.rev(rev) in common_anc for rev in bundle_heads):
433 423 continue # Still nothing new
434 424 if any(cl.rev(rev) not in heads_anc and
435 425 cl.rev(rev) not in common_anc for rev in bundle_heads):
436 426 continue
437 427 if 'bases' in entry:
438 428 try:
439 429 bundle_bases = decodehexstring(entry['bases'])
440 430 except TypeError:
441 431 # Bad bases entry
442 432 continue
443 433 if not all(cl.rev(rev) in common_anc for rev in bundle_bases):
444 434 continue
445 435 path = entry['URL']
446 436 repo.ui.debug('sending pullbundle "%s"\n' % path)
447 437 try:
448 438 return repo.vfs.open(path)
449 439 except IOError:
450 440 repo.ui.debug('pullbundle "%s" not accessible\n' % path)
451 441 continue
452 442 return None
453 443
454 444 @wireprotocommand('getbundle', '*', permission='pull')
455 445 def getbundle(repo, proto, others):
456 446 opts = options('getbundle', wireprototypes.GETBUNDLE_ARGUMENTS.keys(),
457 447 others)
458 448 for k, v in opts.iteritems():
459 449 keytype = wireprototypes.GETBUNDLE_ARGUMENTS[k]
460 450 if keytype == 'nodes':
461 451 opts[k] = wireprototypes.decodelist(v)
462 452 elif keytype == 'csv':
463 453 opts[k] = list(v.split(','))
464 454 elif keytype == 'scsv':
465 455 opts[k] = set(v.split(','))
466 456 elif keytype == 'boolean':
467 457 # Client should serialize False as '0', which is a non-empty string
468 458 # so it evaluates as a True bool.
469 459 if v == '0':
470 460 opts[k] = False
471 461 else:
472 462 opts[k] = bool(v)
473 463 elif keytype != 'plain':
474 464 raise KeyError('unknown getbundle option type %s'
475 465 % keytype)
476 466
477 467 if not bundle1allowed(repo, 'pull'):
478 468 if not exchange.bundle2requested(opts.get('bundlecaps')):
479 469 if proto.name == 'http-v1':
480 470 return wireprototypes.ooberror(bundle2required)
481 471 raise error.Abort(bundle2requiredmain,
482 472 hint=bundle2requiredhint)
483 473
484 474 prefercompressed = True
485 475
486 476 try:
487 477 clheads = set(repo.changelog.heads())
488 478 heads = set(opts.get('heads', set()))
489 479 common = set(opts.get('common', set()))
490 480 common.discard(nullid)
491 481 if (repo.ui.configbool('server', 'pullbundle') and
492 482 'partial-pull' in proto.getprotocaps()):
493 483 # Check if a pre-built bundle covers this request.
494 484 bundle = find_pullbundle(repo, proto, opts, clheads, heads, common)
495 485 if bundle:
496 486 return wireprototypes.streamres(gen=util.filechunkiter(bundle),
497 487 prefer_uncompressed=True)
498 488
499 489 if repo.ui.configbool('server', 'disablefullbundle'):
500 490 # Check to see if this is a full clone.
501 491 changegroup = opts.get('cg', True)
502 492 if changegroup and not common and clheads == heads:
503 493 raise error.Abort(
504 494 _('server has pull-based clones disabled'),
505 495 hint=_('remove --pull if specified or upgrade Mercurial'))
506 496
507 497 info, chunks = exchange.getbundlechunks(repo, 'serve',
508 498 **pycompat.strkwargs(opts))
509 499 prefercompressed = info.get('prefercompressed', True)
510 500 except error.Abort as exc:
511 501 # cleanly forward Abort error to the client
512 502 if not exchange.bundle2requested(opts.get('bundlecaps')):
513 503 if proto.name == 'http-v1':
514 504 return wireprototypes.ooberror(pycompat.bytestr(exc) + '\n')
515 505 raise # cannot do better for bundle1 + ssh
516 506 # bundle2 request expect a bundle2 reply
517 507 bundler = bundle2.bundle20(repo.ui)
518 508 manargs = [('message', pycompat.bytestr(exc))]
519 509 advargs = []
520 510 if exc.hint is not None:
521 511 advargs.append(('hint', exc.hint))
522 512 bundler.addpart(bundle2.bundlepart('error:abort',
523 513 manargs, advargs))
524 514 chunks = bundler.getchunks()
525 515 prefercompressed = False
526 516
527 517 return wireprototypes.streamres(
528 518 gen=chunks, prefer_uncompressed=not prefercompressed)
529 519
530 520 @wireprotocommand('heads', permission='pull')
531 521 def heads(repo, proto):
532 522 h = repo.heads()
533 523 return wireprototypes.bytesresponse(wireprototypes.encodelist(h) + '\n')
534 524
535 525 @wireprotocommand('hello', permission='pull')
536 526 def hello(repo, proto):
537 527 """Called as part of SSH handshake to obtain server info.
538 528
539 529 Returns a list of lines describing interesting things about the
540 530 server, in an RFC822-like format.
541 531
542 532 Currently, the only one defined is ``capabilities``, which consists of a
543 533 line of space separated tokens describing server abilities:
544 534
545 535 capabilities: <token0> <token1> <token2>
546 536 """
547 537 caps = capabilities(repo, proto).data
548 538 return wireprototypes.bytesresponse('capabilities: %s\n' % caps)
549 539
550 540 @wireprotocommand('listkeys', 'namespace', permission='pull')
551 541 def listkeys(repo, proto, namespace):
552 542 d = sorted(repo.listkeys(encoding.tolocal(namespace)).items())
553 543 return wireprototypes.bytesresponse(pushkeymod.encodekeys(d))
554 544
555 545 @wireprotocommand('lookup', 'key', permission='pull')
556 546 def lookup(repo, proto, key):
557 547 try:
558 548 k = encoding.tolocal(key)
559 549 n = repo.lookup(k)
560 550 r = hex(n)
561 551 success = 1
562 552 except Exception as inst:
563 553 r = stringutil.forcebytestr(inst)
564 554 success = 0
565 555 return wireprototypes.bytesresponse('%d %s\n' % (success, r))
566 556
567 557 @wireprotocommand('known', 'nodes *', permission='pull')
568 558 def known(repo, proto, nodes, others):
569 559 v = ''.join(b and '1' or '0'
570 560 for b in repo.known(wireprototypes.decodelist(nodes)))
571 561 return wireprototypes.bytesresponse(v)
572 562
573 563 @wireprotocommand('protocaps', 'caps', permission='pull')
574 564 def protocaps(repo, proto, caps):
575 565 if proto.name == wireprototypes.SSHV1:
576 566 proto._protocaps = set(caps.split(' '))
577 567 return wireprototypes.bytesresponse('OK')
578 568
579 569 @wireprotocommand('pushkey', 'namespace key old new', permission='push')
580 570 def pushkey(repo, proto, namespace, key, old, new):
581 571 # compatibility with pre-1.8 clients which were accidentally
582 572 # sending raw binary nodes rather than utf-8-encoded hex
583 573 if len(new) == 20 and stringutil.escapestr(new) != new:
584 574 # looks like it could be a binary node
585 575 try:
586 576 new.decode('utf-8')
587 577 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
588 578 except UnicodeDecodeError:
589 579 pass # binary, leave unmodified
590 580 else:
591 581 new = encoding.tolocal(new) # normal path
592 582
593 583 with proto.mayberedirectstdio() as output:
594 584 r = repo.pushkey(encoding.tolocal(namespace), encoding.tolocal(key),
595 585 encoding.tolocal(old), new) or False
596 586
597 587 output = output.getvalue() if output else ''
598 588 return wireprototypes.bytesresponse('%d\n%s' % (int(r), output))
599 589
600 590 @wireprotocommand('stream_out', permission='pull')
601 591 def stream(repo, proto):
602 592 '''If the server supports streaming clone, it advertises the "stream"
603 593 capability with a value representing the version and flags of the repo
604 594 it is serving. Client checks to see if it understands the format.
605 595 '''
606 596 return wireprototypes.streamreslegacy(
607 597 streamclone.generatev1wireproto(repo))
608 598
609 599 @wireprotocommand('unbundle', 'heads', permission='push')
610 600 def unbundle(repo, proto, heads):
611 601 their_heads = wireprototypes.decodelist(heads)
612 602
613 603 with proto.mayberedirectstdio() as output:
614 604 try:
615 605 exchange.check_heads(repo, their_heads, 'preparing changes')
616 606 cleanup = lambda: None
617 607 try:
618 608 payload = proto.getpayload()
619 609 if repo.ui.configbool('server', 'streamunbundle'):
620 610 def cleanup():
621 611 # Ensure that the full payload is consumed, so
622 612 # that the connection doesn't contain trailing garbage.
623 613 for p in payload:
624 614 pass
625 615 fp = util.chunkbuffer(payload)
626 616 else:
627 617 # write bundle data to temporary file as it can be big
628 618 fp, tempname = None, None
629 619 def cleanup():
630 620 if fp:
631 621 fp.close()
632 622 if tempname:
633 623 os.unlink(tempname)
634 624 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
635 625 repo.ui.debug('redirecting incoming bundle to %s\n' %
636 626 tempname)
637 627 fp = os.fdopen(fd, pycompat.sysstr('wb+'))
638 628 r = 0
639 629 for p in payload:
640 630 fp.write(p)
641 631 fp.seek(0)
642 632
643 633 gen = exchange.readbundle(repo.ui, fp, None)
644 634 if (isinstance(gen, changegroupmod.cg1unpacker)
645 635 and not bundle1allowed(repo, 'push')):
646 636 if proto.name == 'http-v1':
647 637 # need to special case http because stderr do not get to
648 638 # the http client on failed push so we need to abuse
649 639 # some other error type to make sure the message get to
650 640 # the user.
651 641 return wireprototypes.ooberror(bundle2required)
652 642 raise error.Abort(bundle2requiredmain,
653 643 hint=bundle2requiredhint)
654 644
655 645 r = exchange.unbundle(repo, gen, their_heads, 'serve',
656 646 proto.client())
657 647 if util.safehasattr(r, 'addpart'):
658 648 # The return looks streamable, we are in the bundle2 case
659 649 # and should return a stream.
660 650 return wireprototypes.streamreslegacy(gen=r.getchunks())
661 651 return wireprototypes.pushres(
662 652 r, output.getvalue() if output else '')
663 653
664 654 finally:
665 655 cleanup()
666 656
667 657 except (error.BundleValueError, error.Abort, error.PushRaced) as exc:
668 658 # handle non-bundle2 case first
669 659 if not getattr(exc, 'duringunbundle2', False):
670 660 try:
671 661 raise
672 662 except error.Abort:
673 663 # The old code we moved used procutil.stderr directly.
674 664 # We did not change it to minimise code change.
675 665 # This need to be moved to something proper.
676 666 # Feel free to do it.
677 667 procutil.stderr.write("abort: %s\n" % exc)
678 668 if exc.hint is not None:
679 669 procutil.stderr.write("(%s)\n" % exc.hint)
680 670 procutil.stderr.flush()
681 671 return wireprototypes.pushres(
682 672 0, output.getvalue() if output else '')
683 673 except error.PushRaced:
684 674 return wireprototypes.pusherr(
685 675 pycompat.bytestr(exc),
686 676 output.getvalue() if output else '')
687 677
688 678 bundler = bundle2.bundle20(repo.ui)
689 679 for out in getattr(exc, '_bundle2salvagedoutput', ()):
690 680 bundler.addpart(out)
691 681 try:
692 682 try:
693 683 raise
694 684 except error.PushkeyFailed as exc:
695 685 # check client caps
696 686 remotecaps = getattr(exc, '_replycaps', None)
697 687 if (remotecaps is not None
698 688 and 'pushkey' not in remotecaps.get('error', ())):
699 689 # no support remote side, fallback to Abort handler.
700 690 raise
701 691 part = bundler.newpart('error:pushkey')
702 692 part.addparam('in-reply-to', exc.partid)
703 693 if exc.namespace is not None:
704 694 part.addparam('namespace', exc.namespace,
705 695 mandatory=False)
706 696 if exc.key is not None:
707 697 part.addparam('key', exc.key, mandatory=False)
708 698 if exc.new is not None:
709 699 part.addparam('new', exc.new, mandatory=False)
710 700 if exc.old is not None:
711 701 part.addparam('old', exc.old, mandatory=False)
712 702 if exc.ret is not None:
713 703 part.addparam('ret', exc.ret, mandatory=False)
714 704 except error.BundleValueError as exc:
715 705 errpart = bundler.newpart('error:unsupportedcontent')
716 706 if exc.parttype is not None:
717 707 errpart.addparam('parttype', exc.parttype)
718 708 if exc.params:
719 709 errpart.addparam('params', '\0'.join(exc.params))
720 710 except error.Abort as exc:
721 711 manargs = [('message', stringutil.forcebytestr(exc))]
722 712 advargs = []
723 713 if exc.hint is not None:
724 714 advargs.append(('hint', exc.hint))
725 715 bundler.addpart(bundle2.bundlepart('error:abort',
726 716 manargs, advargs))
727 717 except error.PushRaced as exc:
728 718 bundler.newpart('error:pushraced',
729 719 [('message', stringutil.forcebytestr(exc))])
730 720 return wireprototypes.streamreslegacy(gen=bundler.getchunks())
@@ -1,522 +1,533 b''
1 1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7 from __future__ import absolute_import
8 8
9 9 import contextlib
10 10
11 11 from .i18n import _
12 12 from .thirdparty import (
13 13 cbor,
14 14 )
15 15 from .thirdparty.zope import (
16 16 interface as zi,
17 17 )
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 pycompat,
22 22 streamclone,
23 23 util,
24 24 wireproto,
25 25 wireprotoframing,
26 26 wireprototypes,
27 27 )
28 28
29 29 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
30 30
31 31 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
32 32
33 33 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
34 34 from .hgweb import common as hgwebcommon
35 35
36 36 # URL space looks like: <permissions>/<command>, where <permission> can
37 37 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
38 38
39 39 # Root URL does nothing meaningful... yet.
40 40 if not urlparts:
41 41 res.status = b'200 OK'
42 42 res.headers[b'Content-Type'] = b'text/plain'
43 43 res.setbodybytes(_('HTTP version 2 API handler'))
44 44 return
45 45
46 46 if len(urlparts) == 1:
47 47 res.status = b'404 Not Found'
48 48 res.headers[b'Content-Type'] = b'text/plain'
49 49 res.setbodybytes(_('do not know how to process %s\n') %
50 50 req.dispatchpath)
51 51 return
52 52
53 53 permission, command = urlparts[0:2]
54 54
55 55 if permission not in (b'ro', b'rw'):
56 56 res.status = b'404 Not Found'
57 57 res.headers[b'Content-Type'] = b'text/plain'
58 58 res.setbodybytes(_('unknown permission: %s') % permission)
59 59 return
60 60
61 61 if req.method != 'POST':
62 62 res.status = b'405 Method Not Allowed'
63 63 res.headers[b'Allow'] = b'POST'
64 64 res.setbodybytes(_('commands require POST requests'))
65 65 return
66 66
67 67 # At some point we'll want to use our own API instead of recycling the
68 68 # behavior of version 1 of the wire protocol...
69 69 # TODO return reasonable responses - not responses that overload the
70 70 # HTTP status line message for error reporting.
71 71 try:
72 72 checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
73 73 except hgwebcommon.ErrorResponse as e:
74 74 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
75 75 for k, v in e.headers:
76 76 res.headers[k] = v
77 77 res.setbodybytes('permission denied')
78 78 return
79 79
80 80 # We have a special endpoint to reflect the request back at the client.
81 81 if command == b'debugreflect':
82 82 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
83 83 return
84 84
85 85 # Extra commands that we handle that aren't really wire protocol
86 86 # commands. Think extra hard before making this hackery available to
87 87 # extension.
88 88 extracommands = {'multirequest'}
89 89
90 90 if command not in wireproto.commandsv2 and command not in extracommands:
91 91 res.status = b'404 Not Found'
92 92 res.headers[b'Content-Type'] = b'text/plain'
93 93 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
94 94 return
95 95
96 96 repo = rctx.repo
97 97 ui = repo.ui
98 98
99 99 proto = httpv2protocolhandler(req, ui)
100 100
101 101 if (not wireproto.commandsv2.commandavailable(command, proto)
102 102 and command not in extracommands):
103 103 res.status = b'404 Not Found'
104 104 res.headers[b'Content-Type'] = b'text/plain'
105 105 res.setbodybytes(_('invalid wire protocol command: %s') % command)
106 106 return
107 107
108 108 # TODO consider cases where proxies may add additional Accept headers.
109 109 if req.headers.get(b'Accept') != FRAMINGTYPE:
110 110 res.status = b'406 Not Acceptable'
111 111 res.headers[b'Content-Type'] = b'text/plain'
112 112 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
113 113 % FRAMINGTYPE)
114 114 return
115 115
116 116 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
117 117 res.status = b'415 Unsupported Media Type'
118 118 # TODO we should send a response with appropriate media type,
119 119 # since client does Accept it.
120 120 res.headers[b'Content-Type'] = b'text/plain'
121 121 res.setbodybytes(_('client MUST send Content-Type header with '
122 122 'value: %s\n') % FRAMINGTYPE)
123 123 return
124 124
125 125 _processhttpv2request(ui, repo, req, res, permission, command, proto)
126 126
127 127 def _processhttpv2reflectrequest(ui, repo, req, res):
128 128 """Reads unified frame protocol request and dumps out state to client.
129 129
130 130 This special endpoint can be used to help debug the wire protocol.
131 131
132 132 Instead of routing the request through the normal dispatch mechanism,
133 133 we instead read all frames, decode them, and feed them into our state
134 134 tracker. We then dump the log of all that activity back out to the
135 135 client.
136 136 """
137 137 import json
138 138
139 139 # Reflection APIs have a history of being abused, accidentally disclosing
140 140 # sensitive data, etc. So we have a config knob.
141 141 if not ui.configbool('experimental', 'web.api.debugreflect'):
142 142 res.status = b'404 Not Found'
143 143 res.headers[b'Content-Type'] = b'text/plain'
144 144 res.setbodybytes(_('debugreflect service not available'))
145 145 return
146 146
147 147 # We assume we have a unified framing protocol request body.
148 148
149 149 reactor = wireprotoframing.serverreactor()
150 150 states = []
151 151
152 152 while True:
153 153 frame = wireprotoframing.readframe(req.bodyfh)
154 154
155 155 if not frame:
156 156 states.append(b'received: <no frame>')
157 157 break
158 158
159 159 states.append(b'received: %d %d %d %s' % (frame.typeid, frame.flags,
160 160 frame.requestid,
161 161 frame.payload))
162 162
163 163 action, meta = reactor.onframerecv(frame)
164 164 states.append(json.dumps((action, meta), sort_keys=True,
165 165 separators=(', ', ': ')))
166 166
167 167 action, meta = reactor.oninputeof()
168 168 meta['action'] = action
169 169 states.append(json.dumps(meta, sort_keys=True, separators=(', ',': ')))
170 170
171 171 res.status = b'200 OK'
172 172 res.headers[b'Content-Type'] = b'text/plain'
173 173 res.setbodybytes(b'\n'.join(states))
174 174
175 175 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
176 176 """Post-validation handler for HTTPv2 requests.
177 177
178 178 Called when the HTTP request contains unified frame-based protocol
179 179 frames for evaluation.
180 180 """
181 181 # TODO Some HTTP clients are full duplex and can receive data before
182 182 # the entire request is transmitted. Figure out a way to indicate support
183 183 # for that so we can opt into full duplex mode.
184 184 reactor = wireprotoframing.serverreactor(deferoutput=True)
185 185 seencommand = False
186 186
187 187 outstream = reactor.makeoutputstream()
188 188
189 189 while True:
190 190 frame = wireprotoframing.readframe(req.bodyfh)
191 191 if not frame:
192 192 break
193 193
194 194 action, meta = reactor.onframerecv(frame)
195 195
196 196 if action == 'wantframe':
197 197 # Need more data before we can do anything.
198 198 continue
199 199 elif action == 'runcommand':
200 200 sentoutput = _httpv2runcommand(ui, repo, req, res, authedperm,
201 201 reqcommand, reactor, outstream,
202 202 meta, issubsequent=seencommand)
203 203
204 204 if sentoutput:
205 205 return
206 206
207 207 seencommand = True
208 208
209 209 elif action == 'error':
210 210 # TODO define proper error mechanism.
211 211 res.status = b'200 OK'
212 212 res.headers[b'Content-Type'] = b'text/plain'
213 213 res.setbodybytes(meta['message'] + b'\n')
214 214 return
215 215 else:
216 216 raise error.ProgrammingError(
217 217 'unhandled action from frame processor: %s' % action)
218 218
219 219 action, meta = reactor.oninputeof()
220 220 if action == 'sendframes':
221 221 # We assume we haven't started sending the response yet. If we're
222 222 # wrong, the response type will raise an exception.
223 223 res.status = b'200 OK'
224 224 res.headers[b'Content-Type'] = FRAMINGTYPE
225 225 res.setbodygen(meta['framegen'])
226 226 elif action == 'noop':
227 227 pass
228 228 else:
229 229 raise error.ProgrammingError('unhandled action from frame processor: %s'
230 230 % action)
231 231
232 232 def _httpv2runcommand(ui, repo, req, res, authedperm, reqcommand, reactor,
233 233 outstream, command, issubsequent):
234 234 """Dispatch a wire protocol command made from HTTPv2 requests.
235 235
236 236 The authenticated permission (``authedperm``) along with the original
237 237 command from the URL (``reqcommand``) are passed in.
238 238 """
239 239 # We already validated that the session has permissions to perform the
240 240 # actions in ``authedperm``. In the unified frame protocol, the canonical
241 241 # command to run is expressed in a frame. However, the URL also requested
242 242 # to run a specific command. We need to be careful that the command we
243 243 # run doesn't have permissions requirements greater than what was granted
244 244 # by ``authedperm``.
245 245 #
246 246 # Our rule for this is we only allow one command per HTTP request and
247 247 # that command must match the command in the URL. However, we make
248 248 # an exception for the ``multirequest`` URL. This URL is allowed to
249 249 # execute multiple commands. We double check permissions of each command
250 250 # as it is invoked to ensure there is no privilege escalation.
251 251 # TODO consider allowing multiple commands to regular command URLs
252 252 # iff each command is the same.
253 253
254 254 proto = httpv2protocolhandler(req, ui, args=command['args'])
255 255
256 256 if reqcommand == b'multirequest':
257 257 if not wireproto.commandsv2.commandavailable(command['command'], proto):
258 258 # TODO proper error mechanism
259 259 res.status = b'200 OK'
260 260 res.headers[b'Content-Type'] = b'text/plain'
261 261 res.setbodybytes(_('wire protocol command not available: %s') %
262 262 command['command'])
263 263 return True
264 264
265 265 # TODO don't use assert here, since it may be elided by -O.
266 266 assert authedperm in (b'ro', b'rw')
267 267 wirecommand = wireproto.commandsv2[command['command']]
268 268 assert wirecommand.permission in ('push', 'pull')
269 269
270 270 if authedperm == b'ro' and wirecommand.permission != 'pull':
271 271 # TODO proper error mechanism
272 272 res.status = b'403 Forbidden'
273 273 res.headers[b'Content-Type'] = b'text/plain'
274 274 res.setbodybytes(_('insufficient permissions to execute '
275 275 'command: %s') % command['command'])
276 276 return True
277 277
278 278 # TODO should we also call checkperm() here? Maybe not if we're going
279 279 # to overhaul that API. The granted scope from the URL check should
280 280 # be good enough.
281 281
282 282 else:
283 283 # Don't allow multiple commands outside of ``multirequest`` URL.
284 284 if issubsequent:
285 285 # TODO proper error mechanism
286 286 res.status = b'200 OK'
287 287 res.headers[b'Content-Type'] = b'text/plain'
288 288 res.setbodybytes(_('multiple commands cannot be issued to this '
289 289 'URL'))
290 290 return True
291 291
292 292 if reqcommand != command['command']:
293 293 # TODO define proper error mechanism
294 294 res.status = b'200 OK'
295 295 res.headers[b'Content-Type'] = b'text/plain'
296 296 res.setbodybytes(_('command in frame must match command in URL'))
297 297 return True
298 298
299 rsp = wireproto.dispatch(repo, proto, command['command'])
299 rsp = dispatch(repo, proto, command['command'])
300 300
301 301 res.status = b'200 OK'
302 302 res.headers[b'Content-Type'] = FRAMINGTYPE
303 303
304 304 if isinstance(rsp, wireprototypes.cborresponse):
305 305 encoded = cbor.dumps(rsp.value, canonical=True)
306 306 action, meta = reactor.oncommandresponseready(outstream,
307 307 command['requestid'],
308 308 encoded)
309 309 elif isinstance(rsp, wireprototypes.v2streamingresponse):
310 310 action, meta = reactor.oncommandresponsereadygen(outstream,
311 311 command['requestid'],
312 312 rsp.gen)
313 313 elif isinstance(rsp, wireprototypes.v2errorresponse):
314 314 action, meta = reactor.oncommanderror(outstream,
315 315 command['requestid'],
316 316 rsp.message,
317 317 rsp.args)
318 318 else:
319 319 action, meta = reactor.onservererror(
320 320 _('unhandled response type from wire proto command'))
321 321
322 322 if action == 'sendframes':
323 323 res.setbodygen(meta['framegen'])
324 324 return True
325 325 elif action == 'noop':
326 326 return False
327 327 else:
328 328 raise error.ProgrammingError('unhandled event from reactor: %s' %
329 329 action)
330 330
331 def getdispatchrepo(repo, proto, command):
332 return repo.filtered('served')
333
334 def dispatch(repo, proto, command):
335 repo = getdispatchrepo(repo, proto, command)
336
337 func, spec = wireproto.commandsv2[command]
338 args = proto.getargs(spec)
339
340 return func(repo, proto, **args)
341
331 342 @zi.implementer(wireprototypes.baseprotocolhandler)
332 343 class httpv2protocolhandler(object):
333 344 def __init__(self, req, ui, args=None):
334 345 self._req = req
335 346 self._ui = ui
336 347 self._args = args
337 348
338 349 @property
339 350 def name(self):
340 351 return HTTP_WIREPROTO_V2
341 352
342 353 def getargs(self, args):
343 354 data = {}
344 355 for k, typ in args.items():
345 356 if k == '*':
346 357 raise NotImplementedError('do not support * args')
347 358 elif k in self._args:
348 359 # TODO consider validating value types.
349 360 data[k] = self._args[k]
350 361
351 362 return data
352 363
353 364 def getprotocaps(self):
354 365 # Protocol capabilities are currently not implemented for HTTP V2.
355 366 return set()
356 367
357 368 def getpayload(self):
358 369 raise NotImplementedError
359 370
360 371 @contextlib.contextmanager
361 372 def mayberedirectstdio(self):
362 373 raise NotImplementedError
363 374
364 375 def client(self):
365 376 raise NotImplementedError
366 377
367 378 def addcapabilities(self, repo, caps):
368 379 return caps
369 380
370 381 def checkperm(self, perm):
371 382 raise NotImplementedError
372 383
373 384 def httpv2apidescriptor(req, repo):
374 385 proto = httpv2protocolhandler(req, repo.ui)
375 386
376 387 return _capabilitiesv2(repo, proto)
377 388
378 389 def _capabilitiesv2(repo, proto):
379 390 """Obtain the set of capabilities for version 2 transports.
380 391
381 392 These capabilities are distinct from the capabilities for version 1
382 393 transports.
383 394 """
384 395 compression = []
385 396 for engine in wireproto.supportedcompengines(repo.ui, util.SERVERROLE):
386 397 compression.append({
387 398 b'name': engine.wireprotosupport().name,
388 399 })
389 400
390 401 caps = {
391 402 'commands': {},
392 403 'compression': compression,
393 404 'framingmediatypes': [FRAMINGTYPE],
394 405 }
395 406
396 407 for command, entry in wireproto.commandsv2.items():
397 408 caps['commands'][command] = {
398 409 'args': entry.args,
399 410 'permissions': [entry.permission],
400 411 }
401 412
402 413 if streamclone.allowservergeneration(repo):
403 414 caps['rawrepoformats'] = sorted(repo.requirements &
404 415 repo.supportedformats)
405 416
406 417 return proto.addcapabilities(repo, caps)
407 418
408 419 def wireprotocommand(name, args=None, permission='push'):
409 420 """Decorator to declare a wire protocol command.
410 421
411 422 ``name`` is the name of the wire protocol command being provided.
412 423
413 424 ``args`` is a dict of argument names to example values.
414 425
415 426 ``permission`` defines the permission type needed to run this command.
416 427 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
417 428 respectively. Default is to assume command requires ``push`` permissions
418 429 because otherwise commands not declaring their permissions could modify
419 430 a repository that is supposed to be read-only.
420 431 """
421 432 transports = {k for k, v in wireprototypes.TRANSPORTS.items()
422 433 if v['version'] == 2}
423 434
424 435 if permission not in ('push', 'pull'):
425 436 raise error.ProgrammingError('invalid wire protocol permission; '
426 437 'got %s; expected "push" or "pull"' %
427 438 permission)
428 439
429 440 if args is None:
430 441 args = {}
431 442
432 443 if not isinstance(args, dict):
433 444 raise error.ProgrammingError('arguments for version 2 commands '
434 445 'must be declared as dicts')
435 446
436 447 def register(func):
437 448 if name in wireproto.commandsv2:
438 449 raise error.ProgrammingError('%s command already registered '
439 450 'for version 2' % name)
440 451
441 452 wireproto.commandsv2[name] = wireprototypes.commandentry(
442 453 func, args=args, transports=transports, permission=permission)
443 454
444 455 return func
445 456
446 457 return register
447 458
448 459 @wireprotocommand('branchmap', permission='pull')
449 460 def branchmapv2(repo, proto):
450 461 branchmap = {encoding.fromlocal(k): v
451 462 for k, v in repo.branchmap().iteritems()}
452 463
453 464 return wireprototypes.cborresponse(branchmap)
454 465
455 466 @wireprotocommand('capabilities', permission='pull')
456 467 def capabilitiesv2(repo, proto):
457 468 caps = _capabilitiesv2(repo, proto)
458 469
459 470 return wireprototypes.cborresponse(caps)
460 471
461 472 @wireprotocommand('heads',
462 473 args={
463 474 'publiconly': False,
464 475 },
465 476 permission='pull')
466 477 def headsv2(repo, proto, publiconly=False):
467 478 if publiconly:
468 479 repo = repo.filtered('immutable')
469 480
470 481 return wireprototypes.cborresponse(repo.heads())
471 482
472 483 @wireprotocommand('known',
473 484 args={
474 485 'nodes': [b'deadbeef'],
475 486 },
476 487 permission='pull')
477 488 def knownv2(repo, proto, nodes=None):
478 489 nodes = nodes or []
479 490 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
480 491 return wireprototypes.cborresponse(result)
481 492
482 493 @wireprotocommand('listkeys',
483 494 args={
484 495 'namespace': b'ns',
485 496 },
486 497 permission='pull')
487 498 def listkeysv2(repo, proto, namespace=None):
488 499 keys = repo.listkeys(encoding.tolocal(namespace))
489 500 keys = {encoding.fromlocal(k): encoding.fromlocal(v)
490 501 for k, v in keys.iteritems()}
491 502
492 503 return wireprototypes.cborresponse(keys)
493 504
494 505 @wireprotocommand('lookup',
495 506 args={
496 507 'key': b'foo',
497 508 },
498 509 permission='pull')
499 510 def lookupv2(repo, proto, key):
500 511 key = encoding.tolocal(key)
501 512
502 513 # TODO handle exception.
503 514 node = repo.lookup(key)
504 515
505 516 return wireprototypes.cborresponse(node)
506 517
507 518 @wireprotocommand('pushkey',
508 519 args={
509 520 'namespace': b'ns',
510 521 'key': b'key',
511 522 'old': b'old',
512 523 'new': b'new',
513 524 },
514 525 permission='push')
515 526 def pushkeyv2(repo, proto, namespace, key, old, new):
516 527 # TODO handle ui output redirection
517 528 r = repo.pushkey(encoding.tolocal(namespace),
518 529 encoding.tolocal(key),
519 530 encoding.tolocal(old),
520 531 encoding.tolocal(new))
521 532
522 533 return wireprototypes.cborresponse(r)
General Comments 0
You need to be logged in to leave comments. Login now