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