mercurial/hgweb/hgweb_mod.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43089 c59eb1560c44
equal deleted inserted replaced
43076:2372284d9457 43077:687b865b95ad
    46 )
    46 )
    47 
    47 
    48 
    48 
    49 def getstyle(req, configfn, templatepath):
    49 def getstyle(req, configfn, templatepath):
    50     styles = (
    50     styles = (
    51         req.qsparams.get('style', None),
    51         req.qsparams.get(b'style', None),
    52         configfn('web', 'style'),
    52         configfn(b'web', b'style'),
    53         'paper',
    53         b'paper',
    54     )
    54     )
    55     return styles, templater.stylemap(styles, templatepath)
    55     return styles, templater.stylemap(styles, templatepath)
    56 
    56 
    57 
    57 
    58 def makebreadcrumb(url, prefix=''):
    58 def makebreadcrumb(url, prefix=b''):
    59     '''Return a 'URL breadcrumb' list
    59     '''Return a 'URL breadcrumb' list
    60 
    60 
    61     A 'URL breadcrumb' is a list of URL-name pairs,
    61     A 'URL breadcrumb' is a list of URL-name pairs,
    62     corresponding to each of the path items on a URL.
    62     corresponding to each of the path items on a URL.
    63     This can be used to create path navigation entries.
    63     This can be used to create path navigation entries.
    64     '''
    64     '''
    65     if url.endswith('/'):
    65     if url.endswith(b'/'):
    66         url = url[:-1]
    66         url = url[:-1]
    67     if prefix:
    67     if prefix:
    68         url = '/' + prefix + url
    68         url = b'/' + prefix + url
    69     relpath = url
    69     relpath = url
    70     if relpath.startswith('/'):
    70     if relpath.startswith(b'/'):
    71         relpath = relpath[1:]
    71         relpath = relpath[1:]
    72 
    72 
    73     breadcrumb = []
    73     breadcrumb = []
    74     urlel = url
    74     urlel = url
    75     pathitems = [''] + relpath.split('/')
    75     pathitems = [b''] + relpath.split(b'/')
    76     for pathel in reversed(pathitems):
    76     for pathel in reversed(pathitems):
    77         if not pathel or not urlel:
    77         if not pathel or not urlel:
    78             break
    78             break
    79         breadcrumb.append({'url': urlel, 'name': pathel})
    79         breadcrumb.append({b'url': urlel, b'name': pathel})
    80         urlel = os.path.dirname(urlel)
    80         urlel = os.path.dirname(urlel)
    81     return templateutil.mappinglist(reversed(breadcrumb))
    81     return templateutil.mappinglist(reversed(breadcrumb))
    82 
    82 
    83 
    83 
    84 class requestcontext(object):
    84 class requestcontext(object):
    93         self.repo = repo
    93         self.repo = repo
    94         self.reponame = app.reponame
    94         self.reponame = app.reponame
    95         self.req = req
    95         self.req = req
    96         self.res = res
    96         self.res = res
    97 
    97 
    98         self.maxchanges = self.configint('web', 'maxchanges')
    98         self.maxchanges = self.configint(b'web', b'maxchanges')
    99         self.stripecount = self.configint('web', 'stripes')
    99         self.stripecount = self.configint(b'web', b'stripes')
   100         self.maxshortchanges = self.configint('web', 'maxshortchanges')
   100         self.maxshortchanges = self.configint(b'web', b'maxshortchanges')
   101         self.maxfiles = self.configint('web', 'maxfiles')
   101         self.maxfiles = self.configint(b'web', b'maxfiles')
   102         self.allowpull = self.configbool('web', 'allow-pull')
   102         self.allowpull = self.configbool(b'web', b'allow-pull')
   103 
   103 
   104         # we use untrusted=False to prevent a repo owner from using
   104         # we use untrusted=False to prevent a repo owner from using
   105         # web.templates in .hg/hgrc to get access to any file readable
   105         # web.templates in .hg/hgrc to get access to any file readable
   106         # by the user running the CGI script
   106         # by the user running the CGI script
   107         self.templatepath = self.config('web', 'templates', untrusted=False)
   107         self.templatepath = self.config(b'web', b'templates', untrusted=False)
   108 
   108 
   109         # This object is more expensive to build than simple config values.
   109         # This object is more expensive to build than simple config values.
   110         # It is shared across requests. The app will replace the object
   110         # It is shared across requests. The app will replace the object
   111         # if it is updated. Since this is a reference and nothing should
   111         # if it is updated. Since this is a reference and nothing should
   112         # modify the underlying object, it should be constant for the lifetime
   112         # modify the underlying object, it should be constant for the lifetime
   138         return webutil.archivelist(self.repo.ui, nodeid)
   138         return webutil.archivelist(self.repo.ui, nodeid)
   139 
   139 
   140     def templater(self, req):
   140     def templater(self, req):
   141         # determine scheme, port and server name
   141         # determine scheme, port and server name
   142         # this is needed to create absolute urls
   142         # this is needed to create absolute urls
   143         logourl = self.config('web', 'logourl')
   143         logourl = self.config(b'web', b'logourl')
   144         logoimg = self.config('web', 'logoimg')
   144         logoimg = self.config(b'web', b'logoimg')
   145         staticurl = (
   145         staticurl = (
   146             self.config('web', 'staticurl')
   146             self.config(b'web', b'staticurl')
   147             or req.apppath.rstrip('/') + '/static/'
   147             or req.apppath.rstrip(b'/') + b'/static/'
   148         )
   148         )
   149         if not staticurl.endswith('/'):
   149         if not staticurl.endswith(b'/'):
   150             staticurl += '/'
   150             staticurl += b'/'
   151 
   151 
   152         # figure out which style to use
   152         # figure out which style to use
   153 
   153 
   154         vars = {}
   154         vars = {}
   155         styles, (style, mapfile) = getstyle(req, self.config, self.templatepath)
   155         styles, (style, mapfile) = getstyle(req, self.config, self.templatepath)
   156         if style == styles[0]:
   156         if style == styles[0]:
   157             vars['style'] = style
   157             vars[b'style'] = style
   158 
   158 
   159         sessionvars = webutil.sessionvars(vars, '?')
   159         sessionvars = webutil.sessionvars(vars, b'?')
   160 
   160 
   161         if not self.reponame:
   161         if not self.reponame:
   162             self.reponame = (
   162             self.reponame = (
   163                 self.config('web', 'name', '')
   163                 self.config(b'web', b'name', b'')
   164                 or req.reponame
   164                 or req.reponame
   165                 or req.apppath
   165                 or req.apppath
   166                 or self.repo.root
   166                 or self.repo.root
   167             )
   167             )
   168 
   168 
   169         filters = {}
   169         filters = {}
   170         templatefilter = registrar.templatefilter(filters)
   170         templatefilter = registrar.templatefilter(filters)
   171 
   171 
   172         @templatefilter('websub', intype=bytes)
   172         @templatefilter(b'websub', intype=bytes)
   173         def websubfilter(text):
   173         def websubfilter(text):
   174             return templatefilters.websub(text, self.websubtable)
   174             return templatefilters.websub(text, self.websubtable)
   175 
   175 
   176         # create the templater
   176         # create the templater
   177         # TODO: export all keywords: defaults = templatekw.keywords.copy()
   177         # TODO: export all keywords: defaults = templatekw.keywords.copy()
   178         defaults = {
   178         defaults = {
   179             'url': req.apppath + '/',
   179             b'url': req.apppath + b'/',
   180             'logourl': logourl,
   180             b'logourl': logourl,
   181             'logoimg': logoimg,
   181             b'logoimg': logoimg,
   182             'staticurl': staticurl,
   182             b'staticurl': staticurl,
   183             'urlbase': req.advertisedbaseurl,
   183             b'urlbase': req.advertisedbaseurl,
   184             'repo': self.reponame,
   184             b'repo': self.reponame,
   185             'encoding': encoding.encoding,
   185             b'encoding': encoding.encoding,
   186             'sessionvars': sessionvars,
   186             b'sessionvars': sessionvars,
   187             'pathdef': makebreadcrumb(req.apppath),
   187             b'pathdef': makebreadcrumb(req.apppath),
   188             'style': style,
   188             b'style': style,
   189             'nonce': self.nonce,
   189             b'nonce': self.nonce,
   190         }
   190         }
   191         templatekeyword = registrar.templatekeyword(defaults)
   191         templatekeyword = registrar.templatekeyword(defaults)
   192 
   192 
   193         @templatekeyword('motd', requires=())
   193         @templatekeyword(b'motd', requires=())
   194         def motd(context, mapping):
   194         def motd(context, mapping):
   195             yield self.config('web', 'motd')
   195             yield self.config(b'web', b'motd')
   196 
   196 
   197         tres = formatter.templateresources(self.repo.ui, self.repo)
   197         tres = formatter.templateresources(self.repo.ui, self.repo)
   198         tmpl = templater.templater.frommapfile(
   198         tmpl = templater.templater.frommapfile(
   199             mapfile, filters=filters, defaults=defaults, resources=tres
   199             mapfile, filters=filters, defaults=defaults, resources=tres
   200         )
   200         )
   230             r = hg.repository(u, repo)
   230             r = hg.repository(u, repo)
   231         else:
   231         else:
   232             # we trust caller to give us a private copy
   232             # we trust caller to give us a private copy
   233             r = repo
   233             r = repo
   234 
   234 
   235         r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
   235         r.ui.setconfig(b'ui', b'report_untrusted', b'off', b'hgweb')
   236         r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
   236         r.baseui.setconfig(b'ui', b'report_untrusted', b'off', b'hgweb')
   237         r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
   237         r.ui.setconfig(b'ui', b'nontty', b'true', b'hgweb')
   238         r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
   238         r.baseui.setconfig(b'ui', b'nontty', b'true', b'hgweb')
   239         # resolve file patterns relative to repo root
   239         # resolve file patterns relative to repo root
   240         r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
   240         r.ui.setconfig(b'ui', b'forcecwd', r.root, b'hgweb')
   241         r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
   241         r.baseui.setconfig(b'ui', b'forcecwd', r.root, b'hgweb')
   242         # it's unlikely that we can replace signal handlers in WSGI server,
   242         # it's unlikely that we can replace signal handlers in WSGI server,
   243         # and mod_wsgi issues a big warning. a plain hgweb process (with no
   243         # and mod_wsgi issues a big warning. a plain hgweb process (with no
   244         # threading) could replace signal handlers, but we don't bother
   244         # threading) could replace signal handlers, but we don't bother
   245         # conditionally enabling it.
   245         # conditionally enabling it.
   246         r.ui.setconfig('ui', 'signal-safe-lock', 'false', 'hgweb')
   246         r.ui.setconfig(b'ui', b'signal-safe-lock', b'false', b'hgweb')
   247         r.baseui.setconfig('ui', 'signal-safe-lock', 'false', 'hgweb')
   247         r.baseui.setconfig(b'ui', b'signal-safe-lock', b'false', b'hgweb')
   248         # displaying bundling progress bar while serving feel wrong and may
   248         # displaying bundling progress bar while serving feel wrong and may
   249         # break some wsgi implementation.
   249         # break some wsgi implementation.
   250         r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
   250         r.ui.setconfig(b'progress', b'disable', b'true', b'hgweb')
   251         r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
   251         r.baseui.setconfig(b'progress', b'disable', b'true', b'hgweb')
   252         self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
   252         self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
   253         self._lastrepo = self._repos[0]
   253         self._lastrepo = self._repos[0]
   254         hook.redirect(True)
   254         hook.redirect(True)
   255         self.reponame = name
   255         self.reponame = name
   256 
   256 
   292         """Start a server from CGI environment.
   292         """Start a server from CGI environment.
   293 
   293 
   294         Modern servers should be using WSGI and should avoid this
   294         Modern servers should be using WSGI and should avoid this
   295         method, if possible.
   295         method, if possible.
   296         """
   296         """
   297         if not encoding.environ.get('GATEWAY_INTERFACE', '').startswith(
   297         if not encoding.environ.get(b'GATEWAY_INTERFACE', b'').startswith(
   298             "CGI/1."
   298             b"CGI/1."
   299         ):
   299         ):
   300             raise RuntimeError(
   300             raise RuntimeError(
   301                 "This function is only intended to be "
   301                 b"This function is only intended to be "
   302                 "called while running as a CGI script."
   302                 b"called while running as a CGI script."
   303             )
   303             )
   304         wsgicgi.launch(self)
   304         wsgicgi.launch(self)
   305 
   305 
   306     def __call__(self, env, respond):
   306     def __call__(self, env, respond):
   307         """Run the WSGI application.
   307         """Run the WSGI application.
   318 
   318 
   319         This is typically only called by Mercurial. External consumers
   319         This is typically only called by Mercurial. External consumers
   320         should be using instances of this class as the WSGI application.
   320         should be using instances of this class as the WSGI application.
   321         """
   321         """
   322         with self._obtainrepo() as repo:
   322         with self._obtainrepo() as repo:
   323             profile = repo.ui.configbool('profiling', 'enabled')
   323             profile = repo.ui.configbool(b'profiling', b'enabled')
   324             with profiling.profile(repo.ui, enabled=profile):
   324             with profiling.profile(repo.ui, enabled=profile):
   325                 for r in self._runwsgi(req, res, repo):
   325                 for r in self._runwsgi(req, res, repo):
   326                     yield r
   326                     yield r
   327 
   327 
   328     def _runwsgi(self, req, res, repo):
   328     def _runwsgi(self, req, res, repo):
   329         rctx = requestcontext(self, repo, req, res)
   329         rctx = requestcontext(self, repo, req, res)
   330 
   330 
   331         # This state is global across all threads.
   331         # This state is global across all threads.
   332         encoding.encoding = rctx.config('web', 'encoding')
   332         encoding.encoding = rctx.config(b'web', b'encoding')
   333         rctx.repo.ui.environ = req.rawenv
   333         rctx.repo.ui.environ = req.rawenv
   334 
   334 
   335         if rctx.csp:
   335         if rctx.csp:
   336             # hgwebdir may have added CSP header. Since we generate our own,
   336             # hgwebdir may have added CSP header. Since we generate our own,
   337             # replace it.
   337             # replace it.
   338             res.headers['Content-Security-Policy'] = rctx.csp
   338             res.headers[b'Content-Security-Policy'] = rctx.csp
   339 
   339 
   340         # /api/* is reserved for various API implementations. Dispatch
   340         # /api/* is reserved for various API implementations. Dispatch
   341         # accordingly. But URL paths can conflict with subrepos and virtual
   341         # accordingly. But URL paths can conflict with subrepos and virtual
   342         # repos in hgwebdir. So until we have a workaround for this, only
   342         # repos in hgwebdir. So until we have a workaround for this, only
   343         # expose the URLs if the feature is enabled.
   343         # expose the URLs if the feature is enabled.
   344         apienabled = rctx.repo.ui.configbool('experimental', 'web.apiserver')
   344         apienabled = rctx.repo.ui.configbool(b'experimental', b'web.apiserver')
   345         if apienabled and req.dispatchparts and req.dispatchparts[0] == b'api':
   345         if apienabled and req.dispatchparts and req.dispatchparts[0] == b'api':
   346             wireprotoserver.handlewsgiapirequest(
   346             wireprotoserver.handlewsgiapirequest(
   347                 rctx, req, res, self.check_perm
   347                 rctx, req, res, self.check_perm
   348             )
   348             )
   349             return res.sendresponse()
   349             return res.sendresponse()
   359         # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
   359         # If PATH_INFO is present (signaled by ``req.dispatchpath`` having
   360         # a value), we use it. Otherwise fall back to the query string.
   360         # a value), we use it. Otherwise fall back to the query string.
   361         if req.dispatchpath is not None:
   361         if req.dispatchpath is not None:
   362             query = req.dispatchpath
   362             query = req.dispatchpath
   363         else:
   363         else:
   364             query = req.querystring.partition('&')[0].partition(';')[0]
   364             query = req.querystring.partition(b'&')[0].partition(b';')[0]
   365 
   365 
   366         # translate user-visible url structure to internal structure
   366         # translate user-visible url structure to internal structure
   367 
   367 
   368         args = query.split('/', 2)
   368         args = query.split(b'/', 2)
   369         if 'cmd' not in req.qsparams and args and args[0]:
   369         if b'cmd' not in req.qsparams and args and args[0]:
   370             cmd = args.pop(0)
   370             cmd = args.pop(0)
   371             style = cmd.rfind('-')
   371             style = cmd.rfind(b'-')
   372             if style != -1:
   372             if style != -1:
   373                 req.qsparams['style'] = cmd[:style]
   373                 req.qsparams[b'style'] = cmd[:style]
   374                 cmd = cmd[style + 1 :]
   374                 cmd = cmd[style + 1 :]
   375 
   375 
   376             # avoid accepting e.g. style parameter as command
   376             # avoid accepting e.g. style parameter as command
   377             if util.safehasattr(webcommands, cmd):
   377             if util.safehasattr(webcommands, cmd):
   378                 req.qsparams['cmd'] = cmd
   378                 req.qsparams[b'cmd'] = cmd
   379 
   379 
   380             if cmd == 'static':
   380             if cmd == b'static':
   381                 req.qsparams['file'] = '/'.join(args)
   381                 req.qsparams[b'file'] = b'/'.join(args)
   382             else:
   382             else:
   383                 if args and args[0]:
   383                 if args and args[0]:
   384                     node = args.pop(0).replace('%2F', '/')
   384                     node = args.pop(0).replace(b'%2F', b'/')
   385                     req.qsparams['node'] = node
   385                     req.qsparams[b'node'] = node
   386                 if args:
   386                 if args:
   387                     if 'file' in req.qsparams:
   387                     if b'file' in req.qsparams:
   388                         del req.qsparams['file']
   388                         del req.qsparams[b'file']
   389                     for a in args:
   389                     for a in args:
   390                         req.qsparams.add('file', a)
   390                         req.qsparams.add(b'file', a)
   391 
   391 
   392             ua = req.headers.get('User-Agent', '')
   392             ua = req.headers.get(b'User-Agent', b'')
   393             if cmd == 'rev' and 'mercurial' in ua:
   393             if cmd == b'rev' and b'mercurial' in ua:
   394                 req.qsparams['style'] = 'raw'
   394                 req.qsparams[b'style'] = b'raw'
   395 
   395 
   396             if cmd == 'archive':
   396             if cmd == b'archive':
   397                 fn = req.qsparams['node']
   397                 fn = req.qsparams[b'node']
   398                 for type_, spec in webutil.archivespecs.iteritems():
   398                 for type_, spec in webutil.archivespecs.iteritems():
   399                     ext = spec[2]
   399                     ext = spec[2]
   400                     if fn.endswith(ext):
   400                     if fn.endswith(ext):
   401                         req.qsparams['node'] = fn[: -len(ext)]
   401                         req.qsparams[b'node'] = fn[: -len(ext)]
   402                         req.qsparams['type'] = type_
   402                         req.qsparams[b'type'] = type_
   403         else:
   403         else:
   404             cmd = req.qsparams.get('cmd', '')
   404             cmd = req.qsparams.get(b'cmd', b'')
   405 
   405 
   406         # process the web interface request
   406         # process the web interface request
   407 
   407 
   408         try:
   408         try:
   409             rctx.tmpl = rctx.templater(req)
   409             rctx.tmpl = rctx.templater(req)
   410             ctype = rctx.tmpl.render(
   410             ctype = rctx.tmpl.render(
   411                 'mimetype', {'encoding': encoding.encoding}
   411                 b'mimetype', {b'encoding': encoding.encoding}
   412             )
   412             )
   413 
   413 
   414             # check read permissions non-static content
   414             # check read permissions non-static content
   415             if cmd != 'static':
   415             if cmd != b'static':
   416                 self.check_perm(rctx, req, None)
   416                 self.check_perm(rctx, req, None)
   417 
   417 
   418             if cmd == '':
   418             if cmd == b'':
   419                 req.qsparams['cmd'] = rctx.tmpl.render('default', {})
   419                 req.qsparams[b'cmd'] = rctx.tmpl.render(b'default', {})
   420                 cmd = req.qsparams['cmd']
   420                 cmd = req.qsparams[b'cmd']
   421 
   421 
   422             # Don't enable caching if using a CSP nonce because then it wouldn't
   422             # Don't enable caching if using a CSP nonce because then it wouldn't
   423             # be a nonce.
   423             # be a nonce.
   424             if rctx.configbool('web', 'cache') and not rctx.nonce:
   424             if rctx.configbool(b'web', b'cache') and not rctx.nonce:
   425                 tag = 'W/"%d"' % self.mtime
   425                 tag = b'W/"%d"' % self.mtime
   426                 if req.headers.get('If-None-Match') == tag:
   426                 if req.headers.get(b'If-None-Match') == tag:
   427                     res.status = '304 Not Modified'
   427                     res.status = b'304 Not Modified'
   428                     # Content-Type may be defined globally. It isn't valid on a
   428                     # Content-Type may be defined globally. It isn't valid on a
   429                     # 304, so discard it.
   429                     # 304, so discard it.
   430                     try:
   430                     try:
   431                         del res.headers[b'Content-Type']
   431                         del res.headers[b'Content-Type']
   432                     except KeyError:
   432                     except KeyError:
   433                         pass
   433                         pass
   434                     # Response body not allowed on 304.
   434                     # Response body not allowed on 304.
   435                     res.setbodybytes('')
   435                     res.setbodybytes(b'')
   436                     return res.sendresponse()
   436                     return res.sendresponse()
   437 
   437 
   438                 res.headers['ETag'] = tag
   438                 res.headers[b'ETag'] = tag
   439 
   439 
   440             if cmd not in webcommands.__all__:
   440             if cmd not in webcommands.__all__:
   441                 msg = 'no such method: %s' % cmd
   441                 msg = b'no such method: %s' % cmd
   442                 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
   442                 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
   443             else:
   443             else:
   444                 # Set some globals appropriate for web handlers. Commands can
   444                 # Set some globals appropriate for web handlers. Commands can
   445                 # override easily enough.
   445                 # override easily enough.
   446                 res.status = '200 Script output follows'
   446                 res.status = b'200 Script output follows'
   447                 res.headers['Content-Type'] = ctype
   447                 res.headers[b'Content-Type'] = ctype
   448                 return getattr(webcommands, cmd)(rctx)
   448                 return getattr(webcommands, cmd)(rctx)
   449 
   449 
   450         except (error.LookupError, error.RepoLookupError) as err:
   450         except (error.LookupError, error.RepoLookupError) as err:
   451             msg = pycompat.bytestr(err)
   451             msg = pycompat.bytestr(err)
   452             if util.safehasattr(err, 'name') and not isinstance(
   452             if util.safehasattr(err, b'name') and not isinstance(
   453                 err, error.ManifestLookupError
   453                 err, error.ManifestLookupError
   454             ):
   454             ):
   455                 msg = 'revision not found: %s' % err.name
   455                 msg = b'revision not found: %s' % err.name
   456 
   456 
   457             res.status = '404 Not Found'
   457             res.status = b'404 Not Found'
   458             res.headers['Content-Type'] = ctype
   458             res.headers[b'Content-Type'] = ctype
   459             return rctx.sendtemplate('error', error=msg)
   459             return rctx.sendtemplate(b'error', error=msg)
   460         except (error.RepoError, error.StorageError) as e:
   460         except (error.RepoError, error.StorageError) as e:
   461             res.status = '500 Internal Server Error'
   461             res.status = b'500 Internal Server Error'
   462             res.headers['Content-Type'] = ctype
   462             res.headers[b'Content-Type'] = ctype
   463             return rctx.sendtemplate('error', error=pycompat.bytestr(e))
   463             return rctx.sendtemplate(b'error', error=pycompat.bytestr(e))
   464         except error.Abort as e:
   464         except error.Abort as e:
   465             res.status = '403 Forbidden'
   465             res.status = b'403 Forbidden'
   466             res.headers['Content-Type'] = ctype
   466             res.headers[b'Content-Type'] = ctype
   467             return rctx.sendtemplate('error', error=pycompat.bytestr(e))
   467             return rctx.sendtemplate(b'error', error=pycompat.bytestr(e))
   468         except ErrorResponse as e:
   468         except ErrorResponse as e:
   469             for k, v in e.headers:
   469             for k, v in e.headers:
   470                 res.headers[k] = v
   470                 res.headers[k] = v
   471             res.status = statusmessage(e.code, pycompat.bytestr(e))
   471             res.status = statusmessage(e.code, pycompat.bytestr(e))
   472             res.headers['Content-Type'] = ctype
   472             res.headers[b'Content-Type'] = ctype
   473             return rctx.sendtemplate('error', error=pycompat.bytestr(e))
   473             return rctx.sendtemplate(b'error', error=pycompat.bytestr(e))
   474 
   474 
   475     def check_perm(self, rctx, req, op):
   475     def check_perm(self, rctx, req, op):
   476         for permhook in permhooks:
   476         for permhook in permhooks:
   477             permhook(rctx, req, op)
   477             permhook(rctx, req, op)
   478 
   478 
   487     See the repoview module for details.
   487     See the repoview module for details.
   488 
   488 
   489     The option has been around undocumented since Mercurial 2.5, but no
   489     The option has been around undocumented since Mercurial 2.5, but no
   490     user ever asked about it. So we better keep it undocumented for now."""
   490     user ever asked about it. So we better keep it undocumented for now."""
   491     # experimental config: web.view
   491     # experimental config: web.view
   492     viewconfig = repo.ui.config('web', 'view', untrusted=True)
   492     viewconfig = repo.ui.config(b'web', b'view', untrusted=True)
   493     if viewconfig == 'all':
   493     if viewconfig == b'all':
   494         return repo.unfiltered()
   494         return repo.unfiltered()
   495     elif viewconfig in repoview.filtertable:
   495     elif viewconfig in repoview.filtertable:
   496         return repo.filtered(viewconfig)
   496         return repo.filtered(viewconfig)
   497     else:
   497     else:
   498         return repo.filtered('served')
   498         return repo.filtered(b'served')