##// END OF EJS Templates
hgweb: abstract call to hgwebdir wsgi function...
Gregory Szorc -
r29786:fc244249 default
parent child Browse files
Show More
@@ -1,515 +1,518
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import os
11 import os
12 import re
12 import re
13 import time
13 import time
14
14
15 from ..i18n import _
15 from ..i18n import _
16
16
17 from .common import (
17 from .common import (
18 ErrorResponse,
18 ErrorResponse,
19 HTTP_NOT_FOUND,
19 HTTP_NOT_FOUND,
20 HTTP_OK,
20 HTTP_OK,
21 HTTP_SERVER_ERROR,
21 HTTP_SERVER_ERROR,
22 get_contact,
22 get_contact,
23 get_mtime,
23 get_mtime,
24 ismember,
24 ismember,
25 paritygen,
25 paritygen,
26 staticfile,
26 staticfile,
27 )
27 )
28 from .request import wsgirequest
28 from .request import wsgirequest
29
29
30 from .. import (
30 from .. import (
31 encoding,
31 encoding,
32 error,
32 error,
33 hg,
33 hg,
34 scmutil,
34 scmutil,
35 templater,
35 templater,
36 ui as uimod,
36 ui as uimod,
37 util,
37 util,
38 )
38 )
39
39
40 from . import (
40 from . import (
41 hgweb_mod,
41 hgweb_mod,
42 webutil,
42 webutil,
43 wsgicgi,
43 wsgicgi,
44 )
44 )
45
45
46 def cleannames(items):
46 def cleannames(items):
47 return [(util.pconvert(name).strip('/'), path) for name, path in items]
47 return [(util.pconvert(name).strip('/'), path) for name, path in items]
48
48
49 def findrepos(paths):
49 def findrepos(paths):
50 repos = []
50 repos = []
51 for prefix, root in cleannames(paths):
51 for prefix, root in cleannames(paths):
52 roothead, roottail = os.path.split(root)
52 roothead, roottail = os.path.split(root)
53 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
53 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
54 # /bar/ be served as as foo/N .
54 # /bar/ be served as as foo/N .
55 # '*' will not search inside dirs with .hg (except .hg/patches),
55 # '*' will not search inside dirs with .hg (except .hg/patches),
56 # '**' will search inside dirs with .hg (and thus also find subrepos).
56 # '**' will search inside dirs with .hg (and thus also find subrepos).
57 try:
57 try:
58 recurse = {'*': False, '**': True}[roottail]
58 recurse = {'*': False, '**': True}[roottail]
59 except KeyError:
59 except KeyError:
60 repos.append((prefix, root))
60 repos.append((prefix, root))
61 continue
61 continue
62 roothead = os.path.normpath(os.path.abspath(roothead))
62 roothead = os.path.normpath(os.path.abspath(roothead))
63 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
63 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
64 repos.extend(urlrepos(prefix, roothead, paths))
64 repos.extend(urlrepos(prefix, roothead, paths))
65 return repos
65 return repos
66
66
67 def urlrepos(prefix, roothead, paths):
67 def urlrepos(prefix, roothead, paths):
68 """yield url paths and filesystem paths from a list of repo paths
68 """yield url paths and filesystem paths from a list of repo paths
69
69
70 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
70 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
71 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
71 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
72 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
72 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
73 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
73 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
74 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
74 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
75 """
75 """
76 for path in paths:
76 for path in paths:
77 path = os.path.normpath(path)
77 path = os.path.normpath(path)
78 yield (prefix + '/' +
78 yield (prefix + '/' +
79 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
79 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
80
80
81 def geturlcgivars(baseurl, port):
81 def geturlcgivars(baseurl, port):
82 """
82 """
83 Extract CGI variables from baseurl
83 Extract CGI variables from baseurl
84
84
85 >>> geturlcgivars("http://host.org/base", "80")
85 >>> geturlcgivars("http://host.org/base", "80")
86 ('host.org', '80', '/base')
86 ('host.org', '80', '/base')
87 >>> geturlcgivars("http://host.org:8000/base", "80")
87 >>> geturlcgivars("http://host.org:8000/base", "80")
88 ('host.org', '8000', '/base')
88 ('host.org', '8000', '/base')
89 >>> geturlcgivars('/base', 8000)
89 >>> geturlcgivars('/base', 8000)
90 ('', '8000', '/base')
90 ('', '8000', '/base')
91 >>> geturlcgivars("base", '8000')
91 >>> geturlcgivars("base", '8000')
92 ('', '8000', '/base')
92 ('', '8000', '/base')
93 >>> geturlcgivars("http://host", '8000')
93 >>> geturlcgivars("http://host", '8000')
94 ('host', '8000', '/')
94 ('host', '8000', '/')
95 >>> geturlcgivars("http://host/", '8000')
95 >>> geturlcgivars("http://host/", '8000')
96 ('host', '8000', '/')
96 ('host', '8000', '/')
97 """
97 """
98 u = util.url(baseurl)
98 u = util.url(baseurl)
99 name = u.host or ''
99 name = u.host or ''
100 if u.port:
100 if u.port:
101 port = u.port
101 port = u.port
102 path = u.path or ""
102 path = u.path or ""
103 if not path.startswith('/'):
103 if not path.startswith('/'):
104 path = '/' + path
104 path = '/' + path
105
105
106 return name, str(port), path
106 return name, str(port), path
107
107
108 class hgwebdir(object):
108 class hgwebdir(object):
109 """HTTP server for multiple repositories.
109 """HTTP server for multiple repositories.
110
110
111 Given a configuration, different repositories will be served depending
111 Given a configuration, different repositories will be served depending
112 on the request path.
112 on the request path.
113
113
114 Instances are typically used as WSGI applications.
114 Instances are typically used as WSGI applications.
115 """
115 """
116 def __init__(self, conf, baseui=None):
116 def __init__(self, conf, baseui=None):
117 self.conf = conf
117 self.conf = conf
118 self.baseui = baseui
118 self.baseui = baseui
119 self.ui = None
119 self.ui = None
120 self.lastrefresh = 0
120 self.lastrefresh = 0
121 self.motd = None
121 self.motd = None
122 self.refresh()
122 self.refresh()
123
123
124 def refresh(self):
124 def refresh(self):
125 refreshinterval = 20
125 refreshinterval = 20
126 if self.ui:
126 if self.ui:
127 refreshinterval = self.ui.configint('web', 'refreshinterval',
127 refreshinterval = self.ui.configint('web', 'refreshinterval',
128 refreshinterval)
128 refreshinterval)
129
129
130 # refreshinterval <= 0 means to always refresh.
130 # refreshinterval <= 0 means to always refresh.
131 if (refreshinterval > 0 and
131 if (refreshinterval > 0 and
132 self.lastrefresh + refreshinterval > time.time()):
132 self.lastrefresh + refreshinterval > time.time()):
133 return
133 return
134
134
135 if self.baseui:
135 if self.baseui:
136 u = self.baseui.copy()
136 u = self.baseui.copy()
137 else:
137 else:
138 u = uimod.ui()
138 u = uimod.ui()
139 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
139 u.setconfig('ui', 'report_untrusted', 'off', 'hgwebdir')
140 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
140 u.setconfig('ui', 'nontty', 'true', 'hgwebdir')
141 # displaying bundling progress bar while serving feels wrong and may
141 # displaying bundling progress bar while serving feels wrong and may
142 # break some wsgi implementations.
142 # break some wsgi implementations.
143 u.setconfig('progress', 'disable', 'true', 'hgweb')
143 u.setconfig('progress', 'disable', 'true', 'hgweb')
144
144
145 if not isinstance(self.conf, (dict, list, tuple)):
145 if not isinstance(self.conf, (dict, list, tuple)):
146 map = {'paths': 'hgweb-paths'}
146 map = {'paths': 'hgweb-paths'}
147 if not os.path.exists(self.conf):
147 if not os.path.exists(self.conf):
148 raise error.Abort(_('config file %s not found!') % self.conf)
148 raise error.Abort(_('config file %s not found!') % self.conf)
149 u.readconfig(self.conf, remap=map, trust=True)
149 u.readconfig(self.conf, remap=map, trust=True)
150 paths = []
150 paths = []
151 for name, ignored in u.configitems('hgweb-paths'):
151 for name, ignored in u.configitems('hgweb-paths'):
152 for path in u.configlist('hgweb-paths', name):
152 for path in u.configlist('hgweb-paths', name):
153 paths.append((name, path))
153 paths.append((name, path))
154 elif isinstance(self.conf, (list, tuple)):
154 elif isinstance(self.conf, (list, tuple)):
155 paths = self.conf
155 paths = self.conf
156 elif isinstance(self.conf, dict):
156 elif isinstance(self.conf, dict):
157 paths = self.conf.items()
157 paths = self.conf.items()
158
158
159 repos = findrepos(paths)
159 repos = findrepos(paths)
160 for prefix, root in u.configitems('collections'):
160 for prefix, root in u.configitems('collections'):
161 prefix = util.pconvert(prefix)
161 prefix = util.pconvert(prefix)
162 for path in scmutil.walkrepos(root, followsym=True):
162 for path in scmutil.walkrepos(root, followsym=True):
163 repo = os.path.normpath(path)
163 repo = os.path.normpath(path)
164 name = util.pconvert(repo)
164 name = util.pconvert(repo)
165 if name.startswith(prefix):
165 if name.startswith(prefix):
166 name = name[len(prefix):]
166 name = name[len(prefix):]
167 repos.append((name.lstrip('/'), repo))
167 repos.append((name.lstrip('/'), repo))
168
168
169 self.repos = repos
169 self.repos = repos
170 self.ui = u
170 self.ui = u
171 encoding.encoding = self.ui.config('web', 'encoding',
171 encoding.encoding = self.ui.config('web', 'encoding',
172 encoding.encoding)
172 encoding.encoding)
173 self.style = self.ui.config('web', 'style', 'paper')
173 self.style = self.ui.config('web', 'style', 'paper')
174 self.templatepath = self.ui.config('web', 'templates', None)
174 self.templatepath = self.ui.config('web', 'templates', None)
175 self.stripecount = self.ui.config('web', 'stripes', 1)
175 self.stripecount = self.ui.config('web', 'stripes', 1)
176 if self.stripecount:
176 if self.stripecount:
177 self.stripecount = int(self.stripecount)
177 self.stripecount = int(self.stripecount)
178 self._baseurl = self.ui.config('web', 'baseurl')
178 self._baseurl = self.ui.config('web', 'baseurl')
179 prefix = self.ui.config('web', 'prefix', '')
179 prefix = self.ui.config('web', 'prefix', '')
180 if prefix.startswith('/'):
180 if prefix.startswith('/'):
181 prefix = prefix[1:]
181 prefix = prefix[1:]
182 if prefix.endswith('/'):
182 if prefix.endswith('/'):
183 prefix = prefix[:-1]
183 prefix = prefix[:-1]
184 self.prefix = prefix
184 self.prefix = prefix
185 self.lastrefresh = time.time()
185 self.lastrefresh = time.time()
186
186
187 def run(self):
187 def run(self):
188 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
188 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
189 raise RuntimeError("This function is only intended to be "
189 raise RuntimeError("This function is only intended to be "
190 "called while running as a CGI script.")
190 "called while running as a CGI script.")
191 wsgicgi.launch(self)
191 wsgicgi.launch(self)
192
192
193 def __call__(self, env, respond):
193 def __call__(self, env, respond):
194 req = wsgirequest(env, respond)
194 req = wsgirequest(env, respond)
195 return self.run_wsgi(req)
195 return self.run_wsgi(req)
196
196
197 def read_allowed(self, ui, req):
197 def read_allowed(self, ui, req):
198 """Check allow_read and deny_read config options of a repo's ui object
198 """Check allow_read and deny_read config options of a repo's ui object
199 to determine user permissions. By default, with neither option set (or
199 to determine user permissions. By default, with neither option set (or
200 both empty), allow all users to read the repo. There are two ways a
200 both empty), allow all users to read the repo. There are two ways a
201 user can be denied read access: (1) deny_read is not empty, and the
201 user can be denied read access: (1) deny_read is not empty, and the
202 user is unauthenticated or deny_read contains user (or *), and (2)
202 user is unauthenticated or deny_read contains user (or *), and (2)
203 allow_read is not empty and the user is not in allow_read. Return True
203 allow_read is not empty and the user is not in allow_read. Return True
204 if user is allowed to read the repo, else return False."""
204 if user is allowed to read the repo, else return False."""
205
205
206 user = req.env.get('REMOTE_USER')
206 user = req.env.get('REMOTE_USER')
207
207
208 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
208 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
209 if deny_read and (not user or ismember(ui, user, deny_read)):
209 if deny_read and (not user or ismember(ui, user, deny_read)):
210 return False
210 return False
211
211
212 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
212 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
213 # by default, allow reading if no allow_read option has been set
213 # by default, allow reading if no allow_read option has been set
214 if (not allow_read) or ismember(ui, user, allow_read):
214 if (not allow_read) or ismember(ui, user, allow_read):
215 return True
215 return True
216
216
217 return False
217 return False
218
218
219 def run_wsgi(self, req):
219 def run_wsgi(self, req):
220 return self._runwsgi(req)
221
222 def _runwsgi(self, req):
220 try:
223 try:
221 self.refresh()
224 self.refresh()
222
225
223 virtual = req.env.get("PATH_INFO", "").strip('/')
226 virtual = req.env.get("PATH_INFO", "").strip('/')
224 tmpl = self.templater(req)
227 tmpl = self.templater(req)
225 ctype = tmpl('mimetype', encoding=encoding.encoding)
228 ctype = tmpl('mimetype', encoding=encoding.encoding)
226 ctype = templater.stringify(ctype)
229 ctype = templater.stringify(ctype)
227
230
228 # a static file
231 # a static file
229 if virtual.startswith('static/') or 'static' in req.form:
232 if virtual.startswith('static/') or 'static' in req.form:
230 if virtual.startswith('static/'):
233 if virtual.startswith('static/'):
231 fname = virtual[7:]
234 fname = virtual[7:]
232 else:
235 else:
233 fname = req.form['static'][0]
236 fname = req.form['static'][0]
234 static = self.ui.config("web", "static", None,
237 static = self.ui.config("web", "static", None,
235 untrusted=False)
238 untrusted=False)
236 if not static:
239 if not static:
237 tp = self.templatepath or templater.templatepaths()
240 tp = self.templatepath or templater.templatepaths()
238 if isinstance(tp, str):
241 if isinstance(tp, str):
239 tp = [tp]
242 tp = [tp]
240 static = [os.path.join(p, 'static') for p in tp]
243 static = [os.path.join(p, 'static') for p in tp]
241 staticfile(static, fname, req)
244 staticfile(static, fname, req)
242 return []
245 return []
243
246
244 # top-level index
247 # top-level index
245 elif not virtual:
248 elif not virtual:
246 req.respond(HTTP_OK, ctype)
249 req.respond(HTTP_OK, ctype)
247 return self.makeindex(req, tmpl)
250 return self.makeindex(req, tmpl)
248
251
249 # nested indexes and hgwebs
252 # nested indexes and hgwebs
250
253
251 repos = dict(self.repos)
254 repos = dict(self.repos)
252 virtualrepo = virtual
255 virtualrepo = virtual
253 while virtualrepo:
256 while virtualrepo:
254 real = repos.get(virtualrepo)
257 real = repos.get(virtualrepo)
255 if real:
258 if real:
256 req.env['REPO_NAME'] = virtualrepo
259 req.env['REPO_NAME'] = virtualrepo
257 try:
260 try:
258 # ensure caller gets private copy of ui
261 # ensure caller gets private copy of ui
259 repo = hg.repository(self.ui.copy(), real)
262 repo = hg.repository(self.ui.copy(), real)
260 return hgweb_mod.hgweb(repo).run_wsgi(req)
263 return hgweb_mod.hgweb(repo).run_wsgi(req)
261 except IOError as inst:
264 except IOError as inst:
262 msg = inst.strerror
265 msg = inst.strerror
263 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
266 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
264 except error.RepoError as inst:
267 except error.RepoError as inst:
265 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
268 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
266
269
267 up = virtualrepo.rfind('/')
270 up = virtualrepo.rfind('/')
268 if up < 0:
271 if up < 0:
269 break
272 break
270 virtualrepo = virtualrepo[:up]
273 virtualrepo = virtualrepo[:up]
271
274
272 # browse subdirectories
275 # browse subdirectories
273 subdir = virtual + '/'
276 subdir = virtual + '/'
274 if [r for r in repos if r.startswith(subdir)]:
277 if [r for r in repos if r.startswith(subdir)]:
275 req.respond(HTTP_OK, ctype)
278 req.respond(HTTP_OK, ctype)
276 return self.makeindex(req, tmpl, subdir)
279 return self.makeindex(req, tmpl, subdir)
277
280
278 # prefixes not found
281 # prefixes not found
279 req.respond(HTTP_NOT_FOUND, ctype)
282 req.respond(HTTP_NOT_FOUND, ctype)
280 return tmpl("notfound", repo=virtual)
283 return tmpl("notfound", repo=virtual)
281
284
282 except ErrorResponse as err:
285 except ErrorResponse as err:
283 req.respond(err, ctype)
286 req.respond(err, ctype)
284 return tmpl('error', error=err.message or '')
287 return tmpl('error', error=err.message or '')
285 finally:
288 finally:
286 tmpl = None
289 tmpl = None
287
290
288 def makeindex(self, req, tmpl, subdir=""):
291 def makeindex(self, req, tmpl, subdir=""):
289
292
290 def archivelist(ui, nodeid, url):
293 def archivelist(ui, nodeid, url):
291 allowed = ui.configlist("web", "allow_archive", untrusted=True)
294 allowed = ui.configlist("web", "allow_archive", untrusted=True)
292 archives = []
295 archives = []
293 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
296 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
294 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
297 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
295 untrusted=True):
298 untrusted=True):
296 archives.append({"type" : i[0], "extension": i[1],
299 archives.append({"type" : i[0], "extension": i[1],
297 "node": nodeid, "url": url})
300 "node": nodeid, "url": url})
298 return archives
301 return archives
299
302
300 def rawentries(subdir="", **map):
303 def rawentries(subdir="", **map):
301
304
302 descend = self.ui.configbool('web', 'descend', True)
305 descend = self.ui.configbool('web', 'descend', True)
303 collapse = self.ui.configbool('web', 'collapse', False)
306 collapse = self.ui.configbool('web', 'collapse', False)
304 seenrepos = set()
307 seenrepos = set()
305 seendirs = set()
308 seendirs = set()
306 for name, path in self.repos:
309 for name, path in self.repos:
307
310
308 if not name.startswith(subdir):
311 if not name.startswith(subdir):
309 continue
312 continue
310 name = name[len(subdir):]
313 name = name[len(subdir):]
311 directory = False
314 directory = False
312
315
313 if '/' in name:
316 if '/' in name:
314 if not descend:
317 if not descend:
315 continue
318 continue
316
319
317 nameparts = name.split('/')
320 nameparts = name.split('/')
318 rootname = nameparts[0]
321 rootname = nameparts[0]
319
322
320 if not collapse:
323 if not collapse:
321 pass
324 pass
322 elif rootname in seendirs:
325 elif rootname in seendirs:
323 continue
326 continue
324 elif rootname in seenrepos:
327 elif rootname in seenrepos:
325 pass
328 pass
326 else:
329 else:
327 directory = True
330 directory = True
328 name = rootname
331 name = rootname
329
332
330 # redefine the path to refer to the directory
333 # redefine the path to refer to the directory
331 discarded = '/'.join(nameparts[1:])
334 discarded = '/'.join(nameparts[1:])
332
335
333 # remove name parts plus accompanying slash
336 # remove name parts plus accompanying slash
334 path = path[:-len(discarded) - 1]
337 path = path[:-len(discarded) - 1]
335
338
336 try:
339 try:
337 r = hg.repository(self.ui, path)
340 r = hg.repository(self.ui, path)
338 directory = False
341 directory = False
339 except (IOError, error.RepoError):
342 except (IOError, error.RepoError):
340 pass
343 pass
341
344
342 parts = [name]
345 parts = [name]
343 if 'PATH_INFO' in req.env:
346 if 'PATH_INFO' in req.env:
344 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
347 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
345 if req.env['SCRIPT_NAME']:
348 if req.env['SCRIPT_NAME']:
346 parts.insert(0, req.env['SCRIPT_NAME'])
349 parts.insert(0, req.env['SCRIPT_NAME'])
347 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
350 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
348
351
349 # show either a directory entry or a repository
352 # show either a directory entry or a repository
350 if directory:
353 if directory:
351 # get the directory's time information
354 # get the directory's time information
352 try:
355 try:
353 d = (get_mtime(path), util.makedate()[1])
356 d = (get_mtime(path), util.makedate()[1])
354 except OSError:
357 except OSError:
355 continue
358 continue
356
359
357 # add '/' to the name to make it obvious that
360 # add '/' to the name to make it obvious that
358 # the entry is a directory, not a regular repository
361 # the entry is a directory, not a regular repository
359 row = {'contact': "",
362 row = {'contact': "",
360 'contact_sort': "",
363 'contact_sort': "",
361 'name': name + '/',
364 'name': name + '/',
362 'name_sort': name,
365 'name_sort': name,
363 'url': url,
366 'url': url,
364 'description': "",
367 'description': "",
365 'description_sort': "",
368 'description_sort': "",
366 'lastchange': d,
369 'lastchange': d,
367 'lastchange_sort': d[1]-d[0],
370 'lastchange_sort': d[1]-d[0],
368 'archives': [],
371 'archives': [],
369 'isdirectory': True,
372 'isdirectory': True,
370 'labels': [],
373 'labels': [],
371 }
374 }
372
375
373 seendirs.add(name)
376 seendirs.add(name)
374 yield row
377 yield row
375 continue
378 continue
376
379
377 u = self.ui.copy()
380 u = self.ui.copy()
378 try:
381 try:
379 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
382 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
380 except Exception as e:
383 except Exception as e:
381 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
384 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
382 continue
385 continue
383 def get(section, name, default=None):
386 def get(section, name, default=None):
384 return u.config(section, name, default, untrusted=True)
387 return u.config(section, name, default, untrusted=True)
385
388
386 if u.configbool("web", "hidden", untrusted=True):
389 if u.configbool("web", "hidden", untrusted=True):
387 continue
390 continue
388
391
389 if not self.read_allowed(u, req):
392 if not self.read_allowed(u, req):
390 continue
393 continue
391
394
392 # update time with local timezone
395 # update time with local timezone
393 try:
396 try:
394 r = hg.repository(self.ui, path)
397 r = hg.repository(self.ui, path)
395 except IOError:
398 except IOError:
396 u.warn(_('error accessing repository at %s\n') % path)
399 u.warn(_('error accessing repository at %s\n') % path)
397 continue
400 continue
398 except error.RepoError:
401 except error.RepoError:
399 u.warn(_('error accessing repository at %s\n') % path)
402 u.warn(_('error accessing repository at %s\n') % path)
400 continue
403 continue
401 try:
404 try:
402 d = (get_mtime(r.spath), util.makedate()[1])
405 d = (get_mtime(r.spath), util.makedate()[1])
403 except OSError:
406 except OSError:
404 continue
407 continue
405
408
406 contact = get_contact(get)
409 contact = get_contact(get)
407 description = get("web", "description", "")
410 description = get("web", "description", "")
408 seenrepos.add(name)
411 seenrepos.add(name)
409 name = get("web", "name", name)
412 name = get("web", "name", name)
410 row = {'contact': contact or "unknown",
413 row = {'contact': contact or "unknown",
411 'contact_sort': contact.upper() or "unknown",
414 'contact_sort': contact.upper() or "unknown",
412 'name': name,
415 'name': name,
413 'name_sort': name,
416 'name_sort': name,
414 'url': url,
417 'url': url,
415 'description': description or "unknown",
418 'description': description or "unknown",
416 'description_sort': description.upper() or "unknown",
419 'description_sort': description.upper() or "unknown",
417 'lastchange': d,
420 'lastchange': d,
418 'lastchange_sort': d[1]-d[0],
421 'lastchange_sort': d[1]-d[0],
419 'archives': archivelist(u, "tip", url),
422 'archives': archivelist(u, "tip", url),
420 'isdirectory': None,
423 'isdirectory': None,
421 'labels': u.configlist('web', 'labels', untrusted=True),
424 'labels': u.configlist('web', 'labels', untrusted=True),
422 }
425 }
423
426
424 yield row
427 yield row
425
428
426 sortdefault = None, False
429 sortdefault = None, False
427 def entries(sortcolumn="", descending=False, subdir="", **map):
430 def entries(sortcolumn="", descending=False, subdir="", **map):
428 rows = rawentries(subdir=subdir, **map)
431 rows = rawentries(subdir=subdir, **map)
429
432
430 if sortcolumn and sortdefault != (sortcolumn, descending):
433 if sortcolumn and sortdefault != (sortcolumn, descending):
431 sortkey = '%s_sort' % sortcolumn
434 sortkey = '%s_sort' % sortcolumn
432 rows = sorted(rows, key=lambda x: x[sortkey],
435 rows = sorted(rows, key=lambda x: x[sortkey],
433 reverse=descending)
436 reverse=descending)
434 for row, parity in zip(rows, paritygen(self.stripecount)):
437 for row, parity in zip(rows, paritygen(self.stripecount)):
435 row['parity'] = parity
438 row['parity'] = parity
436 yield row
439 yield row
437
440
438 self.refresh()
441 self.refresh()
439 sortable = ["name", "description", "contact", "lastchange"]
442 sortable = ["name", "description", "contact", "lastchange"]
440 sortcolumn, descending = sortdefault
443 sortcolumn, descending = sortdefault
441 if 'sort' in req.form:
444 if 'sort' in req.form:
442 sortcolumn = req.form['sort'][0]
445 sortcolumn = req.form['sort'][0]
443 descending = sortcolumn.startswith('-')
446 descending = sortcolumn.startswith('-')
444 if descending:
447 if descending:
445 sortcolumn = sortcolumn[1:]
448 sortcolumn = sortcolumn[1:]
446 if sortcolumn not in sortable:
449 if sortcolumn not in sortable:
447 sortcolumn = ""
450 sortcolumn = ""
448
451
449 sort = [("sort_%s" % column,
452 sort = [("sort_%s" % column,
450 "%s%s" % ((not descending and column == sortcolumn)
453 "%s%s" % ((not descending and column == sortcolumn)
451 and "-" or "", column))
454 and "-" or "", column))
452 for column in sortable]
455 for column in sortable]
453
456
454 self.refresh()
457 self.refresh()
455 self.updatereqenv(req.env)
458 self.updatereqenv(req.env)
456
459
457 return tmpl("index", entries=entries, subdir=subdir,
460 return tmpl("index", entries=entries, subdir=subdir,
458 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
461 pathdef=hgweb_mod.makebreadcrumb('/' + subdir, self.prefix),
459 sortcolumn=sortcolumn, descending=descending,
462 sortcolumn=sortcolumn, descending=descending,
460 **dict(sort))
463 **dict(sort))
461
464
462 def templater(self, req):
465 def templater(self, req):
463
466
464 def motd(**map):
467 def motd(**map):
465 if self.motd is not None:
468 if self.motd is not None:
466 yield self.motd
469 yield self.motd
467 else:
470 else:
468 yield config('web', 'motd', '')
471 yield config('web', 'motd', '')
469
472
470 def config(section, name, default=None, untrusted=True):
473 def config(section, name, default=None, untrusted=True):
471 return self.ui.config(section, name, default, untrusted)
474 return self.ui.config(section, name, default, untrusted)
472
475
473 self.updatereqenv(req.env)
476 self.updatereqenv(req.env)
474
477
475 url = req.env.get('SCRIPT_NAME', '')
478 url = req.env.get('SCRIPT_NAME', '')
476 if not url.endswith('/'):
479 if not url.endswith('/'):
477 url += '/'
480 url += '/'
478
481
479 vars = {}
482 vars = {}
480 styles = (
483 styles = (
481 req.form.get('style', [None])[0],
484 req.form.get('style', [None])[0],
482 config('web', 'style'),
485 config('web', 'style'),
483 'paper'
486 'paper'
484 )
487 )
485 style, mapfile = templater.stylemap(styles, self.templatepath)
488 style, mapfile = templater.stylemap(styles, self.templatepath)
486 if style == styles[0]:
489 if style == styles[0]:
487 vars['style'] = style
490 vars['style'] = style
488
491
489 start = url[-1] == '?' and '&' or '?'
492 start = url[-1] == '?' and '&' or '?'
490 sessionvars = webutil.sessionvars(vars, start)
493 sessionvars = webutil.sessionvars(vars, start)
491 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
494 logourl = config('web', 'logourl', 'https://mercurial-scm.org/')
492 logoimg = config('web', 'logoimg', 'hglogo.png')
495 logoimg = config('web', 'logoimg', 'hglogo.png')
493 staticurl = config('web', 'staticurl') or url + 'static/'
496 staticurl = config('web', 'staticurl') or url + 'static/'
494 if not staticurl.endswith('/'):
497 if not staticurl.endswith('/'):
495 staticurl += '/'
498 staticurl += '/'
496
499
497 defaults = {
500 defaults = {
498 "encoding": encoding.encoding,
501 "encoding": encoding.encoding,
499 "motd": motd,
502 "motd": motd,
500 "url": url,
503 "url": url,
501 "logourl": logourl,
504 "logourl": logourl,
502 "logoimg": logoimg,
505 "logoimg": logoimg,
503 "staticurl": staticurl,
506 "staticurl": staticurl,
504 "sessionvars": sessionvars,
507 "sessionvars": sessionvars,
505 "style": style,
508 "style": style,
506 }
509 }
507 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
510 tmpl = templater.templater.frommapfile(mapfile, defaults=defaults)
508 return tmpl
511 return tmpl
509
512
510 def updatereqenv(self, env):
513 def updatereqenv(self, env):
511 if self._baseurl is not None:
514 if self._baseurl is not None:
512 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
515 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
513 env['SERVER_NAME'] = name
516 env['SERVER_NAME'] = name
514 env['SERVER_PORT'] = port
517 env['SERVER_PORT'] = port
515 env['SCRIPT_NAME'] = path
518 env['SCRIPT_NAME'] = path
General Comments 0
You need to be logged in to leave comments. Login now