hgwerc
view hgwebdir_mod.py @ 2:7ac7dee24f7f
fixed tags.tmpl
| author | yiyus@1936 |
|---|---|
| date | Thu Sep 03 21:09:34 2009 +0200 (2009-09-03 ago) |
| parents | |
| children |
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
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.
9 import os, re, time
10 from mercurial.i18n import _
11 from mercurial import ui, hg, util, templater
12 from mercurial import error, encoding
13 from common import ErrorResponse, get_mtime, staticfile, paritygen,\
14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from hgweb_mod import hgweb
16 from request import wsgirequest
17 import webutil
19 def cleannames(items):
20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
22 def findrepos(paths):
23 repos = {}
24 for prefix, root in cleannames(paths):
25 roothead, roottail = os.path.split(root)
26 # "foo = /bar/*" makes every subrepo of /bar/ to be
27 # mounted as foo/subrepo
28 # and "foo = /bar/**" also recurses into the subdirectories,
29 # remember to use it without working dir.
30 try:
31 recurse = {'*': False, '**': True}[roottail]
32 except KeyError:
33 repos[prefix] = root
34 continue
35 roothead = os.path.normpath(roothead)
36 for path in util.walkrepos(roothead, followsym=True, recurse=recurse):
37 path = os.path.normpath(path)
38 name = util.pconvert(path[len(roothead):]).strip('/')
39 if prefix:
40 name = prefix + '/' + name
41 repos[name] = path
42 return repos.items()
44 class hgwebdir(object):
45 refreshinterval = 20
47 def __init__(self, conf, baseui=None):
48 self.conf = conf
49 self.baseui = baseui
50 self.lastrefresh = 0
51 self.refresh()
53 def refresh(self):
54 if self.lastrefresh + self.refreshinterval > time.time():
55 return
57 if self.baseui:
58 self.ui = self.baseui.copy()
59 else:
60 self.ui = ui.ui()
61 self.ui.setconfig('ui', 'report_untrusted', 'off')
62 self.ui.setconfig('ui', 'interactive', 'off')
64 if not isinstance(self.conf, (dict, list, tuple)):
65 map = {'paths': 'hgweb-paths'}
66 self.ui.readconfig(self.conf, remap=map, trust=True)
67 paths = self.ui.configitems('hgweb-paths')
68 elif isinstance(self.conf, (list, tuple)):
69 paths = self.conf
70 elif isinstance(self.conf, dict):
71 paths = self.conf.items()
73 encoding.encoding = self.ui.config('web', 'encoding',
74 encoding.encoding)
75 self.motd = self.ui.config('web', 'motd')
76 self.style = self.ui.config('web', 'style', 'paper')
77 self.stripecount = self.ui.config('web', 'stripes', 1)
78 if self.stripecount:
79 self.stripecount = int(self.stripecount)
80 self._baseurl = self.ui.config('web', 'baseurl')
82 self.repos = findrepos(paths)
83 for prefix, root in self.ui.configitems('collections'):
84 prefix = util.pconvert(prefix)
85 for path in util.walkrepos(root, followsym=True):
86 repo = os.path.normpath(path)
87 name = util.pconvert(repo)
88 if name.startswith(prefix):
89 name = name[len(prefix):]
90 self.repos.append((name.lstrip('/'), repo))
92 self.repos.sort()
93 self.lastrefresh = time.time()
95 def run(self):
96 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
97 raise RuntimeError("This function is only intended to be "
98 "called while running as a CGI script.")
99 import mercurial.hgweb.wsgicgi as wsgicgi
100 wsgicgi.launch(self)
102 def __call__(self, env, respond):
103 req = wsgirequest(env, respond)
104 return self.run_wsgi(req)
106 def read_allowed(self, ui, req):
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
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
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
113 if user is allowed to read the repo, else return False."""
115 user = req.env.get('REMOTE_USER')
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):
119 return False
121 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
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):
124 return True
126 return False
128 def run_wsgi(self, req):
129 try:
130 try:
131 self.refresh()
133 virtual = req.env.get("PATH_INFO", "").strip('/')
134 tmpl = self.templater(req)
135 ctype = tmpl('mimetype', encoding=encoding.encoding)
136 ctype = templater.stringify(ctype)
138 # a static file
139 if virtual.startswith('static/') or 'static' in req.form:
140 if virtual.startswith('static/'):
141 fname = virtual[7:]
142 else:
143 fname = req.form['static'][0]
144 static = templater.templatepath('static')
145 return (staticfile(static, fname, req),)
147 # top-level index
148 elif not virtual:
149 req.respond(HTTP_OK, ctype)
150 return self.makeindex(req, tmpl)
152 # nested indexes and hgwebs
154 repos = dict(self.repos)
155 while virtual:
156 real = repos.get(virtual)
157 if real:
158 req.env['REPO_NAME'] = virtual
159 try:
160 repo = hg.repository(self.ui, real)
161 return hgweb(repo).run_wsgi(req)
162 except IOError, inst:
163 msg = inst.strerror
164 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
165 except error.RepoError, inst:
166 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
168 # browse subdirectories
169 subdir = virtual + '/'
170 if [r for r in repos if r.startswith(subdir)]:
171 req.respond(HTTP_OK, ctype)
172 return self.makeindex(req, tmpl, subdir)
174 up = virtual.rfind('/')
175 if up < 0:
176 break
177 virtual = virtual[:up]
179 # prefixes not found
180 req.respond(HTTP_NOT_FOUND, ctype)
181 return tmpl("notfound", repo=virtual)
183 except ErrorResponse, err:
184 req.respond(err, ctype)
185 return tmpl('error', error=err.message or '')
186 finally:
187 tmpl = None
189 def makeindex(self, req, tmpl, subdir=""):
191 def archivelist(ui, nodeid, url):
192 allowed = ui.configlist("web", "allow_archive", untrusted=True)
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],
195 untrusted=True):
196 yield {"type" : i[0], "extension": i[1],
197 "node": nodeid, "url": url}
199 sortdefault = 'name', False
200 def entries(sortcolumn="", descending=False, subdir="", **map):
201 rows = []
202 parity = paritygen(self.stripecount)
203 for name, path in self.repos:
204 if not name.startswith(subdir):
205 continue
206 name = name[len(subdir):]
208 u = self.ui.copy()
209 try:
210 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
211 except Exception, e:
212 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
213 continue
214 def get(section, name, default=None):
215 return u.config(section, name, default, untrusted=True)
217 if u.configbool("web", "hidden", untrusted=True):
218 continue
220 if not self.read_allowed(u, req):
221 continue
223 parts = [name]
224 if 'PATH_INFO' in req.env:
225 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
226 if req.env['SCRIPT_NAME']:
227 parts.insert(0, req.env['SCRIPT_NAME'])
228 m = re.match('((?:https?://)?)(.*)', '/'.join(parts))
229 # squish repeated slashes out of the path component
230 url = m.group(1) + re.sub('/+', '/', m.group(2)) + '/'
232 # update time with local timezone
233 try:
234 d = (get_mtime(path), util.makedate()[1])
235 except OSError:
236 continue
238 contact = get_contact(get)
239 description = get("web", "description", "")
240 name = get("web", "name", name)
241 row = dict(contact=contact or "unknown",
242 contact_sort=contact.upper() or "unknown",
243 name=name,
244 name_sort=name,
245 url=url,
246 description=description or "unknown",
247 description_sort=description.upper() or "unknown",
248 lastchange=d,
249 lastchange_sort=d[1]-d[0],
250 archives=archivelist(u, "tip", url))
251 if (not sortcolumn or (sortcolumn, descending) == sortdefault):
252 # fast path for unsorted output
253 row['parity'] = parity.next()
254 yield row
255 else:
256 rows.append((row["%s_sort" % sortcolumn], row))
257 if rows:
258 rows.sort()
259 if descending:
260 rows.reverse()
261 for key, row in rows:
262 row['parity'] = parity.next()
263 yield row
265 self.refresh()
266 sortable = ["name", "description", "contact", "lastchange"]
267 sortcolumn, descending = sortdefault
268 if 'sort' in req.form:
269 sortcolumn = req.form['sort'][0]
270 descending = sortcolumn.startswith('-')
271 if descending:
272 sortcolumn = sortcolumn[1:]
273 if sortcolumn not in sortable:
274 sortcolumn = ""
276 sort = [("sort_%s" % column,
277 "%s%s" % ((not descending and column == sortcolumn)
278 and "-" or "", column))
279 for column in sortable]
281 self.refresh()
282 if self._baseurl is not None:
283 req.env['SCRIPT_NAME'] = self._baseurl
285 return tmpl("index", entries=entries, subdir=subdir,
286 sortcolumn=sortcolumn, descending=descending,
287 **dict(sort))
289 def templater(self, req):
291 def header(**map):
292 yield tmpl('header', encoding=encoding.encoding, **map)
294 def footer(**map):
295 yield tmpl("footer", **map)
297 def extra1(**map):
298 yield tmpl("extra1", **map)
300 def extra2(**map):
301 yield tmpl("extra2", **map)
303 def extra3(**map):
304 yield tmpl("extra3", **map)
306 def motd(**map):
307 if self.motd is not None:
308 yield self.motd
309 else:
310 yield config('web', 'motd', '')
312 def config(section, name, default=None, untrusted=True):
313 return self.ui.config(section, name, default, untrusted)
315 if self._baseurl is not None:
316 req.env['SCRIPT_NAME'] = self._baseurl
318 url = req.env.get('SCRIPT_NAME', '')
319 if not url.endswith('/'):
320 url += '/'
322 vars = {}
323 style = self.style
324 if 'style' in req.form:
325 vars['style'] = style = req.form['style'][0]
326 start = url[-1] == '?' and '&' or '?'
327 sessionvars = webutil.sessionvars(vars, start)
329 staticurl = config('web', 'staticurl') or url + 'static/'
330 if not staticurl.endswith('/'):
331 staticurl += '/'
333 style = 'style' in req.form and req.form['style'][0] or self.style
334 mapfile = templater.stylemap(style)
335 tmpl = templater.templater(mapfile,
336 defaults={"header": header,
337 "footer": footer,
338 "extra1": extra1,
339 "extra2": extra2,
340 "extra3": extra3,
341 "motd": motd,
342 "url": url,
343 "staticurl": staticurl,
344 "sessionvars": sessionvars})
345 return tmpl