mercurial/wireprotov2server.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43106 d783f945a701
equal deleted inserted replaced
43076:2372284d9457 43077:687b865b95ad
    54 
    54 
    55     # Root URL does nothing meaningful... yet.
    55     # Root URL does nothing meaningful... yet.
    56     if not urlparts:
    56     if not urlparts:
    57         res.status = b'200 OK'
    57         res.status = b'200 OK'
    58         res.headers[b'Content-Type'] = b'text/plain'
    58         res.headers[b'Content-Type'] = b'text/plain'
    59         res.setbodybytes(_('HTTP version 2 API handler'))
    59         res.setbodybytes(_(b'HTTP version 2 API handler'))
    60         return
    60         return
    61 
    61 
    62     if len(urlparts) == 1:
    62     if len(urlparts) == 1:
    63         res.status = b'404 Not Found'
    63         res.status = b'404 Not Found'
    64         res.headers[b'Content-Type'] = b'text/plain'
    64         res.headers[b'Content-Type'] = b'text/plain'
    65         res.setbodybytes(
    65         res.setbodybytes(
    66             _('do not know how to process %s\n') % req.dispatchpath
    66             _(b'do not know how to process %s\n') % req.dispatchpath
    67         )
    67         )
    68         return
    68         return
    69 
    69 
    70     permission, command = urlparts[0:2]
    70     permission, command = urlparts[0:2]
    71 
    71 
    72     if permission not in (b'ro', b'rw'):
    72     if permission not in (b'ro', b'rw'):
    73         res.status = b'404 Not Found'
    73         res.status = b'404 Not Found'
    74         res.headers[b'Content-Type'] = b'text/plain'
    74         res.headers[b'Content-Type'] = b'text/plain'
    75         res.setbodybytes(_('unknown permission: %s') % permission)
    75         res.setbodybytes(_(b'unknown permission: %s') % permission)
    76         return
    76         return
    77 
    77 
    78     if req.method != 'POST':
    78     if req.method != b'POST':
    79         res.status = b'405 Method Not Allowed'
    79         res.status = b'405 Method Not Allowed'
    80         res.headers[b'Allow'] = b'POST'
    80         res.headers[b'Allow'] = b'POST'
    81         res.setbodybytes(_('commands require POST requests'))
    81         res.setbodybytes(_(b'commands require POST requests'))
    82         return
    82         return
    83 
    83 
    84     # At some point we'll want to use our own API instead of recycling the
    84     # At some point we'll want to use our own API instead of recycling the
    85     # behavior of version 1 of the wire protocol...
    85     # behavior of version 1 of the wire protocol...
    86     # TODO return reasonable responses - not responses that overload the
    86     # TODO return reasonable responses - not responses that overload the
    87     # HTTP status line message for error reporting.
    87     # HTTP status line message for error reporting.
    88     try:
    88     try:
    89         checkperm(rctx, req, 'pull' if permission == b'ro' else 'push')
    89         checkperm(rctx, req, b'pull' if permission == b'ro' else b'push')
    90     except hgwebcommon.ErrorResponse as e:
    90     except hgwebcommon.ErrorResponse as e:
    91         res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
    91         res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
    92         for k, v in e.headers:
    92         for k, v in e.headers:
    93             res.headers[k] = v
    93             res.headers[k] = v
    94         res.setbodybytes('permission denied')
    94         res.setbodybytes(b'permission denied')
    95         return
    95         return
    96 
    96 
    97     # We have a special endpoint to reflect the request back at the client.
    97     # We have a special endpoint to reflect the request back at the client.
    98     if command == b'debugreflect':
    98     if command == b'debugreflect':
    99         _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
    99         _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
   100         return
   100         return
   101 
   101 
   102     # Extra commands that we handle that aren't really wire protocol
   102     # Extra commands that we handle that aren't really wire protocol
   103     # commands. Think extra hard before making this hackery available to
   103     # commands. Think extra hard before making this hackery available to
   104     # extension.
   104     # extension.
   105     extracommands = {'multirequest'}
   105     extracommands = {b'multirequest'}
   106 
   106 
   107     if command not in COMMANDS and command not in extracommands:
   107     if command not in COMMANDS and command not in extracommands:
   108         res.status = b'404 Not Found'
   108         res.status = b'404 Not Found'
   109         res.headers[b'Content-Type'] = b'text/plain'
   109         res.headers[b'Content-Type'] = b'text/plain'
   110         res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
   110         res.setbodybytes(_(b'unknown wire protocol command: %s\n') % command)
   111         return
   111         return
   112 
   112 
   113     repo = rctx.repo
   113     repo = rctx.repo
   114     ui = repo.ui
   114     ui = repo.ui
   115 
   115 
   119         not COMMANDS.commandavailable(command, proto)
   119         not COMMANDS.commandavailable(command, proto)
   120         and command not in extracommands
   120         and command not in extracommands
   121     ):
   121     ):
   122         res.status = b'404 Not Found'
   122         res.status = b'404 Not Found'
   123         res.headers[b'Content-Type'] = b'text/plain'
   123         res.headers[b'Content-Type'] = b'text/plain'
   124         res.setbodybytes(_('invalid wire protocol command: %s') % command)
   124         res.setbodybytes(_(b'invalid wire protocol command: %s') % command)
   125         return
   125         return
   126 
   126 
   127     # TODO consider cases where proxies may add additional Accept headers.
   127     # TODO consider cases where proxies may add additional Accept headers.
   128     if req.headers.get(b'Accept') != FRAMINGTYPE:
   128     if req.headers.get(b'Accept') != FRAMINGTYPE:
   129         res.status = b'406 Not Acceptable'
   129         res.status = b'406 Not Acceptable'
   130         res.headers[b'Content-Type'] = b'text/plain'
   130         res.headers[b'Content-Type'] = b'text/plain'
   131         res.setbodybytes(
   131         res.setbodybytes(
   132             _('client MUST specify Accept header with value: %s\n')
   132             _(b'client MUST specify Accept header with value: %s\n')
   133             % FRAMINGTYPE
   133             % FRAMINGTYPE
   134         )
   134         )
   135         return
   135         return
   136 
   136 
   137     if req.headers.get(b'Content-Type') != FRAMINGTYPE:
   137     if req.headers.get(b'Content-Type') != FRAMINGTYPE:
   138         res.status = b'415 Unsupported Media Type'
   138         res.status = b'415 Unsupported Media Type'
   139         # TODO we should send a response with appropriate media type,
   139         # TODO we should send a response with appropriate media type,
   140         # since client does Accept it.
   140         # since client does Accept it.
   141         res.headers[b'Content-Type'] = b'text/plain'
   141         res.headers[b'Content-Type'] = b'text/plain'
   142         res.setbodybytes(
   142         res.setbodybytes(
   143             _('client MUST send Content-Type header with ' 'value: %s\n')
   143             _(b'client MUST send Content-Type header with ' b'value: %s\n')
   144             % FRAMINGTYPE
   144             % FRAMINGTYPE
   145         )
   145         )
   146         return
   146         return
   147 
   147 
   148     _processhttpv2request(ui, repo, req, res, permission, command, proto)
   148     _processhttpv2request(ui, repo, req, res, permission, command, proto)
   158     tracker. We then dump the log of all that activity back out to the
   158     tracker. We then dump the log of all that activity back out to the
   159     client.
   159     client.
   160     """
   160     """
   161     # Reflection APIs have a history of being abused, accidentally disclosing
   161     # Reflection APIs have a history of being abused, accidentally disclosing
   162     # sensitive data, etc. So we have a config knob.
   162     # sensitive data, etc. So we have a config knob.
   163     if not ui.configbool('experimental', 'web.api.debugreflect'):
   163     if not ui.configbool(b'experimental', b'web.api.debugreflect'):
   164         res.status = b'404 Not Found'
   164         res.status = b'404 Not Found'
   165         res.headers[b'Content-Type'] = b'text/plain'
   165         res.headers[b'Content-Type'] = b'text/plain'
   166         res.setbodybytes(_('debugreflect service not available'))
   166         res.setbodybytes(_(b'debugreflect service not available'))
   167         return
   167         return
   168 
   168 
   169     # We assume we have a unified framing protocol request body.
   169     # We assume we have a unified framing protocol request body.
   170 
   170 
   171     reactor = wireprotoframing.serverreactor(ui)
   171     reactor = wireprotoframing.serverreactor(ui)
   185 
   185 
   186         action, meta = reactor.onframerecv(frame)
   186         action, meta = reactor.onframerecv(frame)
   187         states.append(templatefilters.json((action, meta)))
   187         states.append(templatefilters.json((action, meta)))
   188 
   188 
   189     action, meta = reactor.oninputeof()
   189     action, meta = reactor.oninputeof()
   190     meta['action'] = action
   190     meta[b'action'] = action
   191     states.append(templatefilters.json(meta))
   191     states.append(templatefilters.json(meta))
   192 
   192 
   193     res.status = b'200 OK'
   193     res.status = b'200 OK'
   194     res.headers[b'Content-Type'] = b'text/plain'
   194     res.headers[b'Content-Type'] = b'text/plain'
   195     res.setbodybytes(b'\n'.join(states))
   195     res.setbodybytes(b'\n'.join(states))
   214         if not frame:
   214         if not frame:
   215             break
   215             break
   216 
   216 
   217         action, meta = reactor.onframerecv(frame)
   217         action, meta = reactor.onframerecv(frame)
   218 
   218 
   219         if action == 'wantframe':
   219         if action == b'wantframe':
   220             # Need more data before we can do anything.
   220             # Need more data before we can do anything.
   221             continue
   221             continue
   222         elif action == 'runcommand':
   222         elif action == b'runcommand':
   223             # Defer creating output stream because we need to wait for
   223             # Defer creating output stream because we need to wait for
   224             # protocol settings frames so proper encoding can be applied.
   224             # protocol settings frames so proper encoding can be applied.
   225             if not outstream:
   225             if not outstream:
   226                 outstream = reactor.makeoutputstream()
   226                 outstream = reactor.makeoutputstream()
   227 
   227 
   241             if sentoutput:
   241             if sentoutput:
   242                 return
   242                 return
   243 
   243 
   244             seencommand = True
   244             seencommand = True
   245 
   245 
   246         elif action == 'error':
   246         elif action == b'error':
   247             # TODO define proper error mechanism.
   247             # TODO define proper error mechanism.
   248             res.status = b'200 OK'
   248             res.status = b'200 OK'
   249             res.headers[b'Content-Type'] = b'text/plain'
   249             res.headers[b'Content-Type'] = b'text/plain'
   250             res.setbodybytes(meta['message'] + b'\n')
   250             res.setbodybytes(meta[b'message'] + b'\n')
   251             return
   251             return
   252         else:
   252         else:
   253             raise error.ProgrammingError(
   253             raise error.ProgrammingError(
   254                 'unhandled action from frame processor: %s' % action
   254                 b'unhandled action from frame processor: %s' % action
   255             )
   255             )
   256 
   256 
   257     action, meta = reactor.oninputeof()
   257     action, meta = reactor.oninputeof()
   258     if action == 'sendframes':
   258     if action == b'sendframes':
   259         # We assume we haven't started sending the response yet. If we're
   259         # We assume we haven't started sending the response yet. If we're
   260         # wrong, the response type will raise an exception.
   260         # wrong, the response type will raise an exception.
   261         res.status = b'200 OK'
   261         res.status = b'200 OK'
   262         res.headers[b'Content-Type'] = FRAMINGTYPE
   262         res.headers[b'Content-Type'] = FRAMINGTYPE
   263         res.setbodygen(meta['framegen'])
   263         res.setbodygen(meta[b'framegen'])
   264     elif action == 'noop':
   264     elif action == b'noop':
   265         pass
   265         pass
   266     else:
   266     else:
   267         raise error.ProgrammingError(
   267         raise error.ProgrammingError(
   268             'unhandled action from frame processor: %s' % action
   268             b'unhandled action from frame processor: %s' % action
   269         )
   269         )
   270 
   270 
   271 
   271 
   272 def _httpv2runcommand(
   272 def _httpv2runcommand(
   273     ui,
   273     ui,
   299     # execute multiple commands. We double check permissions of each command
   299     # execute multiple commands. We double check permissions of each command
   300     # as it is invoked to ensure there is no privilege escalation.
   300     # as it is invoked to ensure there is no privilege escalation.
   301     # TODO consider allowing multiple commands to regular command URLs
   301     # TODO consider allowing multiple commands to regular command URLs
   302     # iff each command is the same.
   302     # iff each command is the same.
   303 
   303 
   304     proto = httpv2protocolhandler(req, ui, args=command['args'])
   304     proto = httpv2protocolhandler(req, ui, args=command[b'args'])
   305 
   305 
   306     if reqcommand == b'multirequest':
   306     if reqcommand == b'multirequest':
   307         if not COMMANDS.commandavailable(command['command'], proto):
   307         if not COMMANDS.commandavailable(command[b'command'], proto):
   308             # TODO proper error mechanism
   308             # TODO proper error mechanism
   309             res.status = b'200 OK'
   309             res.status = b'200 OK'
   310             res.headers[b'Content-Type'] = b'text/plain'
   310             res.headers[b'Content-Type'] = b'text/plain'
   311             res.setbodybytes(
   311             res.setbodybytes(
   312                 _('wire protocol command not available: %s')
   312                 _(b'wire protocol command not available: %s')
   313                 % command['command']
   313                 % command[b'command']
   314             )
   314             )
   315             return True
   315             return True
   316 
   316 
   317         # TODO don't use assert here, since it may be elided by -O.
   317         # TODO don't use assert here, since it may be elided by -O.
   318         assert authedperm in (b'ro', b'rw')
   318         assert authedperm in (b'ro', b'rw')
   319         wirecommand = COMMANDS[command['command']]
   319         wirecommand = COMMANDS[command[b'command']]
   320         assert wirecommand.permission in ('push', 'pull')
   320         assert wirecommand.permission in (b'push', b'pull')
   321 
   321 
   322         if authedperm == b'ro' and wirecommand.permission != 'pull':
   322         if authedperm == b'ro' and wirecommand.permission != b'pull':
   323             # TODO proper error mechanism
   323             # TODO proper error mechanism
   324             res.status = b'403 Forbidden'
   324             res.status = b'403 Forbidden'
   325             res.headers[b'Content-Type'] = b'text/plain'
   325             res.headers[b'Content-Type'] = b'text/plain'
   326             res.setbodybytes(
   326             res.setbodybytes(
   327                 _('insufficient permissions to execute ' 'command: %s')
   327                 _(b'insufficient permissions to execute ' b'command: %s')
   328                 % command['command']
   328                 % command[b'command']
   329             )
   329             )
   330             return True
   330             return True
   331 
   331 
   332         # TODO should we also call checkperm() here? Maybe not if we're going
   332         # TODO should we also call checkperm() here? Maybe not if we're going
   333         # to overhaul that API. The granted scope from the URL check should
   333         # to overhaul that API. The granted scope from the URL check should
   338         if issubsequent:
   338         if issubsequent:
   339             # TODO proper error mechanism
   339             # TODO proper error mechanism
   340             res.status = b'200 OK'
   340             res.status = b'200 OK'
   341             res.headers[b'Content-Type'] = b'text/plain'
   341             res.headers[b'Content-Type'] = b'text/plain'
   342             res.setbodybytes(
   342             res.setbodybytes(
   343                 _('multiple commands cannot be issued to this ' 'URL')
   343                 _(b'multiple commands cannot be issued to this ' b'URL')
   344             )
   344             )
   345             return True
   345             return True
   346 
   346 
   347         if reqcommand != command['command']:
   347         if reqcommand != command[b'command']:
   348             # TODO define proper error mechanism
   348             # TODO define proper error mechanism
   349             res.status = b'200 OK'
   349             res.status = b'200 OK'
   350             res.headers[b'Content-Type'] = b'text/plain'
   350             res.headers[b'Content-Type'] = b'text/plain'
   351             res.setbodybytes(_('command in frame must match command in URL'))
   351             res.setbodybytes(_(b'command in frame must match command in URL'))
   352             return True
   352             return True
   353 
   353 
   354     res.status = b'200 OK'
   354     res.status = b'200 OK'
   355     res.headers[b'Content-Type'] = FRAMINGTYPE
   355     res.headers[b'Content-Type'] = FRAMINGTYPE
   356 
   356 
   357     try:
   357     try:
   358         objs = dispatch(repo, proto, command['command'], command['redirect'])
   358         objs = dispatch(repo, proto, command[b'command'], command[b'redirect'])
   359 
   359 
   360         action, meta = reactor.oncommandresponsereadyobjects(
   360         action, meta = reactor.oncommandresponsereadyobjects(
   361             outstream, command['requestid'], objs
   361             outstream, command[b'requestid'], objs
   362         )
   362         )
   363 
   363 
   364     except error.WireprotoCommandError as e:
   364     except error.WireprotoCommandError as e:
   365         action, meta = reactor.oncommanderror(
   365         action, meta = reactor.oncommanderror(
   366             outstream, command['requestid'], e.message, e.messageargs
   366             outstream, command[b'requestid'], e.message, e.messageargs
   367         )
   367         )
   368 
   368 
   369     except Exception as e:
   369     except Exception as e:
   370         action, meta = reactor.onservererror(
   370         action, meta = reactor.onservererror(
   371             outstream,
   371             outstream,
   372             command['requestid'],
   372             command[b'requestid'],
   373             _('exception when invoking command: %s')
   373             _(b'exception when invoking command: %s')
   374             % stringutil.forcebytestr(e),
   374             % stringutil.forcebytestr(e),
   375         )
   375         )
   376 
   376 
   377     if action == 'sendframes':
   377     if action == b'sendframes':
   378         res.setbodygen(meta['framegen'])
   378         res.setbodygen(meta[b'framegen'])
   379         return True
   379         return True
   380     elif action == 'noop':
   380     elif action == b'noop':
   381         return False
   381         return False
   382     else:
   382     else:
   383         raise error.ProgrammingError(
   383         raise error.ProgrammingError(
   384             'unhandled event from reactor: %s' % action
   384             b'unhandled event from reactor: %s' % action
   385         )
   385         )
   386 
   386 
   387 
   387 
   388 def getdispatchrepo(repo, proto, command):
   388 def getdispatchrepo(repo, proto, command):
   389     viewconfig = repo.ui.config('server', 'view')
   389     viewconfig = repo.ui.config(b'server', b'view')
   390     return repo.filtered(viewconfig)
   390     return repo.filtered(viewconfig)
   391 
   391 
   392 
   392 
   393 def dispatch(repo, proto, command, redirect):
   393 def dispatch(repo, proto, command, redirect):
   394     """Run a wire protocol command.
   394     """Run a wire protocol command.
   451 
   451 
   452         # Serve it from the cache, if possible.
   452         # Serve it from the cache, if possible.
   453         cached = cacher.lookup()
   453         cached = cacher.lookup()
   454 
   454 
   455         if cached:
   455         if cached:
   456             for o in cached['objs']:
   456             for o in cached[b'objs']:
   457                 yield o
   457                 yield o
   458             return
   458             return
   459 
   459 
   460         # Else call the command and feed its output into the cacher, allowing
   460         # Else call the command and feed its output into the cacher, allowing
   461         # the cacher to buffer/mutate objects as it desires.
   461         # the cacher to buffer/mutate objects as it desires.
   482         # First look for args that were passed but aren't registered on this
   482         # First look for args that were passed but aren't registered on this
   483         # command.
   483         # command.
   484         extra = set(self._args) - set(args)
   484         extra = set(self._args) - set(args)
   485         if extra:
   485         if extra:
   486             raise error.WireprotoCommandError(
   486             raise error.WireprotoCommandError(
   487                 'unsupported argument to command: %s' % ', '.join(sorted(extra))
   487                 b'unsupported argument to command: %s'
       
   488                 % b', '.join(sorted(extra))
   488             )
   489             )
   489 
   490 
   490         # And look for required arguments that are missing.
   491         # And look for required arguments that are missing.
   491         missing = {a for a in args if args[a]['required']} - set(self._args)
   492         missing = {a for a in args if args[a][b'required']} - set(self._args)
   492 
   493 
   493         if missing:
   494         if missing:
   494             raise error.WireprotoCommandError(
   495             raise error.WireprotoCommandError(
   495                 'missing required arguments: %s' % ', '.join(sorted(missing))
   496                 b'missing required arguments: %s' % b', '.join(sorted(missing))
   496             )
   497             )
   497 
   498 
   498         # Now derive the arguments to pass to the command, taking into
   499         # Now derive the arguments to pass to the command, taking into
   499         # account the arguments specified by the client.
   500         # account the arguments specified by the client.
   500         data = {}
   501         data = {}
   501         for k, meta in sorted(args.items()):
   502         for k, meta in sorted(args.items()):
   502             # This argument wasn't passed by the client.
   503             # This argument wasn't passed by the client.
   503             if k not in self._args:
   504             if k not in self._args:
   504                 data[k] = meta['default']()
   505                 data[k] = meta[b'default']()
   505                 continue
   506                 continue
   506 
   507 
   507             v = self._args[k]
   508             v = self._args[k]
   508 
   509 
   509             # Sets may be expressed as lists. Silently normalize.
   510             # Sets may be expressed as lists. Silently normalize.
   510             if meta['type'] == 'set' and isinstance(v, list):
   511             if meta[b'type'] == b'set' and isinstance(v, list):
   511                 v = set(v)
   512                 v = set(v)
   512 
   513 
   513             # TODO consider more/stronger type validation.
   514             # TODO consider more/stronger type validation.
   514 
   515 
   515             data[k] = v
   516             data[k] = v
   548 
   549 
   549     These capabilities are distinct from the capabilities for version 1
   550     These capabilities are distinct from the capabilities for version 1
   550     transports.
   551     transports.
   551     """
   552     """
   552     caps = {
   553     caps = {
   553         'commands': {},
   554         b'commands': {},
   554         'framingmediatypes': [FRAMINGTYPE],
   555         b'framingmediatypes': [FRAMINGTYPE],
   555         'pathfilterprefixes': set(narrowspec.VALID_PREFIXES),
   556         b'pathfilterprefixes': set(narrowspec.VALID_PREFIXES),
   556     }
   557     }
   557 
   558 
   558     for command, entry in COMMANDS.items():
   559     for command, entry in COMMANDS.items():
   559         args = {}
   560         args = {}
   560 
   561 
   561         for arg, meta in entry.args.items():
   562         for arg, meta in entry.args.items():
   562             args[arg] = {
   563             args[arg] = {
   563                 # TODO should this be a normalized type using CBOR's
   564                 # TODO should this be a normalized type using CBOR's
   564                 # terminology?
   565                 # terminology?
   565                 b'type': meta['type'],
   566                 b'type': meta[b'type'],
   566                 b'required': meta['required'],
   567                 b'required': meta[b'required'],
   567             }
   568             }
   568 
   569 
   569             if not meta['required']:
   570             if not meta[b'required']:
   570                 args[arg][b'default'] = meta['default']()
   571                 args[arg][b'default'] = meta[b'default']()
   571 
   572 
   572             if meta['validvalues']:
   573             if meta[b'validvalues']:
   573                 args[arg][b'validvalues'] = meta['validvalues']
   574                 args[arg][b'validvalues'] = meta[b'validvalues']
   574 
   575 
   575         # TODO this type of check should be defined in a per-command callback.
   576         # TODO this type of check should be defined in a per-command callback.
   576         if (
   577         if (
   577             command == b'rawstorefiledata'
   578             command == b'rawstorefiledata'
   578             and not streamclone.allowservergeneration(repo)
   579             and not streamclone.allowservergeneration(repo)
   579         ):
   580         ):
   580             continue
   581             continue
   581 
   582 
   582         caps['commands'][command] = {
   583         caps[b'commands'][command] = {
   583             'args': args,
   584             b'args': args,
   584             'permissions': [entry.permission],
   585             b'permissions': [entry.permission],
   585         }
   586         }
   586 
   587 
   587         if entry.extracapabilitiesfn:
   588         if entry.extracapabilitiesfn:
   588             extracaps = entry.extracapabilitiesfn(repo, proto)
   589             extracaps = entry.extracapabilitiesfn(repo, proto)
   589             caps['commands'][command].update(extracaps)
   590             caps[b'commands'][command].update(extracaps)
   590 
   591 
   591     caps['rawrepoformats'] = sorted(repo.requirements & repo.supportedformats)
   592     caps[b'rawrepoformats'] = sorted(repo.requirements & repo.supportedformats)
   592 
   593 
   593     targets = getadvertisedredirecttargets(repo, proto)
   594     targets = getadvertisedredirecttargets(repo, proto)
   594     if targets:
   595     if targets:
   595         caps[b'redirect'] = {
   596         caps[b'redirect'] = {
   596             b'targets': [],
   597             b'targets': [],
   597             b'hashes': [b'sha256', b'sha1'],
   598             b'hashes': [b'sha256', b'sha1'],
   598         }
   599         }
   599 
   600 
   600         for target in targets:
   601         for target in targets:
   601             entry = {
   602             entry = {
   602                 b'name': target['name'],
   603                 b'name': target[b'name'],
   603                 b'protocol': target['protocol'],
   604                 b'protocol': target[b'protocol'],
   604                 b'uris': target['uris'],
   605                 b'uris': target[b'uris'],
   605             }
   606             }
   606 
   607 
   607             for key in ('snirequired', 'tlsversions'):
   608             for key in (b'snirequired', b'tlsversions'):
   608                 if key in target:
   609                 if key in target:
   609                     entry[key] = target[key]
   610                     entry[key] = target[key]
   610 
   611 
   611             caps[b'redirect'][b'targets'].append(entry)
   612             caps[b'redirect'][b'targets'].append(entry)
   612 
   613 
   653 
   654 
   654 
   655 
   655 def wireprotocommand(
   656 def wireprotocommand(
   656     name,
   657     name,
   657     args=None,
   658     args=None,
   658     permission='push',
   659     permission=b'push',
   659     cachekeyfn=None,
   660     cachekeyfn=None,
   660     extracapabilitiesfn=None,
   661     extracapabilitiesfn=None,
   661 ):
   662 ):
   662     """Decorator to declare a wire protocol command.
   663     """Decorator to declare a wire protocol command.
   663 
   664 
   708     argument containing the active cacher for the request and returns a bytes
   709     argument containing the active cacher for the request and returns a bytes
   709     containing the key in a cache the response to this command may be cached
   710     containing the key in a cache the response to this command may be cached
   710     under.
   711     under.
   711     """
   712     """
   712     transports = {
   713     transports = {
   713         k for k, v in wireprototypes.TRANSPORTS.items() if v['version'] == 2
   714         k for k, v in wireprototypes.TRANSPORTS.items() if v[b'version'] == 2
   714     }
   715     }
   715 
   716 
   716     if permission not in ('push', 'pull'):
   717     if permission not in (b'push', b'pull'):
   717         raise error.ProgrammingError(
   718         raise error.ProgrammingError(
   718             'invalid wire protocol permission; '
   719             b'invalid wire protocol permission; '
   719             'got %s; expected "push" or "pull"' % permission
   720             b'got %s; expected "push" or "pull"' % permission
   720         )
   721         )
   721 
   722 
   722     if args is None:
   723     if args is None:
   723         args = {}
   724         args = {}
   724 
   725 
   725     if not isinstance(args, dict):
   726     if not isinstance(args, dict):
   726         raise error.ProgrammingError(
   727         raise error.ProgrammingError(
   727             'arguments for version 2 commands ' 'must be declared as dicts'
   728             b'arguments for version 2 commands ' b'must be declared as dicts'
   728         )
   729         )
   729 
   730 
   730     for arg, meta in args.items():
   731     for arg, meta in args.items():
   731         if arg == '*':
   732         if arg == b'*':
   732             raise error.ProgrammingError(
   733             raise error.ProgrammingError(
   733                 '* argument name not allowed on ' 'version 2 commands'
   734                 b'* argument name not allowed on ' b'version 2 commands'
   734             )
   735             )
   735 
   736 
   736         if not isinstance(meta, dict):
   737         if not isinstance(meta, dict):
   737             raise error.ProgrammingError(
   738             raise error.ProgrammingError(
   738                 'arguments for version 2 commands '
   739                 b'arguments for version 2 commands '
   739                 'must declare metadata as a dict'
   740                 b'must declare metadata as a dict'
   740             )
   741             )
   741 
   742 
   742         if 'type' not in meta:
   743         if b'type' not in meta:
   743             raise error.ProgrammingError(
   744             raise error.ProgrammingError(
   744                 '%s argument for command %s does not '
   745                 b'%s argument for command %s does not '
   745                 'declare type field' % (arg, name)
   746                 b'declare type field' % (arg, name)
   746             )
   747             )
   747 
   748 
   748         if meta['type'] not in ('bytes', 'int', 'list', 'dict', 'set', 'bool'):
   749         if meta[b'type'] not in (
       
   750             b'bytes',
       
   751             b'int',
       
   752             b'list',
       
   753             b'dict',
       
   754             b'set',
       
   755             b'bool',
       
   756         ):
   749             raise error.ProgrammingError(
   757             raise error.ProgrammingError(
   750                 '%s argument for command %s has '
   758                 b'%s argument for command %s has '
   751                 'illegal type: %s' % (arg, name, meta['type'])
   759                 b'illegal type: %s' % (arg, name, meta[b'type'])
   752             )
   760             )
   753 
   761 
   754         if 'example' not in meta:
   762         if b'example' not in meta:
   755             raise error.ProgrammingError(
   763             raise error.ProgrammingError(
   756                 '%s argument for command %s does not '
   764                 b'%s argument for command %s does not '
   757                 'declare example field' % (arg, name)
   765                 b'declare example field' % (arg, name)
   758             )
   766             )
   759 
   767 
   760         meta['required'] = 'default' not in meta
   768         meta[b'required'] = b'default' not in meta
   761 
   769 
   762         meta.setdefault('default', lambda: None)
   770         meta.setdefault(b'default', lambda: None)
   763         meta.setdefault('validvalues', None)
   771         meta.setdefault(b'validvalues', None)
   764 
   772 
   765     def register(func):
   773     def register(func):
   766         if name in COMMANDS:
   774         if name in COMMANDS:
   767             raise error.ProgrammingError(
   775             raise error.ProgrammingError(
   768                 '%s command already registered ' 'for version 2' % name
   776                 b'%s command already registered ' b'for version 2' % name
   769             )
   777             )
   770 
   778 
   771         COMMANDS[name] = wireprototypes.commandentry(
   779         COMMANDS[name] = wireprototypes.commandentry(
   772             func,
   780             func,
   773             args=args,
   781             args=args,
   794     * The media type used.
   802     * The media type used.
   795     * Wire protocol version string.
   803     * Wire protocol version string.
   796     * The repository path.
   804     * The repository path.
   797     """
   805     """
   798     if not allargs:
   806     if not allargs:
   799         raise error.ProgrammingError('only allargs=True is currently supported')
   807         raise error.ProgrammingError(
       
   808             b'only allargs=True is currently supported'
       
   809         )
   800 
   810 
   801     if localversion is None:
   811     if localversion is None:
   802         raise error.ProgrammingError('must set localversion argument value')
   812         raise error.ProgrammingError(b'must set localversion argument value')
   803 
   813 
   804     def cachekeyfn(repo, proto, cacher, **args):
   814     def cachekeyfn(repo, proto, cacher, **args):
   805         spec = COMMANDS[command]
   815         spec = COMMANDS[command]
   806 
   816 
   807         # Commands that mutate the repo can not be cached.
   817         # Commands that mutate the repo can not be cached.
   808         if spec.permission == 'push':
   818         if spec.permission == b'push':
   809             return None
   819             return None
   810 
   820 
   811         # TODO config option to disable caching.
   821         # TODO config option to disable caching.
   812 
   822 
   813         # Our key derivation strategy is to construct a data structure
   823         # Our key derivation strategy is to construct a data structure
   878     seen = set()
   888     seen = set()
   879     nodes = []
   889     nodes = []
   880 
   890 
   881     if not isinstance(revisions, list):
   891     if not isinstance(revisions, list):
   882         raise error.WireprotoCommandError(
   892         raise error.WireprotoCommandError(
   883             'revisions must be defined as an ' 'array'
   893             b'revisions must be defined as an ' b'array'
   884         )
   894         )
   885 
   895 
   886     for spec in revisions:
   896     for spec in revisions:
   887         if b'type' not in spec:
   897         if b'type' not in spec:
   888             raise error.WireprotoCommandError(
   898             raise error.WireprotoCommandError(
   889                 'type key not present in revision specifier'
   899                 b'type key not present in revision specifier'
   890             )
   900             )
   891 
   901 
   892         typ = spec[b'type']
   902         typ = spec[b'type']
   893 
   903 
   894         if typ == b'changesetexplicit':
   904         if typ == b'changesetexplicit':
   895             if b'nodes' not in spec:
   905             if b'nodes' not in spec:
   896                 raise error.WireprotoCommandError(
   906                 raise error.WireprotoCommandError(
   897                     'nodes key not present in changesetexplicit revision '
   907                     b'nodes key not present in changesetexplicit revision '
   898                     'specifier'
   908                     b'specifier'
   899                 )
   909                 )
   900 
   910 
   901             for node in spec[b'nodes']:
   911             for node in spec[b'nodes']:
   902                 if node not in seen:
   912                 if node not in seen:
   903                     nodes.append(node)
   913                     nodes.append(node)
   905 
   915 
   906         elif typ == b'changesetexplicitdepth':
   916         elif typ == b'changesetexplicitdepth':
   907             for key in (b'nodes', b'depth'):
   917             for key in (b'nodes', b'depth'):
   908                 if key not in spec:
   918                 if key not in spec:
   909                     raise error.WireprotoCommandError(
   919                     raise error.WireprotoCommandError(
   910                         '%s key not present in changesetexplicitdepth revision '
   920                         b'%s key not present in changesetexplicitdepth revision '
   911                         'specifier',
   921                         b'specifier',
   912                         (key,),
   922                         (key,),
   913                     )
   923                     )
   914 
   924 
   915             for rev in repo.revs(
   925             for rev in repo.revs(
   916                 b'ancestors(%ln, %s)', spec[b'nodes'], spec[b'depth'] - 1
   926                 b'ancestors(%ln, %s)', spec[b'nodes'], spec[b'depth'] - 1
   923 
   933 
   924         elif typ == b'changesetdagrange':
   934         elif typ == b'changesetdagrange':
   925             for key in (b'roots', b'heads'):
   935             for key in (b'roots', b'heads'):
   926                 if key not in spec:
   936                 if key not in spec:
   927                     raise error.WireprotoCommandError(
   937                     raise error.WireprotoCommandError(
   928                         '%s key not present in changesetdagrange revision '
   938                         b'%s key not present in changesetdagrange revision '
   929                         'specifier',
   939                         b'specifier',
   930                         (key,),
   940                         (key,),
   931                     )
   941                     )
   932 
   942 
   933             if not spec[b'heads']:
   943             if not spec[b'heads']:
   934                 raise error.WireprotoCommandError(
   944                 raise error.WireprotoCommandError(
   935                     'heads key in changesetdagrange cannot be empty'
   945                     b'heads key in changesetdagrange cannot be empty'
   936                 )
   946                 )
   937 
   947 
   938             if spec[b'roots']:
   948             if spec[b'roots']:
   939                 common = [n for n in spec[b'roots'] if clhasnode(n)]
   949                 common = [n for n in spec[b'roots'] if clhasnode(n)]
   940             else:
   950             else:
   945                     nodes.append(n)
   955                     nodes.append(n)
   946                     seen.add(n)
   956                     seen.add(n)
   947 
   957 
   948         else:
   958         else:
   949             raise error.WireprotoCommandError(
   959             raise error.WireprotoCommandError(
   950                 'unknown revision specifier type: %s', (typ,)
   960                 b'unknown revision specifier type: %s', (typ,)
   951             )
   961             )
   952 
   962 
   953     return nodes
   963     return nodes
   954 
   964 
   955 
   965 
   956 @wireprotocommand('branchmap', permission='pull')
   966 @wireprotocommand(b'branchmap', permission=b'pull')
   957 def branchmapv2(repo, proto):
   967 def branchmapv2(repo, proto):
   958     yield {encoding.fromlocal(k): v for k, v in repo.branchmap().iteritems()}
   968     yield {encoding.fromlocal(k): v for k, v in repo.branchmap().iteritems()}
   959 
   969 
   960 
   970 
   961 @wireprotocommand('capabilities', permission='pull')
   971 @wireprotocommand(b'capabilities', permission=b'pull')
   962 def capabilitiesv2(repo, proto):
   972 def capabilitiesv2(repo, proto):
   963     yield _capabilitiesv2(repo, proto)
   973     yield _capabilitiesv2(repo, proto)
   964 
   974 
   965 
   975 
   966 @wireprotocommand(
   976 @wireprotocommand(
   967     'changesetdata',
   977     b'changesetdata',
   968     args={
   978     args={
   969         'revisions': {
   979         b'revisions': {
   970             'type': 'list',
   980             b'type': b'list',
   971             'example': [
   981             b'example': [
   972                 {b'type': b'changesetexplicit', b'nodes': [b'abcdef...'],}
   982                 {b'type': b'changesetexplicit', b'nodes': [b'abcdef...'],}
   973             ],
   983             ],
   974         },
   984         },
   975         'fields': {
   985         b'fields': {
   976             'type': 'set',
   986             b'type': b'set',
   977             'default': set,
   987             b'default': set,
   978             'example': {b'parents', b'revision'},
   988             b'example': {b'parents', b'revision'},
   979             'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'},
   989             b'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'},
   980         },
   990         },
   981     },
   991     },
   982     permission='pull',
   992     permission=b'pull',
   983 )
   993 )
   984 def changesetdata(repo, proto, revisions, fields):
   994 def changesetdata(repo, proto, revisions, fields):
   985     # TODO look for unknown fields and abort when they can't be serviced.
   995     # TODO look for unknown fields and abort when they can't be serviced.
   986     # This could probably be validated by dispatcher using validvalues.
   996     # This could probably be validated by dispatcher using validvalues.
   987 
   997 
   988     cl = repo.changelog
   998     cl = repo.changelog
   989     outgoing = resolvenodes(repo, revisions)
   999     outgoing = resolvenodes(repo, revisions)
   990     publishing = repo.publishing()
  1000     publishing = repo.publishing()
   991 
  1001 
   992     if outgoing:
  1002     if outgoing:
   993         repo.hook('preoutgoing', throw=True, source='serve')
  1003         repo.hook(b'preoutgoing', throw=True, source=b'serve')
   994 
  1004 
   995     yield {
  1005     yield {
   996         b'totalitems': len(outgoing),
  1006         b'totalitems': len(outgoing),
   997     }
  1007     }
   998 
  1008 
  1076     # This seems to work even if the file doesn't exist. So catch
  1086     # This seems to work even if the file doesn't exist. So catch
  1077     # "empty" files and return an error.
  1087     # "empty" files and return an error.
  1078     fl = repo.file(path)
  1088     fl = repo.file(path)
  1079 
  1089 
  1080     if not len(fl):
  1090     if not len(fl):
  1081         raise FileAccessError(path, 'unknown file: %s', (path,))
  1091         raise FileAccessError(path, b'unknown file: %s', (path,))
  1082 
  1092 
  1083     return fl
  1093     return fl
  1084 
  1094 
  1085 
  1095 
  1086 def emitfilerevisions(repo, path, revisions, linknodes, fields):
  1096 def emitfilerevisions(repo, path, revisions, linknodes, fields):
  1123     if pathfilter:
  1133     if pathfilter:
  1124         for key in (b'include', b'exclude'):
  1134         for key in (b'include', b'exclude'):
  1125             for pattern in pathfilter.get(key, []):
  1135             for pattern in pathfilter.get(key, []):
  1126                 if not pattern.startswith((b'path:', b'rootfilesin:')):
  1136                 if not pattern.startswith((b'path:', b'rootfilesin:')):
  1127                     raise error.WireprotoCommandError(
  1137                     raise error.WireprotoCommandError(
  1128                         '%s pattern must begin with `path:` or `rootfilesin:`; '
  1138                         b'%s pattern must begin with `path:` or `rootfilesin:`; '
  1129                         'got %s',
  1139                         b'got %s',
  1130                         (key, pattern),
  1140                         (key, pattern),
  1131                     )
  1141                     )
  1132 
  1142 
  1133     if pathfilter:
  1143     if pathfilter:
  1134         matcher = matchmod.match(
  1144         matcher = matchmod.match(
  1144     # filter those out.
  1154     # filter those out.
  1145     return repo.narrowmatch(matcher)
  1155     return repo.narrowmatch(matcher)
  1146 
  1156 
  1147 
  1157 
  1148 @wireprotocommand(
  1158 @wireprotocommand(
  1149     'filedata',
  1159     b'filedata',
  1150     args={
  1160     args={
  1151         'haveparents': {
  1161         b'haveparents': {
  1152             'type': 'bool',
  1162             b'type': b'bool',
  1153             'default': lambda: False,
  1163             b'default': lambda: False,
  1154             'example': True,
  1164             b'example': True,
  1155         },
  1165         },
  1156         'nodes': {'type': 'list', 'example': [b'0123456...'],},
  1166         b'nodes': {b'type': b'list', b'example': [b'0123456...'],},
  1157         'fields': {
  1167         b'fields': {
  1158             'type': 'set',
  1168             b'type': b'set',
  1159             'default': set,
  1169             b'default': set,
  1160             'example': {b'parents', b'revision'},
  1170             b'example': {b'parents', b'revision'},
  1161             'validvalues': {b'parents', b'revision', b'linknode'},
  1171             b'validvalues': {b'parents', b'revision', b'linknode'},
  1162         },
  1172         },
  1163         'path': {'type': 'bytes', 'example': b'foo.txt',},
  1173         b'path': {b'type': b'bytes', b'example': b'foo.txt',},
  1164     },
  1174     },
  1165     permission='pull',
  1175     permission=b'pull',
  1166     # TODO censoring a file revision won't invalidate the cache.
  1176     # TODO censoring a file revision won't invalidate the cache.
  1167     # Figure out a way to take censoring into account when deriving
  1177     # Figure out a way to take censoring into account when deriving
  1168     # the cache key.
  1178     # the cache key.
  1169     cachekeyfn=makecommandcachekeyfn('filedata', 1, allargs=True),
  1179     cachekeyfn=makecommandcachekeyfn(b'filedata', 1, allargs=True),
  1170 )
  1180 )
  1171 def filedata(repo, proto, haveparents, nodes, fields, path):
  1181 def filedata(repo, proto, haveparents, nodes, fields, path):
  1172     # TODO this API allows access to file revisions that are attached to
  1182     # TODO this API allows access to file revisions that are attached to
  1173     # secret changesets. filesdata does not have this problem. Maybe this
  1183     # secret changesets. filesdata does not have this problem. Maybe this
  1174     # API should be deleted?
  1184     # API should be deleted?
  1186     for node in nodes:
  1196     for node in nodes:
  1187         try:
  1197         try:
  1188             store.rev(node)
  1198             store.rev(node)
  1189         except error.LookupError:
  1199         except error.LookupError:
  1190             raise error.WireprotoCommandError(
  1200             raise error.WireprotoCommandError(
  1191                 'unknown file node: %s', (hex(node),)
  1201                 b'unknown file node: %s', (hex(node),)
  1192             )
  1202             )
  1193 
  1203 
  1194         # TODO by creating the filectx against a specific file revision
  1204         # TODO by creating the filectx against a specific file revision
  1195         # instead of changeset, linkrev() is always used. This is wrong for
  1205         # instead of changeset, linkrev() is always used. This is wrong for
  1196         # cases where linkrev() may refer to a hidden changeset. But since this
  1206         # cases where linkrev() may refer to a hidden changeset. But since this
  1221         b'recommendedbatchsize': batchsize,
  1231         b'recommendedbatchsize': batchsize,
  1222     }
  1232     }
  1223 
  1233 
  1224 
  1234 
  1225 @wireprotocommand(
  1235 @wireprotocommand(
  1226     'filesdata',
  1236     b'filesdata',
  1227     args={
  1237     args={
  1228         'haveparents': {
  1238         b'haveparents': {
  1229             'type': 'bool',
  1239             b'type': b'bool',
  1230             'default': lambda: False,
  1240             b'default': lambda: False,
  1231             'example': True,
  1241             b'example': True,
  1232         },
  1242         },
  1233         'fields': {
  1243         b'fields': {
  1234             'type': 'set',
  1244             b'type': b'set',
  1235             'default': set,
  1245             b'default': set,
  1236             'example': {b'parents', b'revision'},
  1246             b'example': {b'parents', b'revision'},
  1237             'validvalues': {
  1247             b'validvalues': {
  1238                 b'firstchangeset',
  1248                 b'firstchangeset',
  1239                 b'linknode',
  1249                 b'linknode',
  1240                 b'parents',
  1250                 b'parents',
  1241                 b'revision',
  1251                 b'revision',
  1242             },
  1252             },
  1243         },
  1253         },
  1244         'pathfilter': {
  1254         b'pathfilter': {
  1245             'type': 'dict',
  1255             b'type': b'dict',
  1246             'default': lambda: None,
  1256             b'default': lambda: None,
  1247             'example': {b'include': [b'path:tests']},
  1257             b'example': {b'include': [b'path:tests']},
  1248         },
  1258         },
  1249         'revisions': {
  1259         b'revisions': {
  1250             'type': 'list',
  1260             b'type': b'list',
  1251             'example': [
  1261             b'example': [
  1252                 {b'type': b'changesetexplicit', b'nodes': [b'abcdef...'],}
  1262                 {b'type': b'changesetexplicit', b'nodes': [b'abcdef...'],}
  1253             ],
  1263             ],
  1254         },
  1264         },
  1255     },
  1265     },
  1256     permission='pull',
  1266     permission=b'pull',
  1257     # TODO censoring a file revision won't invalidate the cache.
  1267     # TODO censoring a file revision won't invalidate the cache.
  1258     # Figure out a way to take censoring into account when deriving
  1268     # Figure out a way to take censoring into account when deriving
  1259     # the cache key.
  1269     # the cache key.
  1260     cachekeyfn=makecommandcachekeyfn('filesdata', 1, allargs=True),
  1270     cachekeyfn=makecommandcachekeyfn(b'filesdata', 1, allargs=True),
  1261     extracapabilitiesfn=filesdatacapabilities,
  1271     extracapabilitiesfn=filesdatacapabilities,
  1262 )
  1272 )
  1263 def filesdata(repo, proto, haveparents, fields, pathfilter, revisions):
  1273 def filesdata(repo, proto, haveparents, fields, pathfilter, revisions):
  1264     # TODO This should operate on a repo that exposes obsolete changesets. There
  1274     # TODO This should operate on a repo that exposes obsolete changesets. There
  1265     # is a race between a client making a push that obsoletes a changeset and
  1275     # is a race between a client making a push that obsoletes a changeset and
  1325         for o in emitfilerevisions(repo, path, revisions, filenodes, fields):
  1335         for o in emitfilerevisions(repo, path, revisions, filenodes, fields):
  1326             yield o
  1336             yield o
  1327 
  1337 
  1328 
  1338 
  1329 @wireprotocommand(
  1339 @wireprotocommand(
  1330     'heads',
  1340     b'heads',
  1331     args={
  1341     args={
  1332         'publiconly': {
  1342         b'publiconly': {
  1333             'type': 'bool',
  1343             b'type': b'bool',
  1334             'default': lambda: False,
  1344             b'default': lambda: False,
  1335             'example': False,
  1345             b'example': False,
  1336         },
  1346         },
  1337     },
  1347     },
  1338     permission='pull',
  1348     permission=b'pull',
  1339 )
  1349 )
  1340 def headsv2(repo, proto, publiconly):
  1350 def headsv2(repo, proto, publiconly):
  1341     if publiconly:
  1351     if publiconly:
  1342         repo = repo.filtered('immutable')
  1352         repo = repo.filtered(b'immutable')
  1343 
  1353 
  1344     yield repo.heads()
  1354     yield repo.heads()
  1345 
  1355 
  1346 
  1356 
  1347 @wireprotocommand(
  1357 @wireprotocommand(
  1348     'known',
  1358     b'known',
  1349     args={
  1359     args={
  1350         'nodes': {'type': 'list', 'default': list, 'example': [b'deadbeef'],},
  1360         b'nodes': {
       
  1361             b'type': b'list',
       
  1362             b'default': list,
       
  1363             b'example': [b'deadbeef'],
       
  1364         },
  1351     },
  1365     },
  1352     permission='pull',
  1366     permission=b'pull',
  1353 )
  1367 )
  1354 def knownv2(repo, proto, nodes):
  1368 def knownv2(repo, proto, nodes):
  1355     result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
  1369     result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
  1356     yield result
  1370     yield result
  1357 
  1371 
  1358 
  1372 
  1359 @wireprotocommand(
  1373 @wireprotocommand(
  1360     'listkeys',
  1374     b'listkeys',
  1361     args={'namespace': {'type': 'bytes', 'example': b'ns',},},
  1375     args={b'namespace': {b'type': b'bytes', b'example': b'ns',},},
  1362     permission='pull',
  1376     permission=b'pull',
  1363 )
  1377 )
  1364 def listkeysv2(repo, proto, namespace):
  1378 def listkeysv2(repo, proto, namespace):
  1365     keys = repo.listkeys(encoding.tolocal(namespace))
  1379     keys = repo.listkeys(encoding.tolocal(namespace))
  1366     keys = {
  1380     keys = {
  1367         encoding.fromlocal(k): encoding.fromlocal(v)
  1381         encoding.fromlocal(k): encoding.fromlocal(v)
  1370 
  1384 
  1371     yield keys
  1385     yield keys
  1372 
  1386 
  1373 
  1387 
  1374 @wireprotocommand(
  1388 @wireprotocommand(
  1375     'lookup',
  1389     b'lookup',
  1376     args={'key': {'type': 'bytes', 'example': b'foo',},},
  1390     args={b'key': {b'type': b'bytes', b'example': b'foo',},},
  1377     permission='pull',
  1391     permission=b'pull',
  1378 )
  1392 )
  1379 def lookupv2(repo, proto, key):
  1393 def lookupv2(repo, proto, key):
  1380     key = encoding.tolocal(key)
  1394     key = encoding.tolocal(key)
  1381 
  1395 
  1382     # TODO handle exception.
  1396     # TODO handle exception.
  1394         b'recommendedbatchsize': batchsize,
  1408         b'recommendedbatchsize': batchsize,
  1395     }
  1409     }
  1396 
  1410 
  1397 
  1411 
  1398 @wireprotocommand(
  1412 @wireprotocommand(
  1399     'manifestdata',
  1413     b'manifestdata',
  1400     args={
  1414     args={
  1401         'nodes': {'type': 'list', 'example': [b'0123456...'],},
  1415         b'nodes': {b'type': b'list', b'example': [b'0123456...'],},
  1402         'haveparents': {
  1416         b'haveparents': {
  1403             'type': 'bool',
  1417             b'type': b'bool',
  1404             'default': lambda: False,
  1418             b'default': lambda: False,
  1405             'example': True,
  1419             b'example': True,
  1406         },
  1420         },
  1407         'fields': {
  1421         b'fields': {
  1408             'type': 'set',
  1422             b'type': b'set',
  1409             'default': set,
  1423             b'default': set,
  1410             'example': {b'parents', b'revision'},
  1424             b'example': {b'parents', b'revision'},
  1411             'validvalues': {b'parents', b'revision'},
  1425             b'validvalues': {b'parents', b'revision'},
  1412         },
  1426         },
  1413         'tree': {'type': 'bytes', 'example': b'',},
  1427         b'tree': {b'type': b'bytes', b'example': b'',},
  1414     },
  1428     },
  1415     permission='pull',
  1429     permission=b'pull',
  1416     cachekeyfn=makecommandcachekeyfn('manifestdata', 1, allargs=True),
  1430     cachekeyfn=makecommandcachekeyfn(b'manifestdata', 1, allargs=True),
  1417     extracapabilitiesfn=manifestdatacapabilities,
  1431     extracapabilitiesfn=manifestdatacapabilities,
  1418 )
  1432 )
  1419 def manifestdata(repo, proto, haveparents, nodes, fields, tree):
  1433 def manifestdata(repo, proto, haveparents, nodes, fields, tree):
  1420     store = repo.manifestlog.getstorage(tree)
  1434     store = repo.manifestlog.getstorage(tree)
  1421 
  1435 
  1422     # Validate the node is known and abort on unknown revisions.
  1436     # Validate the node is known and abort on unknown revisions.
  1423     for node in nodes:
  1437     for node in nodes:
  1424         try:
  1438         try:
  1425             store.rev(node)
  1439             store.rev(node)
  1426         except error.LookupError:
  1440         except error.LookupError:
  1427             raise error.WireprotoCommandError('unknown node: %s', (node,))
  1441             raise error.WireprotoCommandError(b'unknown node: %s', (node,))
  1428 
  1442 
  1429     revisions = store.emitrevisions(
  1443     revisions = store.emitrevisions(
  1430         nodes,
  1444         nodes,
  1431         revisiondata=b'revision' in fields,
  1445         revisiondata=b'revision' in fields,
  1432         assumehaveparentrevisions=haveparents,
  1446         assumehaveparentrevisions=haveparents,
  1464         for extra in followingdata:
  1478         for extra in followingdata:
  1465             yield extra
  1479             yield extra
  1466 
  1480 
  1467 
  1481 
  1468 @wireprotocommand(
  1482 @wireprotocommand(
  1469     'pushkey',
  1483     b'pushkey',
  1470     args={
  1484     args={
  1471         'namespace': {'type': 'bytes', 'example': b'ns',},
  1485         b'namespace': {b'type': b'bytes', b'example': b'ns',},
  1472         'key': {'type': 'bytes', 'example': b'key',},
  1486         b'key': {b'type': b'bytes', b'example': b'key',},
  1473         'old': {'type': 'bytes', 'example': b'old',},
  1487         b'old': {b'type': b'bytes', b'example': b'old',},
  1474         'new': {'type': 'bytes', 'example': 'new',},
  1488         b'new': {b'type': b'bytes', b'example': b'new',},
  1475     },
  1489     },
  1476     permission='push',
  1490     permission=b'push',
  1477 )
  1491 )
  1478 def pushkeyv2(repo, proto, namespace, key, old, new):
  1492 def pushkeyv2(repo, proto, namespace, key, old, new):
  1479     # TODO handle ui output redirection
  1493     # TODO handle ui output redirection
  1480     yield repo.pushkey(
  1494     yield repo.pushkey(
  1481         encoding.tolocal(namespace),
  1495         encoding.tolocal(namespace),
  1484         encoding.tolocal(new),
  1498         encoding.tolocal(new),
  1485     )
  1499     )
  1486 
  1500 
  1487 
  1501 
  1488 @wireprotocommand(
  1502 @wireprotocommand(
  1489     'rawstorefiledata',
  1503     b'rawstorefiledata',
  1490     args={
  1504     args={
  1491         'files': {'type': 'list', 'example': [b'changelog', b'manifestlog'],},
  1505         b'files': {
  1492         'pathfilter': {
  1506             b'type': b'list',
  1493             'type': 'list',
  1507             b'example': [b'changelog', b'manifestlog'],
  1494             'default': lambda: None,
  1508         },
  1495             'example': {b'include': [b'path:tests']},
  1509         b'pathfilter': {
       
  1510             b'type': b'list',
       
  1511             b'default': lambda: None,
       
  1512             b'example': {b'include': [b'path:tests']},
  1496         },
  1513         },
  1497     },
  1514     },
  1498     permission='pull',
  1515     permission=b'pull',
  1499 )
  1516 )
  1500 def rawstorefiledata(repo, proto, files, pathfilter):
  1517 def rawstorefiledata(repo, proto, files, pathfilter):
  1501     if not streamclone.allowservergeneration(repo):
  1518     if not streamclone.allowservergeneration(repo):
  1502         raise error.WireprotoCommandError(b'stream clone is disabled')
  1519         raise error.WireprotoCommandError(b'stream clone is disabled')
  1503 
  1520 
  1544         }
  1561         }
  1545 
  1562 
  1546         # We have to use a closure for this to ensure the context manager is
  1563         # We have to use a closure for this to ensure the context manager is
  1547         # closed only after sending the final chunk.
  1564         # closed only after sending the final chunk.
  1548         def getfiledata():
  1565         def getfiledata():
  1549             with repo.svfs(name, 'rb', auditpath=False) as fh:
  1566             with repo.svfs(name, b'rb', auditpath=False) as fh:
  1550                 for chunk in util.filechunkiter(fh, limit=size):
  1567                 for chunk in util.filechunkiter(fh, limit=size):
  1551                     yield chunk
  1568                     yield chunk
  1552 
  1569 
  1553         yield wireprototypes.indefinitebytestringresponse(getfiledata())
  1570         yield wireprototypes.indefinitebytestringresponse(getfiledata())