##// END OF EJS Templates
hgweb: use a tuple-list instead of dictionary for append-only store
Dirkjan Ochtman -
r9723:a235644a default
parent child Browse files
Show More
@@ -1,338 +1,338 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, incorporated herein by reference.
7 # GNU General Public License version 2, incorporated herein by reference.
8
8
9 import os, re, time
9 import os, re, time
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11 from mercurial import ui, hg, util, templater
11 from mercurial import ui, hg, util, templater
12 from mercurial import error, encoding
12 from mercurial import error, encoding
13 from common import ErrorResponse, get_mtime, staticfile, paritygen,\
13 from common import ErrorResponse, get_mtime, staticfile, paritygen,\
14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from hgweb_mod import hgweb
15 from hgweb_mod import hgweb
16 from request import wsgirequest
16 from request import wsgirequest
17 import webutil
17 import webutil
18
18
19 def cleannames(items):
19 def cleannames(items):
20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
21
21
22 def findrepos(paths):
22 def findrepos(paths):
23 repos = {}
23 repos = []
24 for prefix, root in cleannames(paths):
24 for prefix, root in cleannames(paths):
25 roothead, roottail = os.path.split(root)
25 roothead, roottail = os.path.split(root)
26 # "foo = /bar/*" makes every subrepo of /bar/ to be
26 # "foo = /bar/*" makes every subrepo of /bar/ to be
27 # mounted as foo/subrepo
27 # mounted as foo/subrepo
28 # and "foo = /bar/**" also recurses into the subdirectories,
28 # and "foo = /bar/**" also recurses into the subdirectories,
29 # remember to use it without working dir.
29 # remember to use it without working dir.
30 try:
30 try:
31 recurse = {'*': False, '**': True}[roottail]
31 recurse = {'*': False, '**': True}[roottail]
32 except KeyError:
32 except KeyError:
33 repos[prefix] = root
33 repos.append((prefix, root))
34 continue
34 continue
35 roothead = os.path.normpath(roothead)
35 roothead = os.path.normpath(roothead)
36 for path in util.walkrepos(roothead, followsym=True, recurse=recurse):
36 for path in util.walkrepos(roothead, followsym=True, recurse=recurse):
37 path = os.path.normpath(path)
37 path = os.path.normpath(path)
38 name = util.pconvert(path[len(roothead):]).strip('/')
38 name = util.pconvert(path[len(roothead):]).strip('/')
39 if prefix:
39 if prefix:
40 name = prefix + '/' + name
40 name = prefix + '/' + name
41 repos[name] = path
41 repos.append((name, path))
42 return repos.items()
42 return repos
43
43
44 class hgwebdir(object):
44 class hgwebdir(object):
45 refreshinterval = 20
45 refreshinterval = 20
46
46
47 def __init__(self, conf, baseui=None):
47 def __init__(self, conf, baseui=None):
48 self.conf = conf
48 self.conf = conf
49 self.baseui = baseui
49 self.baseui = baseui
50 self.lastrefresh = 0
50 self.lastrefresh = 0
51 self.refresh()
51 self.refresh()
52
52
53 def refresh(self):
53 def refresh(self):
54 if self.lastrefresh + self.refreshinterval > time.time():
54 if self.lastrefresh + self.refreshinterval > time.time():
55 return
55 return
56
56
57 if self.baseui:
57 if self.baseui:
58 self.ui = self.baseui.copy()
58 self.ui = self.baseui.copy()
59 else:
59 else:
60 self.ui = ui.ui()
60 self.ui = ui.ui()
61 self.ui.setconfig('ui', 'report_untrusted', 'off')
61 self.ui.setconfig('ui', 'report_untrusted', 'off')
62 self.ui.setconfig('ui', 'interactive', 'off')
62 self.ui.setconfig('ui', 'interactive', 'off')
63
63
64 if not isinstance(self.conf, (dict, list, tuple)):
64 if not isinstance(self.conf, (dict, list, tuple)):
65 map = {'paths': 'hgweb-paths'}
65 map = {'paths': 'hgweb-paths'}
66 self.ui.readconfig(self.conf, remap=map, trust=True)
66 self.ui.readconfig(self.conf, remap=map, trust=True)
67 paths = self.ui.configitems('hgweb-paths')
67 paths = self.ui.configitems('hgweb-paths')
68 elif isinstance(self.conf, (list, tuple)):
68 elif isinstance(self.conf, (list, tuple)):
69 paths = self.conf
69 paths = self.conf
70 elif isinstance(self.conf, dict):
70 elif isinstance(self.conf, dict):
71 paths = self.conf.items()
71 paths = self.conf.items()
72
72
73 encoding.encoding = self.ui.config('web', 'encoding',
73 encoding.encoding = self.ui.config('web', 'encoding',
74 encoding.encoding)
74 encoding.encoding)
75 self.motd = self.ui.config('web', 'motd')
75 self.motd = self.ui.config('web', 'motd')
76 self.style = self.ui.config('web', 'style', 'paper')
76 self.style = self.ui.config('web', 'style', 'paper')
77 self.stripecount = self.ui.config('web', 'stripes', 1)
77 self.stripecount = self.ui.config('web', 'stripes', 1)
78 if self.stripecount:
78 if self.stripecount:
79 self.stripecount = int(self.stripecount)
79 self.stripecount = int(self.stripecount)
80 self._baseurl = self.ui.config('web', 'baseurl')
80 self._baseurl = self.ui.config('web', 'baseurl')
81
81
82 self.repos = findrepos(paths)
82 self.repos = findrepos(paths)
83 for prefix, root in self.ui.configitems('collections'):
83 for prefix, root in self.ui.configitems('collections'):
84 prefix = util.pconvert(prefix)
84 prefix = util.pconvert(prefix)
85 for path in util.walkrepos(root, followsym=True):
85 for path in util.walkrepos(root, followsym=True):
86 repo = os.path.normpath(path)
86 repo = os.path.normpath(path)
87 name = util.pconvert(repo)
87 name = util.pconvert(repo)
88 if name.startswith(prefix):
88 if name.startswith(prefix):
89 name = name[len(prefix):]
89 name = name[len(prefix):]
90 self.repos.append((name.lstrip('/'), repo))
90 self.repos.append((name.lstrip('/'), repo))
91
91
92 self.repos.sort()
92 self.repos.sort()
93 self.lastrefresh = time.time()
93 self.lastrefresh = time.time()
94
94
95 def run(self):
95 def run(self):
96 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
96 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
97 raise RuntimeError("This function is only intended to be "
97 raise RuntimeError("This function is only intended to be "
98 "called while running as a CGI script.")
98 "called while running as a CGI script.")
99 import mercurial.hgweb.wsgicgi as wsgicgi
99 import mercurial.hgweb.wsgicgi as wsgicgi
100 wsgicgi.launch(self)
100 wsgicgi.launch(self)
101
101
102 def __call__(self, env, respond):
102 def __call__(self, env, respond):
103 req = wsgirequest(env, respond)
103 req = wsgirequest(env, respond)
104 return self.run_wsgi(req)
104 return self.run_wsgi(req)
105
105
106 def read_allowed(self, ui, req):
106 def read_allowed(self, ui, req):
107 """Check allow_read and deny_read config options of a repo's ui object
107 """Check allow_read and deny_read config options of a repo's ui object
108 to determine user permissions. By default, with neither option set (or
108 to determine user permissions. By default, with neither option set (or
109 both empty), allow all users to read the repo. There are two ways a
109 both empty), allow all users to read the repo. There are two ways a
110 user can be denied read access: (1) deny_read is not empty, and the
110 user can be denied read access: (1) deny_read is not empty, and the
111 user is unauthenticated or deny_read contains user (or *), and (2)
111 user is unauthenticated or deny_read contains user (or *), and (2)
112 allow_read is not empty and the user is not in allow_read. Return True
112 allow_read is not empty and the user is not in allow_read. Return True
113 if user is allowed to read the repo, else return False."""
113 if user is allowed to read the repo, else return False."""
114
114
115 user = req.env.get('REMOTE_USER')
115 user = req.env.get('REMOTE_USER')
116
116
117 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
117 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
118 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
118 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
119 return False
119 return False
120
120
121 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
121 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
122 # by default, allow reading if no allow_read option has been set
122 # by default, allow reading if no allow_read option has been set
123 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
123 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
124 return True
124 return True
125
125
126 return False
126 return False
127
127
128 def run_wsgi(self, req):
128 def run_wsgi(self, req):
129 try:
129 try:
130 try:
130 try:
131 self.refresh()
131 self.refresh()
132
132
133 virtual = req.env.get("PATH_INFO", "").strip('/')
133 virtual = req.env.get("PATH_INFO", "").strip('/')
134 tmpl = self.templater(req)
134 tmpl = self.templater(req)
135 ctype = tmpl('mimetype', encoding=encoding.encoding)
135 ctype = tmpl('mimetype', encoding=encoding.encoding)
136 ctype = templater.stringify(ctype)
136 ctype = templater.stringify(ctype)
137
137
138 # a static file
138 # a static file
139 if virtual.startswith('static/') or 'static' in req.form:
139 if virtual.startswith('static/') or 'static' in req.form:
140 if virtual.startswith('static/'):
140 if virtual.startswith('static/'):
141 fname = virtual[7:]
141 fname = virtual[7:]
142 else:
142 else:
143 fname = req.form['static'][0]
143 fname = req.form['static'][0]
144 static = templater.templatepath('static')
144 static = templater.templatepath('static')
145 return (staticfile(static, fname, req),)
145 return (staticfile(static, fname, req),)
146
146
147 # top-level index
147 # top-level index
148 elif not virtual:
148 elif not virtual:
149 req.respond(HTTP_OK, ctype)
149 req.respond(HTTP_OK, ctype)
150 return self.makeindex(req, tmpl)
150 return self.makeindex(req, tmpl)
151
151
152 # nested indexes and hgwebs
152 # nested indexes and hgwebs
153
153
154 repos = dict(self.repos)
154 repos = dict(self.repos)
155 while virtual:
155 while virtual:
156 real = repos.get(virtual)
156 real = repos.get(virtual)
157 if real:
157 if real:
158 req.env['REPO_NAME'] = virtual
158 req.env['REPO_NAME'] = virtual
159 try:
159 try:
160 repo = hg.repository(self.ui, real)
160 repo = hg.repository(self.ui, real)
161 return hgweb(repo).run_wsgi(req)
161 return hgweb(repo).run_wsgi(req)
162 except IOError, inst:
162 except IOError, inst:
163 msg = inst.strerror
163 msg = inst.strerror
164 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
164 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
165 except error.RepoError, inst:
165 except error.RepoError, inst:
166 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
166 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
167
167
168 # browse subdirectories
168 # browse subdirectories
169 subdir = virtual + '/'
169 subdir = virtual + '/'
170 if [r for r in repos if r.startswith(subdir)]:
170 if [r for r in repos if r.startswith(subdir)]:
171 req.respond(HTTP_OK, ctype)
171 req.respond(HTTP_OK, ctype)
172 return self.makeindex(req, tmpl, subdir)
172 return self.makeindex(req, tmpl, subdir)
173
173
174 up = virtual.rfind('/')
174 up = virtual.rfind('/')
175 if up < 0:
175 if up < 0:
176 break
176 break
177 virtual = virtual[:up]
177 virtual = virtual[:up]
178
178
179 # prefixes not found
179 # prefixes not found
180 req.respond(HTTP_NOT_FOUND, ctype)
180 req.respond(HTTP_NOT_FOUND, ctype)
181 return tmpl("notfound", repo=virtual)
181 return tmpl("notfound", repo=virtual)
182
182
183 except ErrorResponse, err:
183 except ErrorResponse, err:
184 req.respond(err, ctype)
184 req.respond(err, ctype)
185 return tmpl('error', error=err.message or '')
185 return tmpl('error', error=err.message or '')
186 finally:
186 finally:
187 tmpl = None
187 tmpl = None
188
188
189 def makeindex(self, req, tmpl, subdir=""):
189 def makeindex(self, req, tmpl, subdir=""):
190
190
191 def archivelist(ui, nodeid, url):
191 def archivelist(ui, nodeid, url):
192 allowed = ui.configlist("web", "allow_archive", untrusted=True)
192 allowed = ui.configlist("web", "allow_archive", untrusted=True)
193 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
193 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
194 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
194 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
195 untrusted=True):
195 untrusted=True):
196 yield {"type" : i[0], "extension": i[1],
196 yield {"type" : i[0], "extension": i[1],
197 "node": nodeid, "url": url}
197 "node": nodeid, "url": url}
198
198
199 sortdefault = 'name', False
199 sortdefault = 'name', False
200 def entries(sortcolumn="", descending=False, subdir="", **map):
200 def entries(sortcolumn="", descending=False, subdir="", **map):
201
201
202 rows = []
202 rows = []
203 parity = paritygen(self.stripecount)
203 parity = paritygen(self.stripecount)
204 descend = self.ui.configbool('web', 'descend', True)
204 descend = self.ui.configbool('web', 'descend', True)
205 for name, path in self.repos:
205 for name, path in self.repos:
206
206
207 if not name.startswith(subdir):
207 if not name.startswith(subdir):
208 continue
208 continue
209 name = name[len(subdir):]
209 name = name[len(subdir):]
210 if not descend and '/' in name:
210 if not descend and '/' in name:
211 continue
211 continue
212
212
213 u = self.ui.copy()
213 u = self.ui.copy()
214 try:
214 try:
215 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
215 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
216 except Exception, e:
216 except Exception, e:
217 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
217 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
218 continue
218 continue
219 def get(section, name, default=None):
219 def get(section, name, default=None):
220 return u.config(section, name, default, untrusted=True)
220 return u.config(section, name, default, untrusted=True)
221
221
222 if u.configbool("web", "hidden", untrusted=True):
222 if u.configbool("web", "hidden", untrusted=True):
223 continue
223 continue
224
224
225 if not self.read_allowed(u, req):
225 if not self.read_allowed(u, req):
226 continue
226 continue
227
227
228 parts = [name]
228 parts = [name]
229 if 'PATH_INFO' in req.env:
229 if 'PATH_INFO' in req.env:
230 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
230 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
231 if req.env['SCRIPT_NAME']:
231 if req.env['SCRIPT_NAME']:
232 parts.insert(0, req.env['SCRIPT_NAME'])
232 parts.insert(0, req.env['SCRIPT_NAME'])
233 m = re.match('((?:https?://)?)(.*)', '/'.join(parts))
233 m = re.match('((?:https?://)?)(.*)', '/'.join(parts))
234 # squish repeated slashes out of the path component
234 # squish repeated slashes out of the path component
235 url = m.group(1) + re.sub('/+', '/', m.group(2)) + '/'
235 url = m.group(1) + re.sub('/+', '/', m.group(2)) + '/'
236
236
237 # update time with local timezone
237 # update time with local timezone
238 try:
238 try:
239 d = (get_mtime(path), util.makedate()[1])
239 d = (get_mtime(path), util.makedate()[1])
240 except OSError:
240 except OSError:
241 continue
241 continue
242
242
243 contact = get_contact(get)
243 contact = get_contact(get)
244 description = get("web", "description", "")
244 description = get("web", "description", "")
245 name = get("web", "name", name)
245 name = get("web", "name", name)
246 row = dict(contact=contact or "unknown",
246 row = dict(contact=contact or "unknown",
247 contact_sort=contact.upper() or "unknown",
247 contact_sort=contact.upper() or "unknown",
248 name=name,
248 name=name,
249 name_sort=name,
249 name_sort=name,
250 url=url,
250 url=url,
251 description=description or "unknown",
251 description=description or "unknown",
252 description_sort=description.upper() or "unknown",
252 description_sort=description.upper() or "unknown",
253 lastchange=d,
253 lastchange=d,
254 lastchange_sort=d[1]-d[0],
254 lastchange_sort=d[1]-d[0],
255 archives=archivelist(u, "tip", url))
255 archives=archivelist(u, "tip", url))
256 if (not sortcolumn or (sortcolumn, descending) == sortdefault):
256 if (not sortcolumn or (sortcolumn, descending) == sortdefault):
257 # fast path for unsorted output
257 # fast path for unsorted output
258 row['parity'] = parity.next()
258 row['parity'] = parity.next()
259 yield row
259 yield row
260 else:
260 else:
261 rows.append((row["%s_sort" % sortcolumn], row))
261 rows.append((row["%s_sort" % sortcolumn], row))
262 if rows:
262 if rows:
263 rows.sort()
263 rows.sort()
264 if descending:
264 if descending:
265 rows.reverse()
265 rows.reverse()
266 for key, row in rows:
266 for key, row in rows:
267 row['parity'] = parity.next()
267 row['parity'] = parity.next()
268 yield row
268 yield row
269
269
270 self.refresh()
270 self.refresh()
271 sortable = ["name", "description", "contact", "lastchange"]
271 sortable = ["name", "description", "contact", "lastchange"]
272 sortcolumn, descending = sortdefault
272 sortcolumn, descending = sortdefault
273 if 'sort' in req.form:
273 if 'sort' in req.form:
274 sortcolumn = req.form['sort'][0]
274 sortcolumn = req.form['sort'][0]
275 descending = sortcolumn.startswith('-')
275 descending = sortcolumn.startswith('-')
276 if descending:
276 if descending:
277 sortcolumn = sortcolumn[1:]
277 sortcolumn = sortcolumn[1:]
278 if sortcolumn not in sortable:
278 if sortcolumn not in sortable:
279 sortcolumn = ""
279 sortcolumn = ""
280
280
281 sort = [("sort_%s" % column,
281 sort = [("sort_%s" % column,
282 "%s%s" % ((not descending and column == sortcolumn)
282 "%s%s" % ((not descending and column == sortcolumn)
283 and "-" or "", column))
283 and "-" or "", column))
284 for column in sortable]
284 for column in sortable]
285
285
286 self.refresh()
286 self.refresh()
287 if self._baseurl is not None:
287 if self._baseurl is not None:
288 req.env['SCRIPT_NAME'] = self._baseurl
288 req.env['SCRIPT_NAME'] = self._baseurl
289
289
290 return tmpl("index", entries=entries, subdir=subdir,
290 return tmpl("index", entries=entries, subdir=subdir,
291 sortcolumn=sortcolumn, descending=descending,
291 sortcolumn=sortcolumn, descending=descending,
292 **dict(sort))
292 **dict(sort))
293
293
294 def templater(self, req):
294 def templater(self, req):
295
295
296 def header(**map):
296 def header(**map):
297 yield tmpl('header', encoding=encoding.encoding, **map)
297 yield tmpl('header', encoding=encoding.encoding, **map)
298
298
299 def footer(**map):
299 def footer(**map):
300 yield tmpl("footer", **map)
300 yield tmpl("footer", **map)
301
301
302 def motd(**map):
302 def motd(**map):
303 if self.motd is not None:
303 if self.motd is not None:
304 yield self.motd
304 yield self.motd
305 else:
305 else:
306 yield config('web', 'motd', '')
306 yield config('web', 'motd', '')
307
307
308 def config(section, name, default=None, untrusted=True):
308 def config(section, name, default=None, untrusted=True):
309 return self.ui.config(section, name, default, untrusted)
309 return self.ui.config(section, name, default, untrusted)
310
310
311 if self._baseurl is not None:
311 if self._baseurl is not None:
312 req.env['SCRIPT_NAME'] = self._baseurl
312 req.env['SCRIPT_NAME'] = self._baseurl
313
313
314 url = req.env.get('SCRIPT_NAME', '')
314 url = req.env.get('SCRIPT_NAME', '')
315 if not url.endswith('/'):
315 if not url.endswith('/'):
316 url += '/'
316 url += '/'
317
317
318 vars = {}
318 vars = {}
319 style = self.style
319 style = self.style
320 if 'style' in req.form:
320 if 'style' in req.form:
321 vars['style'] = style = req.form['style'][0]
321 vars['style'] = style = req.form['style'][0]
322 start = url[-1] == '?' and '&' or '?'
322 start = url[-1] == '?' and '&' or '?'
323 sessionvars = webutil.sessionvars(vars, start)
323 sessionvars = webutil.sessionvars(vars, start)
324
324
325 staticurl = config('web', 'staticurl') or url + 'static/'
325 staticurl = config('web', 'staticurl') or url + 'static/'
326 if not staticurl.endswith('/'):
326 if not staticurl.endswith('/'):
327 staticurl += '/'
327 staticurl += '/'
328
328
329 style = 'style' in req.form and req.form['style'][0] or self.style
329 style = 'style' in req.form and req.form['style'][0] or self.style
330 mapfile = templater.stylemap(style)
330 mapfile = templater.stylemap(style)
331 tmpl = templater.templater(mapfile,
331 tmpl = templater.templater(mapfile,
332 defaults={"header": header,
332 defaults={"header": header,
333 "footer": footer,
333 "footer": footer,
334 "motd": motd,
334 "motd": motd,
335 "url": url,
335 "url": url,
336 "staticurl": staticurl,
336 "staticurl": staticurl,
337 "sessionvars": sessionvars})
337 "sessionvars": sessionvars})
338 return tmpl
338 return tmpl
General Comments 0
You need to be logged in to leave comments. Login now