mercurial/hgweb/hgwebdir_mod.py
changeset 43076 2372284d9457
parent 42343 d8e55c0c642c
child 43077 687b865b95ad
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
    48     webutil,
    48     webutil,
    49     wsgicgi,
    49     wsgicgi,
    50 )
    50 )
    51 from ..utils import dateutil
    51 from ..utils import dateutil
    52 
    52 
       
    53 
    53 def cleannames(items):
    54 def cleannames(items):
    54     return [(util.pconvert(name).strip('/'), path) for name, path in items]
    55     return [(util.pconvert(name).strip('/'), path) for name, path in items]
       
    56 
    55 
    57 
    56 def findrepos(paths):
    58 def findrepos(paths):
    57     repos = []
    59     repos = []
    58     for prefix, root in cleannames(paths):
    60     for prefix, root in cleannames(paths):
    59         roothead, roottail = os.path.split(root)
    61         roothead, roottail = os.path.split(root)
    69         roothead = os.path.normpath(os.path.abspath(roothead))
    71         roothead = os.path.normpath(os.path.abspath(roothead))
    70         paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
    72         paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
    71         repos.extend(urlrepos(prefix, roothead, paths))
    73         repos.extend(urlrepos(prefix, roothead, paths))
    72     return repos
    74     return repos
    73 
    75 
       
    76 
    74 def urlrepos(prefix, roothead, paths):
    77 def urlrepos(prefix, roothead, paths):
    75     """yield url paths and filesystem paths from a list of repo paths
    78     """yield url paths and filesystem paths from a list of repo paths
    76 
    79 
    77     >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
    80     >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
    78     >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
    81     >>> conv(urlrepos(b'hg', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
    80     >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
    83     >>> conv(urlrepos(b'', b'/opt', [b'/opt/r', b'/opt/r/r', b'/opt']))
    81     [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
    84     [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
    82     """
    85     """
    83     for path in paths:
    86     for path in paths:
    84         path = os.path.normpath(path)
    87         path = os.path.normpath(path)
    85         yield (prefix + '/' +
    88         yield (
    86                util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
    89             prefix + '/' + util.pconvert(path[len(roothead) :]).lstrip('/')
       
    90         ).strip('/'), path
       
    91 
    87 
    92 
    88 def readallowed(ui, req):
    93 def readallowed(ui, req):
    89     """Check allow_read and deny_read config options of a repo's ui object
    94     """Check allow_read and deny_read config options of a repo's ui object
    90     to determine user permissions.  By default, with neither option set (or
    95     to determine user permissions.  By default, with neither option set (or
    91     both empty), allow all users to read the repo.  There are two ways a
    96     both empty), allow all users to read the repo.  There are two ways a
   105     if not allow_read or ismember(ui, user, allow_read):
   110     if not allow_read or ismember(ui, user, allow_read):
   106         return True
   111         return True
   107 
   112 
   108     return False
   113     return False
   109 
   114 
       
   115 
   110 def rawindexentries(ui, repos, req, subdir=''):
   116 def rawindexentries(ui, repos, req, subdir=''):
   111     descend = ui.configbool('web', 'descend')
   117     descend = ui.configbool('web', 'descend')
   112     collapse = ui.configbool('web', 'collapse')
   118     collapse = ui.configbool('web', 'collapse')
   113     seenrepos = set()
   119     seenrepos = set()
   114     seendirs = set()
   120     seendirs = set()
   115     for name, path in repos:
   121     for name, path in repos:
   116 
   122 
   117         if not name.startswith(subdir):
   123         if not name.startswith(subdir):
   118             continue
   124             continue
   119         name = name[len(subdir):]
   125         name = name[len(subdir) :]
   120         directory = False
   126         directory = False
   121 
   127 
   122         if '/' in name:
   128         if '/' in name:
   123             if not descend:
   129             if not descend:
   124                 continue
   130                 continue
   138 
   144 
   139                 # redefine the path to refer to the directory
   145                 # redefine the path to refer to the directory
   140                 discarded = '/'.join(nameparts[1:])
   146                 discarded = '/'.join(nameparts[1:])
   141 
   147 
   142                 # remove name parts plus accompanying slash
   148                 # remove name parts plus accompanying slash
   143                 path = path[:-len(discarded) - 1]
   149                 path = path[: -len(discarded) - 1]
   144 
   150 
   145                 try:
   151                 try:
   146                     hg.repository(ui, path)
   152                     hg.repository(ui, path)
   147                     directory = False
   153                     directory = False
   148                 except (IOError, error.RepoError):
   154                 except (IOError, error.RepoError):
   163             except OSError:
   169             except OSError:
   164                 continue
   170                 continue
   165 
   171 
   166             # add '/' to the name to make it obvious that
   172             # add '/' to the name to make it obvious that
   167             # the entry is a directory, not a regular repository
   173             # the entry is a directory, not a regular repository
   168             row = {'contact': "",
   174             row = {
   169                    'contact_sort': "",
   175                 'contact': "",
   170                    'name': name + '/',
   176                 'contact_sort': "",
   171                    'name_sort': name,
   177                 'name': name + '/',
   172                    'url': url,
   178                 'name_sort': name,
   173                    'description': "",
   179                 'url': url,
   174                    'description_sort': "",
   180                 'description': "",
   175                    'lastchange': d,
   181                 'description_sort': "",
   176                    'lastchange_sort': d[1] - d[0],
   182                 'lastchange': d,
   177                    'archives': templateutil.mappinglist([]),
   183                 'lastchange_sort': d[1] - d[0],
   178                    'isdirectory': True,
   184                 'archives': templateutil.mappinglist([]),
   179                    'labels': templateutil.hybridlist([], name='label'),
   185                 'isdirectory': True,
   180                    }
   186                 'labels': templateutil.hybridlist([], name='label'),
       
   187             }
   181 
   188 
   182             seendirs.add(name)
   189             seendirs.add(name)
   183             yield row
   190             yield row
   184             continue
   191             continue
   185 
   192 
   216         contact = get_contact(get)
   223         contact = get_contact(get)
   217         description = get("web", "description")
   224         description = get("web", "description")
   218         seenrepos.add(name)
   225         seenrepos.add(name)
   219         name = get("web", "name", name)
   226         name = get("web", "name", name)
   220         labels = u.configlist('web', 'labels', untrusted=True)
   227         labels = u.configlist('web', 'labels', untrusted=True)
   221         row = {'contact': contact or "unknown",
   228         row = {
   222                'contact_sort': contact.upper() or "unknown",
   229             'contact': contact or "unknown",
   223                'name': name,
   230             'contact_sort': contact.upper() or "unknown",
   224                'name_sort': name,
   231             'name': name,
   225                'url': url,
   232             'name_sort': name,
   226                'description': description or "unknown",
   233             'url': url,
   227                'description_sort': description.upper() or "unknown",
   234             'description': description or "unknown",
   228                'lastchange': d,
   235             'description_sort': description.upper() or "unknown",
   229                'lastchange_sort': d[1] - d[0],
   236             'lastchange': d,
   230                'archives': webutil.archivelist(u, "tip", url),
   237             'lastchange_sort': d[1] - d[0],
   231                'isdirectory': None,
   238             'archives': webutil.archivelist(u, "tip", url),
   232                'labels': templateutil.hybridlist(labels, name='label'),
   239             'isdirectory': None,
   233                }
   240             'labels': templateutil.hybridlist(labels, name='label'),
       
   241         }
   234 
   242 
   235         yield row
   243         yield row
   236 
   244 
   237 def _indexentriesgen(context, ui, repos, req, stripecount, sortcolumn,
   245 
   238                      descending, subdir):
   246 def _indexentriesgen(
       
   247     context, ui, repos, req, stripecount, sortcolumn, descending, subdir
       
   248 ):
   239     rows = rawindexentries(ui, repos, req, subdir=subdir)
   249     rows = rawindexentries(ui, repos, req, subdir=subdir)
   240 
   250 
   241     sortdefault = None, False
   251     sortdefault = None, False
   242 
   252 
   243     if sortcolumn and sortdefault != (sortcolumn, descending):
   253     if sortcolumn and sortdefault != (sortcolumn, descending):
   244         sortkey = '%s_sort' % sortcolumn
   254         sortkey = '%s_sort' % sortcolumn
   245         rows = sorted(rows, key=lambda x: x[sortkey],
   255         rows = sorted(rows, key=lambda x: x[sortkey], reverse=descending)
   246                       reverse=descending)
       
   247 
   256 
   248     for row, parity in zip(rows, paritygen(stripecount)):
   257     for row, parity in zip(rows, paritygen(stripecount)):
   249         row['parity'] = parity
   258         row['parity'] = parity
   250         yield row
   259         yield row
   251 
   260 
   252 def indexentries(ui, repos, req, stripecount, sortcolumn='',
   261 
   253                  descending=False, subdir=''):
   262 def indexentries(
       
   263     ui, repos, req, stripecount, sortcolumn='', descending=False, subdir=''
       
   264 ):
   254     args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
   265     args = (ui, repos, req, stripecount, sortcolumn, descending, subdir)
   255     return templateutil.mappinggenerator(_indexentriesgen, args=args)
   266     return templateutil.mappinggenerator(_indexentriesgen, args=args)
   256 
   267 
       
   268 
   257 class hgwebdir(object):
   269 class hgwebdir(object):
   258     """HTTP server for multiple repositories.
   270     """HTTP server for multiple repositories.
   259 
   271 
   260     Given a configuration, different repositories will be served depending
   272     Given a configuration, different repositories will be served depending
   261     on the request path.
   273     on the request path.
   262 
   274 
   263     Instances are typically used as WSGI applications.
   275     Instances are typically used as WSGI applications.
   264     """
   276     """
       
   277 
   265     def __init__(self, conf, baseui=None):
   278     def __init__(self, conf, baseui=None):
   266         self.conf = conf
   279         self.conf = conf
   267         self.baseui = baseui
   280         self.baseui = baseui
   268         self.ui = None
   281         self.ui = None
   269         self.lastrefresh = 0
   282         self.lastrefresh = 0
   280         else:
   293         else:
   281             item = configitems.coreitems['web']['refreshinterval']
   294             item = configitems.coreitems['web']['refreshinterval']
   282             refreshinterval = item.default
   295             refreshinterval = item.default
   283 
   296 
   284         # refreshinterval <= 0 means to always refresh.
   297         # refreshinterval <= 0 means to always refresh.
   285         if (refreshinterval > 0 and
   298         if (
   286             self.lastrefresh + refreshinterval > time.time()):
   299             refreshinterval > 0
       
   300             and self.lastrefresh + refreshinterval > time.time()
       
   301         ):
   287             return
   302             return
   288 
   303 
   289         if self.baseui:
   304         if self.baseui:
   290             u = self.baseui.copy()
   305             u = self.baseui.copy()
   291         else:
   306         else:
   316             prefix = util.pconvert(prefix)
   331             prefix = util.pconvert(prefix)
   317             for path in scmutil.walkrepos(root, followsym=True):
   332             for path in scmutil.walkrepos(root, followsym=True):
   318                 repo = os.path.normpath(path)
   333                 repo = os.path.normpath(path)
   319                 name = util.pconvert(repo)
   334                 name = util.pconvert(repo)
   320                 if name.startswith(prefix):
   335                 if name.startswith(prefix):
   321                     name = name[len(prefix):]
   336                     name = name[len(prefix) :]
   322                 repos.append((name.lstrip('/'), repo))
   337                 repos.append((name.lstrip('/'), repo))
   323 
   338 
   324         self.repos = repos
   339         self.repos = repos
   325         self.ui = u
   340         self.ui = u
   326         encoding.encoding = self.ui.config('web', 'encoding')
   341         encoding.encoding = self.ui.config('web', 'encoding')
   336             prefix = prefix[:-1]
   351             prefix = prefix[:-1]
   337         self.prefix = prefix
   352         self.prefix = prefix
   338         self.lastrefresh = time.time()
   353         self.lastrefresh = time.time()
   339 
   354 
   340     def run(self):
   355     def run(self):
   341         if not encoding.environ.get('GATEWAY_INTERFACE',
   356         if not encoding.environ.get('GATEWAY_INTERFACE', '').startswith(
   342                                     '').startswith("CGI/1."):
   357             "CGI/1."
   343             raise RuntimeError("This function is only intended to be "
   358         ):
   344                                "called while running as a CGI script.")
   359             raise RuntimeError(
       
   360                 "This function is only intended to be "
       
   361                 "called while running as a CGI script."
       
   362             )
   345         wsgicgi.launch(self)
   363         wsgicgi.launch(self)
   346 
   364 
   347     def __call__(self, env, respond):
   365     def __call__(self, env, respond):
   348         baseurl = self.ui.config('web', 'baseurl')
   366         baseurl = self.ui.config('web', 'baseurl')
   349         req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
   367         req = requestmod.parserequestfromenv(env, altbaseurl=baseurl)
   407                 return self.makeindex(req, res, tmpl)
   425                 return self.makeindex(req, res, tmpl)
   408 
   426 
   409             # nested indexes and hgwebs
   427             # nested indexes and hgwebs
   410 
   428 
   411             if virtual.endswith('/index') and virtual not in repos:
   429             if virtual.endswith('/index') and virtual not in repos:
   412                 subdir = virtual[:-len('index')]
   430                 subdir = virtual[: -len('index')]
   413                 if any(r.startswith(subdir) for r in repos):
   431                 if any(r.startswith(subdir) for r in repos):
   414                     return self.makeindex(req, res, tmpl, subdir)
   432                     return self.makeindex(req, res, tmpl, subdir)
   415 
   433 
   416             def _virtualdirs():
   434             def _virtualdirs():
   417                 # Check the full virtual path, and each parent
   435                 # Check the full virtual path, and each parent
   424                 if real:
   442                 if real:
   425                     # Re-parse the WSGI environment to take into account our
   443                     # Re-parse the WSGI environment to take into account our
   426                     # repository path component.
   444                     # repository path component.
   427                     uenv = req.rawenv
   445                     uenv = req.rawenv
   428                     if pycompat.ispy3:
   446                     if pycompat.ispy3:
   429                         uenv = {k.decode('latin1'): v for k, v in
   447                         uenv = {
   430                                 uenv.iteritems()}
   448                             k.decode('latin1'): v for k, v in uenv.iteritems()
       
   449                         }
   431                     req = requestmod.parserequestfromenv(
   450                     req = requestmod.parserequestfromenv(
   432                         uenv, reponame=virtualrepo,
   451                         uenv,
       
   452                         reponame=virtualrepo,
   433                         altbaseurl=self.ui.config('web', 'baseurl'),
   453                         altbaseurl=self.ui.config('web', 'baseurl'),
   434                         # Reuse wrapped body file object otherwise state
   454                         # Reuse wrapped body file object otherwise state
   435                         # tracking can get confused.
   455                         # tracking can get confused.
   436                         bodyfh=req.bodyfh)
   456                         bodyfh=req.bodyfh,
       
   457                     )
   437                     try:
   458                     try:
   438                         # ensure caller gets private copy of ui
   459                         # ensure caller gets private copy of ui
   439                         repo = hg.repository(self.ui.copy(), real)
   460                         repo = hg.repository(self.ui.copy(), real)
   440                         return hgweb_mod.hgweb(repo).run_wsgi(req, res)
   461                         return hgweb_mod.hgweb(repo).run_wsgi(req, res)
   441                     except IOError as inst:
   462                     except IOError as inst:
   471             if descending:
   492             if descending:
   472                 sortcolumn = sortcolumn[1:]
   493                 sortcolumn = sortcolumn[1:]
   473             if sortcolumn not in sortable:
   494             if sortcolumn not in sortable:
   474                 sortcolumn = ""
   495                 sortcolumn = ""
   475 
   496 
   476         sort = [("sort_%s" % column,
   497         sort = [
   477                  "%s%s" % ((not descending and column == sortcolumn)
   498             (
   478                             and "-" or "", column))
   499                 "sort_%s" % column,
   479                 for column in sortable]
   500                 "%s%s"
       
   501                 % (
       
   502                     (not descending and column == sortcolumn) and "-" or "",
       
   503                     column,
       
   504                 ),
       
   505             )
       
   506             for column in sortable
       
   507         ]
   480 
   508 
   481         self.refresh()
   509         self.refresh()
   482 
   510 
   483         entries = indexentries(self.ui, self.repos, req,
   511         entries = indexentries(
   484                                self.stripecount, sortcolumn=sortcolumn,
   512             self.ui,
   485                                descending=descending, subdir=subdir)
   513             self.repos,
       
   514             req,
       
   515             self.stripecount,
       
   516             sortcolumn=sortcolumn,
       
   517             descending=descending,
       
   518             subdir=subdir,
       
   519         )
   486 
   520 
   487         mapping = {
   521         mapping = {
   488             'entries': entries,
   522             'entries': entries,
   489             'subdir': subdir,
   523             'subdir': subdir,
   490             'pathdef': hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
   524             'pathdef': hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
   494         mapping.update(sort)
   528         mapping.update(sort)
   495         res.setbodygen(tmpl.generate('index', mapping))
   529         res.setbodygen(tmpl.generate('index', mapping))
   496         return res.sendresponse()
   530         return res.sendresponse()
   497 
   531 
   498     def templater(self, req, nonce):
   532     def templater(self, req, nonce):
   499 
       
   500         def config(section, name, default=uimod._unset, untrusted=True):
   533         def config(section, name, default=uimod._unset, untrusted=True):
   501             return self.ui.config(section, name, default, untrusted)
   534             return self.ui.config(section, name, default, untrusted)
   502 
   535 
   503         vars = {}
   536         vars = {}
   504         styles, (style, mapfile) = hgweb_mod.getstyle(req, config,
   537         styles, (style, mapfile) = hgweb_mod.getstyle(
   505                                                       self.templatepath)
   538             req, config, self.templatepath
       
   539         )
   506         if style == styles[0]:
   540         if style == styles[0]:
   507             vars['style'] = style
   541             vars['style'] = style
   508 
   542 
   509         sessionvars = webutil.sessionvars(vars, '?')
   543         sessionvars = webutil.sessionvars(vars, '?')
   510         logourl = config('web', 'logourl')
   544         logourl = config('web', 'logourl')
   511         logoimg = config('web', 'logoimg')
   545         logoimg = config('web', 'logoimg')
   512         staticurl = (config('web', 'staticurl')
   546         staticurl = (
   513                      or req.apppath.rstrip('/') + '/static/')
   547             config('web', 'staticurl') or req.apppath.rstrip('/') + '/static/'
       
   548         )
   514         if not staticurl.endswith('/'):
   549         if not staticurl.endswith('/'):
   515             staticurl += '/'
   550             staticurl += '/'
   516 
   551 
   517         defaults = {
   552         defaults = {
   518             "encoding": encoding.encoding,
   553             "encoding": encoding.encoding,
   523             "sessionvars": sessionvars,
   558             "sessionvars": sessionvars,
   524             "style": style,
   559             "style": style,
   525             "nonce": nonce,
   560             "nonce": nonce,
   526         }
   561         }
   527         templatekeyword = registrar.templatekeyword(defaults)
   562         templatekeyword = registrar.templatekeyword(defaults)
       
   563 
   528         @templatekeyword('motd', requires=())
   564         @templatekeyword('motd', requires=())
   529         def motd(context, mapping):
   565         def motd(context, mapping):
   530             if self.motd is not None:
   566             if self.motd is not None:
   531                 yield self.motd
   567                 yield self.motd
   532             else:
   568             else: