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