##// END OF EJS Templates
Support web.baseurl in hgwebdir, overriding SCRIPT_NAME
Brendan Cully -
r6221:2eb18c78 default
parent child Browse files
Show More
@@ -1,279 +1,288 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 gettext as _
10 from mercurial.i18n import gettext as _
11 from mercurial.repo import RepoError
11 from mercurial.repo import RepoError
12 from mercurial import ui, hg, util, templater, templatefilters
12 from mercurial import ui, hg, util, templater, templatefilters
13 from common import ErrorResponse, get_mtime, staticfile, style_map, paritygen,\
13 from common import ErrorResponse, get_mtime, staticfile, style_map, 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 # This is a stopgap
19 class hgwebdir(object):
19 class hgwebdir(object):
20 def __init__(self, config, parentui=None):
20 def __init__(self, config, parentui=None):
21 def cleannames(items):
21 def cleannames(items):
22 return [(util.pconvert(name).strip('/'), path)
22 return [(util.pconvert(name).strip('/'), path)
23 for name, path in items]
23 for name, path in items]
24
24
25 self.parentui = parentui or ui.ui(report_untrusted=False,
25 self.parentui = parentui or ui.ui(report_untrusted=False,
26 interactive = False)
26 interactive = False)
27 self.motd = None
27 self.motd = None
28 self.style = None
28 self.style = None
29 self.stripecount = None
29 self.stripecount = None
30 self.repos_sorted = ('name', False)
30 self.repos_sorted = ('name', False)
31 self._baseurl = None
31 if isinstance(config, (list, tuple)):
32 if isinstance(config, (list, tuple)):
32 self.repos = cleannames(config)
33 self.repos = cleannames(config)
33 self.repos_sorted = ('', False)
34 self.repos_sorted = ('', False)
34 elif isinstance(config, dict):
35 elif isinstance(config, dict):
35 self.repos = cleannames(config.items())
36 self.repos = cleannames(config.items())
36 self.repos.sort()
37 self.repos.sort()
37 else:
38 else:
38 if isinstance(config, util.configparser):
39 if isinstance(config, util.configparser):
39 cp = config
40 cp = config
40 else:
41 else:
41 cp = util.configparser()
42 cp = util.configparser()
42 cp.read(config)
43 cp.read(config)
43 self.repos = []
44 self.repos = []
44 if cp.has_section('web'):
45 if cp.has_section('web'):
45 if cp.has_option('web', 'motd'):
46 if cp.has_option('web', 'motd'):
46 self.motd = cp.get('web', 'motd')
47 self.motd = cp.get('web', 'motd')
47 if cp.has_option('web', 'style'):
48 if cp.has_option('web', 'style'):
48 self.style = cp.get('web', 'style')
49 self.style = cp.get('web', 'style')
49 if cp.has_option('web', 'stripes'):
50 if cp.has_option('web', 'stripes'):
50 self.stripecount = int(cp.get('web', 'stripes'))
51 self.stripecount = int(cp.get('web', 'stripes'))
52 if cp.has_option('web', 'baseurl'):
53 self._baseurl = cp.get('web', 'baseurl')
51 if cp.has_section('paths'):
54 if cp.has_section('paths'):
52 self.repos.extend(cleannames(cp.items('paths')))
55 self.repos.extend(cleannames(cp.items('paths')))
53 if cp.has_section('collections'):
56 if cp.has_section('collections'):
54 for prefix, root in cp.items('collections'):
57 for prefix, root in cp.items('collections'):
55 for path in util.walkrepos(root):
58 for path in util.walkrepos(root):
56 repo = os.path.normpath(path)
59 repo = os.path.normpath(path)
57 name = repo
60 name = repo
58 if name.startswith(prefix):
61 if name.startswith(prefix):
59 name = name[len(prefix):]
62 name = name[len(prefix):]
60 self.repos.append((name.lstrip(os.sep), repo))
63 self.repos.append((name.lstrip(os.sep), repo))
61 self.repos.sort()
64 self.repos.sort()
62
65
63 def run(self):
66 def run(self):
64 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
67 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
65 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
68 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
66 import mercurial.hgweb.wsgicgi as wsgicgi
69 import mercurial.hgweb.wsgicgi as wsgicgi
67 wsgicgi.launch(self)
70 wsgicgi.launch(self)
68
71
69 def __call__(self, env, respond):
72 def __call__(self, env, respond):
70 req = wsgirequest(env, respond)
73 req = wsgirequest(env, respond)
71 self.run_wsgi(req)
74 self.run_wsgi(req)
72 return req
75 return req
73
76
74 def run_wsgi(self, req):
77 def run_wsgi(self, req):
75
78
76 try:
79 try:
77 try:
80 try:
78
81
79 virtual = req.env.get("PATH_INFO", "").strip('/')
82 virtual = req.env.get("PATH_INFO", "").strip('/')
80 tmpl = self.templater(req)
83 tmpl = self.templater(req)
81 ctype = tmpl('mimetype', encoding=util._encoding)
84 ctype = tmpl('mimetype', encoding=util._encoding)
82 ctype = templater.stringify(ctype)
85 ctype = templater.stringify(ctype)
83
86
84 # a static file
87 # a static file
85 if virtual.startswith('static/') or 'static' in req.form:
88 if virtual.startswith('static/') or 'static' in req.form:
86 static = os.path.join(templater.templatepath(), 'static')
89 static = os.path.join(templater.templatepath(), 'static')
87 if virtual.startswith('static/'):
90 if virtual.startswith('static/'):
88 fname = virtual[7:]
91 fname = virtual[7:]
89 else:
92 else:
90 fname = req.form['static'][0]
93 fname = req.form['static'][0]
91 req.write(staticfile(static, fname, req))
94 req.write(staticfile(static, fname, req))
92 return
95 return
93
96
94 # top-level index
97 # top-level index
95 elif not virtual:
98 elif not virtual:
96 req.respond(HTTP_OK, ctype)
99 req.respond(HTTP_OK, ctype)
97 req.write(self.makeindex(req, tmpl))
100 req.write(self.makeindex(req, tmpl))
98 return
101 return
99
102
100 # nested indexes and hgwebs
103 # nested indexes and hgwebs
101
104
102 repos = dict(self.repos)
105 repos = dict(self.repos)
103 while virtual:
106 while virtual:
104 real = repos.get(virtual)
107 real = repos.get(virtual)
105 if real:
108 if real:
106 req.env['REPO_NAME'] = virtual
109 req.env['REPO_NAME'] = virtual
107 try:
110 try:
108 repo = hg.repository(self.parentui, real)
111 repo = hg.repository(self.parentui, real)
109 hgweb(repo).run_wsgi(req)
112 hgweb(repo).run_wsgi(req)
110 return
113 return
111 except IOError, inst:
114 except IOError, inst:
112 msg = inst.strerror
115 msg = inst.strerror
113 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
116 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
114 except RepoError, inst:
117 except RepoError, inst:
115 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
118 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
116
119
117 # browse subdirectories
120 # browse subdirectories
118 subdir = virtual + '/'
121 subdir = virtual + '/'
119 if [r for r in repos if r.startswith(subdir)]:
122 if [r for r in repos if r.startswith(subdir)]:
120 req.respond(HTTP_OK, ctype)
123 req.respond(HTTP_OK, ctype)
121 req.write(self.makeindex(req, tmpl, subdir))
124 req.write(self.makeindex(req, tmpl, subdir))
122 return
125 return
123
126
124 up = virtual.rfind('/')
127 up = virtual.rfind('/')
125 if up < 0:
128 if up < 0:
126 break
129 break
127 virtual = virtual[:up]
130 virtual = virtual[:up]
128
131
129 # prefixes not found
132 # prefixes not found
130 req.respond(HTTP_NOT_FOUND, ctype)
133 req.respond(HTTP_NOT_FOUND, ctype)
131 req.write(tmpl("notfound", repo=virtual))
134 req.write(tmpl("notfound", repo=virtual))
132
135
133 except ErrorResponse, err:
136 except ErrorResponse, err:
134 req.respond(err.code, ctype)
137 req.respond(err.code, ctype)
135 req.write(tmpl('error', error=err.message or ''))
138 req.write(tmpl('error', error=err.message or ''))
136 finally:
139 finally:
137 tmpl = None
140 tmpl = None
138
141
139 def makeindex(self, req, tmpl, subdir=""):
142 def makeindex(self, req, tmpl, subdir=""):
140
143
141 def archivelist(ui, nodeid, url):
144 def archivelist(ui, nodeid, url):
142 allowed = ui.configlist("web", "allow_archive", untrusted=True)
145 allowed = ui.configlist("web", "allow_archive", untrusted=True)
143 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
146 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
144 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
147 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
145 untrusted=True):
148 untrusted=True):
146 yield {"type" : i[0], "extension": i[1],
149 yield {"type" : i[0], "extension": i[1],
147 "node": nodeid, "url": url}
150 "node": nodeid, "url": url}
148
151
149 def entries(sortcolumn="", descending=False, subdir="", **map):
152 def entries(sortcolumn="", descending=False, subdir="", **map):
150 def sessionvars(**map):
153 def sessionvars(**map):
151 fields = []
154 fields = []
152 if 'style' in req.form:
155 if 'style' in req.form:
153 style = req.form['style'][0]
156 style = req.form['style'][0]
154 if style != get('web', 'style', ''):
157 if style != get('web', 'style', ''):
155 fields.append(('style', style))
158 fields.append(('style', style))
156
159
157 separator = url[-1] == '?' and ';' or '?'
160 separator = url[-1] == '?' and ';' or '?'
158 for name, value in fields:
161 for name, value in fields:
159 yield dict(name=name, value=value, separator=separator)
162 yield dict(name=name, value=value, separator=separator)
160 separator = ';'
163 separator = ';'
161
164
162 rows = []
165 rows = []
163 parity = paritygen(self.stripecount)
166 parity = paritygen(self.stripecount)
164 for name, path in self.repos:
167 for name, path in self.repos:
165 if not name.startswith(subdir):
168 if not name.startswith(subdir):
166 continue
169 continue
167 name = name[len(subdir):]
170 name = name[len(subdir):]
168
171
169 u = ui.ui(parentui=self.parentui)
172 u = ui.ui(parentui=self.parentui)
170 try:
173 try:
171 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
174 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
172 except Exception, e:
175 except Exception, e:
173 u.warn(_('error reading %s/.hg/hgrc: %s\n' % (path, e)))
176 u.warn(_('error reading %s/.hg/hgrc: %s\n' % (path, e)))
174 continue
177 continue
175 def get(section, name, default=None):
178 def get(section, name, default=None):
176 return u.config(section, name, default, untrusted=True)
179 return u.config(section, name, default, untrusted=True)
177
180
178 if u.configbool("web", "hidden", untrusted=True):
181 if u.configbool("web", "hidden", untrusted=True):
179 continue
182 continue
180
183
181 parts = [req.env['PATH_INFO'].rstrip('/'), name]
184 parts = [req.env['PATH_INFO'].rstrip('/'), name]
182 if req.env['SCRIPT_NAME']:
185 if req.env['SCRIPT_NAME']:
183 parts.insert(0, req.env['SCRIPT_NAME'])
186 parts.insert(0, req.env['SCRIPT_NAME'])
184 url = ('/'.join(parts).replace("//", "/")) + '/'
187 url = ('/'.join(parts).replace("//", "/")) + '/'
185
188
186 # update time with local timezone
189 # update time with local timezone
187 try:
190 try:
188 d = (get_mtime(path), util.makedate()[1])
191 d = (get_mtime(path), util.makedate()[1])
189 except OSError:
192 except OSError:
190 continue
193 continue
191
194
192 contact = get_contact(get)
195 contact = get_contact(get)
193 description = get("web", "description", "")
196 description = get("web", "description", "")
194 name = get("web", "name", name)
197 name = get("web", "name", name)
195 row = dict(contact=contact or "unknown",
198 row = dict(contact=contact or "unknown",
196 contact_sort=contact.upper() or "unknown",
199 contact_sort=contact.upper() or "unknown",
197 name=name,
200 name=name,
198 name_sort=name,
201 name_sort=name,
199 url=url,
202 url=url,
200 description=description or "unknown",
203 description=description or "unknown",
201 description_sort=description.upper() or "unknown",
204 description_sort=description.upper() or "unknown",
202 lastchange=d,
205 lastchange=d,
203 lastchange_sort=d[1]-d[0],
206 lastchange_sort=d[1]-d[0],
204 sessionvars=sessionvars,
207 sessionvars=sessionvars,
205 archives=archivelist(u, "tip", url))
208 archives=archivelist(u, "tip", url))
206 if (not sortcolumn
209 if (not sortcolumn
207 or (sortcolumn, descending) == self.repos_sorted):
210 or (sortcolumn, descending) == self.repos_sorted):
208 # fast path for unsorted output
211 # fast path for unsorted output
209 row['parity'] = parity.next()
212 row['parity'] = parity.next()
210 yield row
213 yield row
211 else:
214 else:
212 rows.append((row["%s_sort" % sortcolumn], row))
215 rows.append((row["%s_sort" % sortcolumn], row))
213 if rows:
216 if rows:
214 rows.sort()
217 rows.sort()
215 if descending:
218 if descending:
216 rows.reverse()
219 rows.reverse()
217 for key, row in rows:
220 for key, row in rows:
218 row['parity'] = parity.next()
221 row['parity'] = parity.next()
219 yield row
222 yield row
220
223
221 sortable = ["name", "description", "contact", "lastchange"]
224 sortable = ["name", "description", "contact", "lastchange"]
222 sortcolumn, descending = self.repos_sorted
225 sortcolumn, descending = self.repos_sorted
223 if 'sort' in req.form:
226 if 'sort' in req.form:
224 sortcolumn = req.form['sort'][0]
227 sortcolumn = req.form['sort'][0]
225 descending = sortcolumn.startswith('-')
228 descending = sortcolumn.startswith('-')
226 if descending:
229 if descending:
227 sortcolumn = sortcolumn[1:]
230 sortcolumn = sortcolumn[1:]
228 if sortcolumn not in sortable:
231 if sortcolumn not in sortable:
229 sortcolumn = ""
232 sortcolumn = ""
230
233
231 sort = [("sort_%s" % column,
234 sort = [("sort_%s" % column,
232 "%s%s" % ((not descending and column == sortcolumn)
235 "%s%s" % ((not descending and column == sortcolumn)
233 and "-" or "", column))
236 and "-" or "", column))
234 for column in sortable]
237 for column in sortable]
235
238
239 if self._baseurl is not None:
240 req.env['SCRIPT_NAME'] = self._baseurl
241
236 return tmpl("index", entries=entries, subdir=subdir,
242 return tmpl("index", entries=entries, subdir=subdir,
237 sortcolumn=sortcolumn, descending=descending,
243 sortcolumn=sortcolumn, descending=descending,
238 **dict(sort))
244 **dict(sort))
239
245
240 def templater(self, req):
246 def templater(self, req):
241
247
242 def header(**map):
248 def header(**map):
243 yield tmpl('header', encoding=util._encoding, **map)
249 yield tmpl('header', encoding=util._encoding, **map)
244
250
245 def footer(**map):
251 def footer(**map):
246 yield tmpl("footer", **map)
252 yield tmpl("footer", **map)
247
253
248 def motd(**map):
254 def motd(**map):
249 if self.motd is not None:
255 if self.motd is not None:
250 yield self.motd
256 yield self.motd
251 else:
257 else:
252 yield config('web', 'motd', '')
258 yield config('web', 'motd', '')
253
259
254 def config(section, name, default=None, untrusted=True):
260 def config(section, name, default=None, untrusted=True):
255 return self.parentui.config(section, name, default, untrusted)
261 return self.parentui.config(section, name, default, untrusted)
256
262
263 if self._baseurl is not None:
264 req.env['SCRIPT_NAME'] = self._baseurl
265
257 url = req.env.get('SCRIPT_NAME', '')
266 url = req.env.get('SCRIPT_NAME', '')
258 if not url.endswith('/'):
267 if not url.endswith('/'):
259 url += '/'
268 url += '/'
260
269
261 staticurl = config('web', 'staticurl') or url + 'static/'
270 staticurl = config('web', 'staticurl') or url + 'static/'
262 if not staticurl.endswith('/'):
271 if not staticurl.endswith('/'):
263 staticurl += '/'
272 staticurl += '/'
264
273
265 style = self.style
274 style = self.style
266 if style is None:
275 if style is None:
267 style = config('web', 'style', '')
276 style = config('web', 'style', '')
268 if 'style' in req.form:
277 if 'style' in req.form:
269 style = req.form['style'][0]
278 style = req.form['style'][0]
270 if self.stripecount is None:
279 if self.stripecount is None:
271 self.stripecount = int(config('web', 'stripes', 1))
280 self.stripecount = int(config('web', 'stripes', 1))
272 mapfile = style_map(templater.templatepath(), style)
281 mapfile = style_map(templater.templatepath(), style)
273 tmpl = templater.templater(mapfile, templatefilters.filters,
282 tmpl = templater.templater(mapfile, templatefilters.filters,
274 defaults={"header": header,
283 defaults={"header": header,
275 "footer": footer,
284 "footer": footer,
276 "motd": motd,
285 "motd": motd,
277 "url": url,
286 "url": url,
278 "staticurl": staticurl})
287 "staticurl": staticurl})
279 return tmpl
288 return tmpl
General Comments 0
You need to be logged in to leave comments. Login now