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