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