##// END OF EJS Templates
hgwebdir: use web.prefix when creating url breadcrumbs (issue3790)...
Angel Ezquerra -
r18515:bf8bbbf4 stable
parent child Browse files
Show More
@@ -1,334 +1,336 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
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-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 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 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import os
9 import os
10 from mercurial import ui, hg, hook, error, encoding, templater, util
10 from mercurial import ui, hg, hook, error, encoding, templater, util
11 from common import get_stat, ErrorResponse, permhooks, caching
11 from common import get_stat, ErrorResponse, permhooks, caching
12 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
12 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
13 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
13 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
14 from request import wsgirequest
14 from request import wsgirequest
15 import webcommands, protocol, webutil
15 import webcommands, protocol, webutil
16
16
17 perms = {
17 perms = {
18 'changegroup': 'pull',
18 'changegroup': 'pull',
19 'changegroupsubset': 'pull',
19 'changegroupsubset': 'pull',
20 'getbundle': 'pull',
20 'getbundle': 'pull',
21 'stream_out': 'pull',
21 'stream_out': 'pull',
22 'listkeys': 'pull',
22 'listkeys': 'pull',
23 'unbundle': 'push',
23 'unbundle': 'push',
24 'pushkey': 'push',
24 'pushkey': 'push',
25 }
25 }
26
26
27 def makebreadcrumb(url):
27 def makebreadcrumb(url, prefix=''):
28 '''Return a 'URL breadcrumb' list
28 '''Return a 'URL breadcrumb' list
29
29
30 A 'URL breadcrumb' is a list of URL-name pairs,
30 A 'URL breadcrumb' is a list of URL-name pairs,
31 corresponding to each of the path items on a URL.
31 corresponding to each of the path items on a URL.
32 This can be used to create path navigation entries.
32 This can be used to create path navigation entries.
33 '''
33 '''
34 if url.endswith('/'):
34 if url.endswith('/'):
35 url = url[:-1]
35 url = url[:-1]
36 if prefix:
37 url = '/' + prefix + url
36 relpath = url
38 relpath = url
37 if relpath.startswith('/'):
39 if relpath.startswith('/'):
38 relpath = relpath[1:]
40 relpath = relpath[1:]
39
41
40 breadcrumb = []
42 breadcrumb = []
41 urlel = url
43 urlel = url
42 pathitems = [''] + relpath.split('/')
44 pathitems = [''] + relpath.split('/')
43 for pathel in reversed(pathitems):
45 for pathel in reversed(pathitems):
44 if not pathel or not urlel:
46 if not pathel or not urlel:
45 break
47 break
46 breadcrumb.append({'url': urlel, 'name': pathel})
48 breadcrumb.append({'url': urlel, 'name': pathel})
47 urlel = os.path.dirname(urlel)
49 urlel = os.path.dirname(urlel)
48 return reversed(breadcrumb)
50 return reversed(breadcrumb)
49
51
50
52
51 class hgweb(object):
53 class hgweb(object):
52 def __init__(self, repo, name=None, baseui=None):
54 def __init__(self, repo, name=None, baseui=None):
53 if isinstance(repo, str):
55 if isinstance(repo, str):
54 if baseui:
56 if baseui:
55 u = baseui.copy()
57 u = baseui.copy()
56 else:
58 else:
57 u = ui.ui()
59 u = ui.ui()
58 self.repo = hg.repository(u, repo)
60 self.repo = hg.repository(u, repo)
59 else:
61 else:
60 self.repo = repo
62 self.repo = repo
61
63
62 self.repo = self.repo.filtered('served')
64 self.repo = self.repo.filtered('served')
63 self.repo.ui.setconfig('ui', 'report_untrusted', 'off')
65 self.repo.ui.setconfig('ui', 'report_untrusted', 'off')
64 self.repo.ui.setconfig('ui', 'nontty', 'true')
66 self.repo.ui.setconfig('ui', 'nontty', 'true')
65 hook.redirect(True)
67 hook.redirect(True)
66 self.mtime = -1
68 self.mtime = -1
67 self.size = -1
69 self.size = -1
68 self.reponame = name
70 self.reponame = name
69 self.archives = 'zip', 'gz', 'bz2'
71 self.archives = 'zip', 'gz', 'bz2'
70 self.stripecount = 1
72 self.stripecount = 1
71 # a repo owner may set web.templates in .hg/hgrc to get any file
73 # a repo owner may set web.templates in .hg/hgrc to get any file
72 # readable by the user running the CGI script
74 # readable by the user running the CGI script
73 self.templatepath = self.config('web', 'templates')
75 self.templatepath = self.config('web', 'templates')
74
76
75 # The CGI scripts are often run by a user different from the repo owner.
77 # The CGI scripts are often run by a user different from the repo owner.
76 # Trust the settings from the .hg/hgrc files by default.
78 # Trust the settings from the .hg/hgrc files by default.
77 def config(self, section, name, default=None, untrusted=True):
79 def config(self, section, name, default=None, untrusted=True):
78 return self.repo.ui.config(section, name, default,
80 return self.repo.ui.config(section, name, default,
79 untrusted=untrusted)
81 untrusted=untrusted)
80
82
81 def configbool(self, section, name, default=False, untrusted=True):
83 def configbool(self, section, name, default=False, untrusted=True):
82 return self.repo.ui.configbool(section, name, default,
84 return self.repo.ui.configbool(section, name, default,
83 untrusted=untrusted)
85 untrusted=untrusted)
84
86
85 def configlist(self, section, name, default=None, untrusted=True):
87 def configlist(self, section, name, default=None, untrusted=True):
86 return self.repo.ui.configlist(section, name, default,
88 return self.repo.ui.configlist(section, name, default,
87 untrusted=untrusted)
89 untrusted=untrusted)
88
90
89 def refresh(self, request=None):
91 def refresh(self, request=None):
90 if request:
92 if request:
91 self.repo.ui.environ = request.env
93 self.repo.ui.environ = request.env
92 st = get_stat(self.repo.spath)
94 st = get_stat(self.repo.spath)
93 # compare changelog size in addition to mtime to catch
95 # compare changelog size in addition to mtime to catch
94 # rollbacks made less than a second ago
96 # rollbacks made less than a second ago
95 if st.st_mtime != self.mtime or st.st_size != self.size:
97 if st.st_mtime != self.mtime or st.st_size != self.size:
96 self.mtime = st.st_mtime
98 self.mtime = st.st_mtime
97 self.size = st.st_size
99 self.size = st.st_size
98 self.repo = hg.repository(self.repo.ui, self.repo.root)
100 self.repo = hg.repository(self.repo.ui, self.repo.root)
99 self.repo = self.repo.filtered('served')
101 self.repo = self.repo.filtered('served')
100 self.maxchanges = int(self.config("web", "maxchanges", 10))
102 self.maxchanges = int(self.config("web", "maxchanges", 10))
101 self.stripecount = int(self.config("web", "stripes", 1))
103 self.stripecount = int(self.config("web", "stripes", 1))
102 self.maxshortchanges = int(self.config("web", "maxshortchanges",
104 self.maxshortchanges = int(self.config("web", "maxshortchanges",
103 60))
105 60))
104 self.maxfiles = int(self.config("web", "maxfiles", 10))
106 self.maxfiles = int(self.config("web", "maxfiles", 10))
105 self.allowpull = self.configbool("web", "allowpull", True)
107 self.allowpull = self.configbool("web", "allowpull", True)
106 encoding.encoding = self.config("web", "encoding",
108 encoding.encoding = self.config("web", "encoding",
107 encoding.encoding)
109 encoding.encoding)
108
110
109 def run(self):
111 def run(self):
110 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
112 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
111 raise RuntimeError("This function is only intended to be "
113 raise RuntimeError("This function is only intended to be "
112 "called while running as a CGI script.")
114 "called while running as a CGI script.")
113 import mercurial.hgweb.wsgicgi as wsgicgi
115 import mercurial.hgweb.wsgicgi as wsgicgi
114 wsgicgi.launch(self)
116 wsgicgi.launch(self)
115
117
116 def __call__(self, env, respond):
118 def __call__(self, env, respond):
117 req = wsgirequest(env, respond)
119 req = wsgirequest(env, respond)
118 return self.run_wsgi(req)
120 return self.run_wsgi(req)
119
121
120 def run_wsgi(self, req):
122 def run_wsgi(self, req):
121
123
122 self.refresh(req)
124 self.refresh(req)
123
125
124 # work with CGI variables to create coherent structure
126 # work with CGI variables to create coherent structure
125 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
127 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
126
128
127 req.url = req.env['SCRIPT_NAME']
129 req.url = req.env['SCRIPT_NAME']
128 if not req.url.endswith('/'):
130 if not req.url.endswith('/'):
129 req.url += '/'
131 req.url += '/'
130 if 'REPO_NAME' in req.env:
132 if 'REPO_NAME' in req.env:
131 req.url += req.env['REPO_NAME'] + '/'
133 req.url += req.env['REPO_NAME'] + '/'
132
134
133 if 'PATH_INFO' in req.env:
135 if 'PATH_INFO' in req.env:
134 parts = req.env['PATH_INFO'].strip('/').split('/')
136 parts = req.env['PATH_INFO'].strip('/').split('/')
135 repo_parts = req.env.get('REPO_NAME', '').split('/')
137 repo_parts = req.env.get('REPO_NAME', '').split('/')
136 if parts[:len(repo_parts)] == repo_parts:
138 if parts[:len(repo_parts)] == repo_parts:
137 parts = parts[len(repo_parts):]
139 parts = parts[len(repo_parts):]
138 query = '/'.join(parts)
140 query = '/'.join(parts)
139 else:
141 else:
140 query = req.env['QUERY_STRING'].split('&', 1)[0]
142 query = req.env['QUERY_STRING'].split('&', 1)[0]
141 query = query.split(';', 1)[0]
143 query = query.split(';', 1)[0]
142
144
143 # process this if it's a protocol request
145 # process this if it's a protocol request
144 # protocol bits don't need to create any URLs
146 # protocol bits don't need to create any URLs
145 # and the clients always use the old URL structure
147 # and the clients always use the old URL structure
146
148
147 cmd = req.form.get('cmd', [''])[0]
149 cmd = req.form.get('cmd', [''])[0]
148 if protocol.iscmd(cmd):
150 if protocol.iscmd(cmd):
149 try:
151 try:
150 if query:
152 if query:
151 raise ErrorResponse(HTTP_NOT_FOUND)
153 raise ErrorResponse(HTTP_NOT_FOUND)
152 if cmd in perms:
154 if cmd in perms:
153 self.check_perm(req, perms[cmd])
155 self.check_perm(req, perms[cmd])
154 return protocol.call(self.repo, req, cmd)
156 return protocol.call(self.repo, req, cmd)
155 except ErrorResponse, inst:
157 except ErrorResponse, inst:
156 # A client that sends unbundle without 100-continue will
158 # A client that sends unbundle without 100-continue will
157 # break if we respond early.
159 # break if we respond early.
158 if (cmd == 'unbundle' and
160 if (cmd == 'unbundle' and
159 (req.env.get('HTTP_EXPECT',
161 (req.env.get('HTTP_EXPECT',
160 '').lower() != '100-continue') or
162 '').lower() != '100-continue') or
161 req.env.get('X-HgHttp2', '')):
163 req.env.get('X-HgHttp2', '')):
162 req.drain()
164 req.drain()
163 req.respond(inst, protocol.HGTYPE,
165 req.respond(inst, protocol.HGTYPE,
164 body='0\n%s\n' % inst.message)
166 body='0\n%s\n' % inst.message)
165 return ''
167 return ''
166
168
167 # translate user-visible url structure to internal structure
169 # translate user-visible url structure to internal structure
168
170
169 args = query.split('/', 2)
171 args = query.split('/', 2)
170 if 'cmd' not in req.form and args and args[0]:
172 if 'cmd' not in req.form and args and args[0]:
171
173
172 cmd = args.pop(0)
174 cmd = args.pop(0)
173 style = cmd.rfind('-')
175 style = cmd.rfind('-')
174 if style != -1:
176 if style != -1:
175 req.form['style'] = [cmd[:style]]
177 req.form['style'] = [cmd[:style]]
176 cmd = cmd[style + 1:]
178 cmd = cmd[style + 1:]
177
179
178 # avoid accepting e.g. style parameter as command
180 # avoid accepting e.g. style parameter as command
179 if util.safehasattr(webcommands, cmd):
181 if util.safehasattr(webcommands, cmd):
180 req.form['cmd'] = [cmd]
182 req.form['cmd'] = [cmd]
181 else:
183 else:
182 cmd = ''
184 cmd = ''
183
185
184 if cmd == 'static':
186 if cmd == 'static':
185 req.form['file'] = ['/'.join(args)]
187 req.form['file'] = ['/'.join(args)]
186 else:
188 else:
187 if args and args[0]:
189 if args and args[0]:
188 node = args.pop(0)
190 node = args.pop(0)
189 req.form['node'] = [node]
191 req.form['node'] = [node]
190 if args:
192 if args:
191 req.form['file'] = args
193 req.form['file'] = args
192
194
193 ua = req.env.get('HTTP_USER_AGENT', '')
195 ua = req.env.get('HTTP_USER_AGENT', '')
194 if cmd == 'rev' and 'mercurial' in ua:
196 if cmd == 'rev' and 'mercurial' in ua:
195 req.form['style'] = ['raw']
197 req.form['style'] = ['raw']
196
198
197 if cmd == 'archive':
199 if cmd == 'archive':
198 fn = req.form['node'][0]
200 fn = req.form['node'][0]
199 for type_, spec in self.archive_specs.iteritems():
201 for type_, spec in self.archive_specs.iteritems():
200 ext = spec[2]
202 ext = spec[2]
201 if fn.endswith(ext):
203 if fn.endswith(ext):
202 req.form['node'] = [fn[:-len(ext)]]
204 req.form['node'] = [fn[:-len(ext)]]
203 req.form['type'] = [type_]
205 req.form['type'] = [type_]
204
206
205 # process the web interface request
207 # process the web interface request
206
208
207 try:
209 try:
208 tmpl = self.templater(req)
210 tmpl = self.templater(req)
209 ctype = tmpl('mimetype', encoding=encoding.encoding)
211 ctype = tmpl('mimetype', encoding=encoding.encoding)
210 ctype = templater.stringify(ctype)
212 ctype = templater.stringify(ctype)
211
213
212 # check read permissions non-static content
214 # check read permissions non-static content
213 if cmd != 'static':
215 if cmd != 'static':
214 self.check_perm(req, None)
216 self.check_perm(req, None)
215
217
216 if cmd == '':
218 if cmd == '':
217 req.form['cmd'] = [tmpl.cache['default']]
219 req.form['cmd'] = [tmpl.cache['default']]
218 cmd = req.form['cmd'][0]
220 cmd = req.form['cmd'][0]
219
221
220 if self.configbool('web', 'cache', True):
222 if self.configbool('web', 'cache', True):
221 caching(self, req) # sets ETag header or raises NOT_MODIFIED
223 caching(self, req) # sets ETag header or raises NOT_MODIFIED
222 if cmd not in webcommands.__all__:
224 if cmd not in webcommands.__all__:
223 msg = 'no such method: %s' % cmd
225 msg = 'no such method: %s' % cmd
224 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
226 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
225 elif cmd == 'file' and 'raw' in req.form.get('style', []):
227 elif cmd == 'file' and 'raw' in req.form.get('style', []):
226 self.ctype = ctype
228 self.ctype = ctype
227 content = webcommands.rawfile(self, req, tmpl)
229 content = webcommands.rawfile(self, req, tmpl)
228 else:
230 else:
229 content = getattr(webcommands, cmd)(self, req, tmpl)
231 content = getattr(webcommands, cmd)(self, req, tmpl)
230 req.respond(HTTP_OK, ctype)
232 req.respond(HTTP_OK, ctype)
231
233
232 return content
234 return content
233
235
234 except error.LookupError, err:
236 except error.LookupError, err:
235 req.respond(HTTP_NOT_FOUND, ctype)
237 req.respond(HTTP_NOT_FOUND, ctype)
236 msg = str(err)
238 msg = str(err)
237 if 'manifest' not in msg:
239 if 'manifest' not in msg:
238 msg = 'revision not found: %s' % err.name
240 msg = 'revision not found: %s' % err.name
239 return tmpl('error', error=msg)
241 return tmpl('error', error=msg)
240 except (error.RepoError, error.RevlogError), inst:
242 except (error.RepoError, error.RevlogError), inst:
241 req.respond(HTTP_SERVER_ERROR, ctype)
243 req.respond(HTTP_SERVER_ERROR, ctype)
242 return tmpl('error', error=str(inst))
244 return tmpl('error', error=str(inst))
243 except ErrorResponse, inst:
245 except ErrorResponse, inst:
244 req.respond(inst, ctype)
246 req.respond(inst, ctype)
245 if inst.code == HTTP_NOT_MODIFIED:
247 if inst.code == HTTP_NOT_MODIFIED:
246 # Not allowed to return a body on a 304
248 # Not allowed to return a body on a 304
247 return ['']
249 return ['']
248 return tmpl('error', error=inst.message)
250 return tmpl('error', error=inst.message)
249
251
250 def templater(self, req):
252 def templater(self, req):
251
253
252 # determine scheme, port and server name
254 # determine scheme, port and server name
253 # this is needed to create absolute urls
255 # this is needed to create absolute urls
254
256
255 proto = req.env.get('wsgi.url_scheme')
257 proto = req.env.get('wsgi.url_scheme')
256 if proto == 'https':
258 if proto == 'https':
257 proto = 'https'
259 proto = 'https'
258 default_port = "443"
260 default_port = "443"
259 else:
261 else:
260 proto = 'http'
262 proto = 'http'
261 default_port = "80"
263 default_port = "80"
262
264
263 port = req.env["SERVER_PORT"]
265 port = req.env["SERVER_PORT"]
264 port = port != default_port and (":" + port) or ""
266 port = port != default_port and (":" + port) or ""
265 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
267 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
266 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
268 logourl = self.config("web", "logourl", "http://mercurial.selenic.com/")
267 logoimg = self.config("web", "logoimg", "hglogo.png")
269 logoimg = self.config("web", "logoimg", "hglogo.png")
268 staticurl = self.config("web", "staticurl") or req.url + 'static/'
270 staticurl = self.config("web", "staticurl") or req.url + 'static/'
269 if not staticurl.endswith('/'):
271 if not staticurl.endswith('/'):
270 staticurl += '/'
272 staticurl += '/'
271
273
272 # some functions for the templater
274 # some functions for the templater
273
275
274 def header(**map):
276 def header(**map):
275 yield tmpl('header', encoding=encoding.encoding, **map)
277 yield tmpl('header', encoding=encoding.encoding, **map)
276
278
277 def footer(**map):
279 def footer(**map):
278 yield tmpl("footer", **map)
280 yield tmpl("footer", **map)
279
281
280 def motd(**map):
282 def motd(**map):
281 yield self.config("web", "motd", "")
283 yield self.config("web", "motd", "")
282
284
283 # figure out which style to use
285 # figure out which style to use
284
286
285 vars = {}
287 vars = {}
286 styles = (
288 styles = (
287 req.form.get('style', [None])[0],
289 req.form.get('style', [None])[0],
288 self.config('web', 'style'),
290 self.config('web', 'style'),
289 'paper',
291 'paper',
290 )
292 )
291 style, mapfile = templater.stylemap(styles, self.templatepath)
293 style, mapfile = templater.stylemap(styles, self.templatepath)
292 if style == styles[0]:
294 if style == styles[0]:
293 vars['style'] = style
295 vars['style'] = style
294
296
295 start = req.url[-1] == '?' and '&' or '?'
297 start = req.url[-1] == '?' and '&' or '?'
296 sessionvars = webutil.sessionvars(vars, start)
298 sessionvars = webutil.sessionvars(vars, start)
297
299
298 if not self.reponame:
300 if not self.reponame:
299 self.reponame = (self.config("web", "name")
301 self.reponame = (self.config("web", "name")
300 or req.env.get('REPO_NAME')
302 or req.env.get('REPO_NAME')
301 or req.url.strip('/') or self.repo.root)
303 or req.url.strip('/') or self.repo.root)
302
304
303 # create the templater
305 # create the templater
304
306
305 tmpl = templater.templater(mapfile,
307 tmpl = templater.templater(mapfile,
306 defaults={"url": req.url,
308 defaults={"url": req.url,
307 "logourl": logourl,
309 "logourl": logourl,
308 "logoimg": logoimg,
310 "logoimg": logoimg,
309 "staticurl": staticurl,
311 "staticurl": staticurl,
310 "urlbase": urlbase,
312 "urlbase": urlbase,
311 "repo": self.reponame,
313 "repo": self.reponame,
312 "header": header,
314 "header": header,
313 "footer": footer,
315 "footer": footer,
314 "motd": motd,
316 "motd": motd,
315 "sessionvars": sessionvars,
317 "sessionvars": sessionvars,
316 "pathdef": makebreadcrumb(req.url),
318 "pathdef": makebreadcrumb(req.url),
317 })
319 })
318 return tmpl
320 return tmpl
319
321
320 def archivelist(self, nodeid):
322 def archivelist(self, nodeid):
321 allowed = self.configlist("web", "allow_archive")
323 allowed = self.configlist("web", "allow_archive")
322 for i, spec in self.archive_specs.iteritems():
324 for i, spec in self.archive_specs.iteritems():
323 if i in allowed or self.configbool("web", "allow" + i):
325 if i in allowed or self.configbool("web", "allow" + i):
324 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
326 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
325
327
326 archive_specs = {
328 archive_specs = {
327 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
329 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
328 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
330 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
329 'zip': ('application/zip', 'zip', '.zip', None),
331 'zip': ('application/zip', 'zip', '.zip', None),
330 }
332 }
331
333
332 def check_perm(self, req, op):
334 def check_perm(self, req, op):
333 for hook in permhooks:
335 for hook in permhooks:
334 hook(self, req, op)
336 hook(self, req, op)
@@ -1,459 +1,465 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 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import os, re, time
9 import os, re, time
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11 from mercurial import ui, hg, scmutil, util, templater
11 from mercurial import ui, hg, scmutil, 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, makebreadcrumb
15 from hgweb_mod import hgweb, makebreadcrumb
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):
22 def findrepos(paths):
23 repos = []
23 repos = []
24 for prefix, root in cleannames(paths):
24 for prefix, root in cleannames(paths):
25 roothead, roottail = os.path.split(root)
25 roothead, roottail = os.path.split(root)
26 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
26 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
27 # /bar/ be served as as foo/N .
27 # /bar/ be served as as foo/N .
28 # '*' will not search inside dirs with .hg (except .hg/patches),
28 # '*' will not search inside dirs with .hg (except .hg/patches),
29 # '**' will search inside dirs with .hg (and thus also find subrepos).
29 # '**' will search inside dirs with .hg (and thus also find subrepos).
30 try:
30 try:
31 recurse = {'*': False, '**': True}[roottail]
31 recurse = {'*': False, '**': True}[roottail]
32 except KeyError:
32 except KeyError:
33 repos.append((prefix, root))
33 repos.append((prefix, root))
34 continue
34 continue
35 roothead = os.path.normpath(os.path.abspath(roothead))
35 roothead = os.path.normpath(os.path.abspath(roothead))
36 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
36 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
37 repos.extend(urlrepos(prefix, roothead, paths))
37 repos.extend(urlrepos(prefix, roothead, paths))
38 return repos
38 return repos
39
39
40 def urlrepos(prefix, roothead, paths):
40 def urlrepos(prefix, roothead, paths):
41 """yield url paths and filesystem paths from a list of repo paths
41 """yield url paths and filesystem paths from a list of repo paths
42
42
43 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
43 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
44 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
44 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
45 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
45 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
46 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
46 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
47 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
47 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
48 """
48 """
49 for path in paths:
49 for path in paths:
50 path = os.path.normpath(path)
50 path = os.path.normpath(path)
51 yield (prefix + '/' +
51 yield (prefix + '/' +
52 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
52 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
53
53
54 def geturlcgivars(baseurl, port):
54 def geturlcgivars(baseurl, port):
55 """
55 """
56 Extract CGI variables from baseurl
56 Extract CGI variables from baseurl
57
57
58 >>> geturlcgivars("http://host.org/base", "80")
58 >>> geturlcgivars("http://host.org/base", "80")
59 ('host.org', '80', '/base')
59 ('host.org', '80', '/base')
60 >>> geturlcgivars("http://host.org:8000/base", "80")
60 >>> geturlcgivars("http://host.org:8000/base", "80")
61 ('host.org', '8000', '/base')
61 ('host.org', '8000', '/base')
62 >>> geturlcgivars('/base', 8000)
62 >>> geturlcgivars('/base', 8000)
63 ('', '8000', '/base')
63 ('', '8000', '/base')
64 >>> geturlcgivars("base", '8000')
64 >>> geturlcgivars("base", '8000')
65 ('', '8000', '/base')
65 ('', '8000', '/base')
66 >>> geturlcgivars("http://host", '8000')
66 >>> geturlcgivars("http://host", '8000')
67 ('host', '8000', '/')
67 ('host', '8000', '/')
68 >>> geturlcgivars("http://host/", '8000')
68 >>> geturlcgivars("http://host/", '8000')
69 ('host', '8000', '/')
69 ('host', '8000', '/')
70 """
70 """
71 u = util.url(baseurl)
71 u = util.url(baseurl)
72 name = u.host or ''
72 name = u.host or ''
73 if u.port:
73 if u.port:
74 port = u.port
74 port = u.port
75 path = u.path or ""
75 path = u.path or ""
76 if not path.startswith('/'):
76 if not path.startswith('/'):
77 path = '/' + path
77 path = '/' + path
78
78
79 return name, str(port), path
79 return name, str(port), path
80
80
81 class hgwebdir(object):
81 class hgwebdir(object):
82 refreshinterval = 20
82 refreshinterval = 20
83
83
84 def __init__(self, conf, baseui=None):
84 def __init__(self, conf, baseui=None):
85 self.conf = conf
85 self.conf = conf
86 self.baseui = baseui
86 self.baseui = baseui
87 self.lastrefresh = 0
87 self.lastrefresh = 0
88 self.motd = None
88 self.motd = None
89 self.refresh()
89 self.refresh()
90
90
91 def refresh(self):
91 def refresh(self):
92 if self.lastrefresh + self.refreshinterval > time.time():
92 if self.lastrefresh + self.refreshinterval > time.time():
93 return
93 return
94
94
95 if self.baseui:
95 if self.baseui:
96 u = self.baseui.copy()
96 u = self.baseui.copy()
97 else:
97 else:
98 u = ui.ui()
98 u = ui.ui()
99 u.setconfig('ui', 'report_untrusted', 'off')
99 u.setconfig('ui', 'report_untrusted', 'off')
100 u.setconfig('ui', 'nontty', 'true')
100 u.setconfig('ui', 'nontty', 'true')
101
101
102 if not isinstance(self.conf, (dict, list, tuple)):
102 if not isinstance(self.conf, (dict, list, tuple)):
103 map = {'paths': 'hgweb-paths'}
103 map = {'paths': 'hgweb-paths'}
104 if not os.path.exists(self.conf):
104 if not os.path.exists(self.conf):
105 raise util.Abort(_('config file %s not found!') % self.conf)
105 raise util.Abort(_('config file %s not found!') % self.conf)
106 u.readconfig(self.conf, remap=map, trust=True)
106 u.readconfig(self.conf, remap=map, trust=True)
107 paths = []
107 paths = []
108 for name, ignored in u.configitems('hgweb-paths'):
108 for name, ignored in u.configitems('hgweb-paths'):
109 for path in u.configlist('hgweb-paths', name):
109 for path in u.configlist('hgweb-paths', name):
110 paths.append((name, path))
110 paths.append((name, path))
111 elif isinstance(self.conf, (list, tuple)):
111 elif isinstance(self.conf, (list, tuple)):
112 paths = self.conf
112 paths = self.conf
113 elif isinstance(self.conf, dict):
113 elif isinstance(self.conf, dict):
114 paths = self.conf.items()
114 paths = self.conf.items()
115
115
116 repos = findrepos(paths)
116 repos = findrepos(paths)
117 for prefix, root in u.configitems('collections'):
117 for prefix, root in u.configitems('collections'):
118 prefix = util.pconvert(prefix)
118 prefix = util.pconvert(prefix)
119 for path in scmutil.walkrepos(root, followsym=True):
119 for path in scmutil.walkrepos(root, followsym=True):
120 repo = os.path.normpath(path)
120 repo = os.path.normpath(path)
121 name = util.pconvert(repo)
121 name = util.pconvert(repo)
122 if name.startswith(prefix):
122 if name.startswith(prefix):
123 name = name[len(prefix):]
123 name = name[len(prefix):]
124 repos.append((name.lstrip('/'), repo))
124 repos.append((name.lstrip('/'), repo))
125
125
126 self.repos = repos
126 self.repos = repos
127 self.ui = u
127 self.ui = u
128 encoding.encoding = self.ui.config('web', 'encoding',
128 encoding.encoding = self.ui.config('web', 'encoding',
129 encoding.encoding)
129 encoding.encoding)
130 self.style = self.ui.config('web', 'style', 'paper')
130 self.style = self.ui.config('web', 'style', 'paper')
131 self.templatepath = self.ui.config('web', 'templates', None)
131 self.templatepath = self.ui.config('web', 'templates', None)
132 self.stripecount = self.ui.config('web', 'stripes', 1)
132 self.stripecount = self.ui.config('web', 'stripes', 1)
133 if self.stripecount:
133 if self.stripecount:
134 self.stripecount = int(self.stripecount)
134 self.stripecount = int(self.stripecount)
135 self._baseurl = self.ui.config('web', 'baseurl')
135 self._baseurl = self.ui.config('web', 'baseurl')
136 prefix = self.ui.config('web', 'prefix', '')
137 if prefix.startswith('/'):
138 prefix = prefix[1:]
139 if prefix.endswith('/'):
140 prefix = prefix[:-1]
141 self.prefix = prefix
136 self.lastrefresh = time.time()
142 self.lastrefresh = time.time()
137
143
138 def run(self):
144 def run(self):
139 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
145 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
140 raise RuntimeError("This function is only intended to be "
146 raise RuntimeError("This function is only intended to be "
141 "called while running as a CGI script.")
147 "called while running as a CGI script.")
142 import mercurial.hgweb.wsgicgi as wsgicgi
148 import mercurial.hgweb.wsgicgi as wsgicgi
143 wsgicgi.launch(self)
149 wsgicgi.launch(self)
144
150
145 def __call__(self, env, respond):
151 def __call__(self, env, respond):
146 req = wsgirequest(env, respond)
152 req = wsgirequest(env, respond)
147 return self.run_wsgi(req)
153 return self.run_wsgi(req)
148
154
149 def read_allowed(self, ui, req):
155 def read_allowed(self, ui, req):
150 """Check allow_read and deny_read config options of a repo's ui object
156 """Check allow_read and deny_read config options of a repo's ui object
151 to determine user permissions. By default, with neither option set (or
157 to determine user permissions. By default, with neither option set (or
152 both empty), allow all users to read the repo. There are two ways a
158 both empty), allow all users to read the repo. There are two ways a
153 user can be denied read access: (1) deny_read is not empty, and the
159 user can be denied read access: (1) deny_read is not empty, and the
154 user is unauthenticated or deny_read contains user (or *), and (2)
160 user is unauthenticated or deny_read contains user (or *), and (2)
155 allow_read is not empty and the user is not in allow_read. Return True
161 allow_read is not empty and the user is not in allow_read. Return True
156 if user is allowed to read the repo, else return False."""
162 if user is allowed to read the repo, else return False."""
157
163
158 user = req.env.get('REMOTE_USER')
164 user = req.env.get('REMOTE_USER')
159
165
160 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
166 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
161 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
167 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
162 return False
168 return False
163
169
164 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
170 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
165 # by default, allow reading if no allow_read option has been set
171 # by default, allow reading if no allow_read option has been set
166 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
172 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
167 return True
173 return True
168
174
169 return False
175 return False
170
176
171 def run_wsgi(self, req):
177 def run_wsgi(self, req):
172 try:
178 try:
173 try:
179 try:
174 self.refresh()
180 self.refresh()
175
181
176 virtual = req.env.get("PATH_INFO", "").strip('/')
182 virtual = req.env.get("PATH_INFO", "").strip('/')
177 tmpl = self.templater(req)
183 tmpl = self.templater(req)
178 ctype = tmpl('mimetype', encoding=encoding.encoding)
184 ctype = tmpl('mimetype', encoding=encoding.encoding)
179 ctype = templater.stringify(ctype)
185 ctype = templater.stringify(ctype)
180
186
181 # a static file
187 # a static file
182 if virtual.startswith('static/') or 'static' in req.form:
188 if virtual.startswith('static/') or 'static' in req.form:
183 if virtual.startswith('static/'):
189 if virtual.startswith('static/'):
184 fname = virtual[7:]
190 fname = virtual[7:]
185 else:
191 else:
186 fname = req.form['static'][0]
192 fname = req.form['static'][0]
187 static = self.ui.config("web", "static", None,
193 static = self.ui.config("web", "static", None,
188 untrusted=False)
194 untrusted=False)
189 if not static:
195 if not static:
190 tp = self.templatepath or templater.templatepath()
196 tp = self.templatepath or templater.templatepath()
191 if isinstance(tp, str):
197 if isinstance(tp, str):
192 tp = [tp]
198 tp = [tp]
193 static = [os.path.join(p, 'static') for p in tp]
199 static = [os.path.join(p, 'static') for p in tp]
194 return (staticfile(static, fname, req),)
200 return (staticfile(static, fname, req),)
195
201
196 # top-level index
202 # top-level index
197 elif not virtual:
203 elif not virtual:
198 req.respond(HTTP_OK, ctype)
204 req.respond(HTTP_OK, ctype)
199 return self.makeindex(req, tmpl)
205 return self.makeindex(req, tmpl)
200
206
201 # nested indexes and hgwebs
207 # nested indexes and hgwebs
202
208
203 repos = dict(self.repos)
209 repos = dict(self.repos)
204 virtualrepo = virtual
210 virtualrepo = virtual
205 while virtualrepo:
211 while virtualrepo:
206 real = repos.get(virtualrepo)
212 real = repos.get(virtualrepo)
207 if real:
213 if real:
208 req.env['REPO_NAME'] = virtualrepo
214 req.env['REPO_NAME'] = virtualrepo
209 try:
215 try:
210 repo = hg.repository(self.ui, real)
216 repo = hg.repository(self.ui, real)
211 return hgweb(repo).run_wsgi(req)
217 return hgweb(repo).run_wsgi(req)
212 except IOError, inst:
218 except IOError, inst:
213 msg = inst.strerror
219 msg = inst.strerror
214 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
220 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
215 except error.RepoError, inst:
221 except error.RepoError, inst:
216 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
222 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
217
223
218 up = virtualrepo.rfind('/')
224 up = virtualrepo.rfind('/')
219 if up < 0:
225 if up < 0:
220 break
226 break
221 virtualrepo = virtualrepo[:up]
227 virtualrepo = virtualrepo[:up]
222
228
223 # browse subdirectories
229 # browse subdirectories
224 subdir = virtual + '/'
230 subdir = virtual + '/'
225 if [r for r in repos if r.startswith(subdir)]:
231 if [r for r in repos if r.startswith(subdir)]:
226 req.respond(HTTP_OK, ctype)
232 req.respond(HTTP_OK, ctype)
227 return self.makeindex(req, tmpl, subdir)
233 return self.makeindex(req, tmpl, subdir)
228
234
229 # prefixes not found
235 # prefixes not found
230 req.respond(HTTP_NOT_FOUND, ctype)
236 req.respond(HTTP_NOT_FOUND, ctype)
231 return tmpl("notfound", repo=virtual)
237 return tmpl("notfound", repo=virtual)
232
238
233 except ErrorResponse, err:
239 except ErrorResponse, err:
234 req.respond(err, ctype)
240 req.respond(err, ctype)
235 return tmpl('error', error=err.message or '')
241 return tmpl('error', error=err.message or '')
236 finally:
242 finally:
237 tmpl = None
243 tmpl = None
238
244
239 def makeindex(self, req, tmpl, subdir=""):
245 def makeindex(self, req, tmpl, subdir=""):
240
246
241 def archivelist(ui, nodeid, url):
247 def archivelist(ui, nodeid, url):
242 allowed = ui.configlist("web", "allow_archive", untrusted=True)
248 allowed = ui.configlist("web", "allow_archive", untrusted=True)
243 archives = []
249 archives = []
244 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
250 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
245 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
251 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
246 untrusted=True):
252 untrusted=True):
247 archives.append({"type" : i[0], "extension": i[1],
253 archives.append({"type" : i[0], "extension": i[1],
248 "node": nodeid, "url": url})
254 "node": nodeid, "url": url})
249 return archives
255 return archives
250
256
251 def rawentries(subdir="", **map):
257 def rawentries(subdir="", **map):
252
258
253 descend = self.ui.configbool('web', 'descend', True)
259 descend = self.ui.configbool('web', 'descend', True)
254 collapse = self.ui.configbool('web', 'collapse', False)
260 collapse = self.ui.configbool('web', 'collapse', False)
255 seenrepos = set()
261 seenrepos = set()
256 seendirs = set()
262 seendirs = set()
257 for name, path in self.repos:
263 for name, path in self.repos:
258
264
259 if not name.startswith(subdir):
265 if not name.startswith(subdir):
260 continue
266 continue
261 name = name[len(subdir):]
267 name = name[len(subdir):]
262 directory = False
268 directory = False
263
269
264 if '/' in name:
270 if '/' in name:
265 if not descend:
271 if not descend:
266 continue
272 continue
267
273
268 nameparts = name.split('/')
274 nameparts = name.split('/')
269 rootname = nameparts[0]
275 rootname = nameparts[0]
270
276
271 if not collapse:
277 if not collapse:
272 pass
278 pass
273 elif rootname in seendirs:
279 elif rootname in seendirs:
274 continue
280 continue
275 elif rootname in seenrepos:
281 elif rootname in seenrepos:
276 pass
282 pass
277 else:
283 else:
278 directory = True
284 directory = True
279 name = rootname
285 name = rootname
280
286
281 # redefine the path to refer to the directory
287 # redefine the path to refer to the directory
282 discarded = '/'.join(nameparts[1:])
288 discarded = '/'.join(nameparts[1:])
283
289
284 # remove name parts plus accompanying slash
290 # remove name parts plus accompanying slash
285 path = path[:-len(discarded) - 1]
291 path = path[:-len(discarded) - 1]
286
292
287 parts = [name]
293 parts = [name]
288 if 'PATH_INFO' in req.env:
294 if 'PATH_INFO' in req.env:
289 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
295 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
290 if req.env['SCRIPT_NAME']:
296 if req.env['SCRIPT_NAME']:
291 parts.insert(0, req.env['SCRIPT_NAME'])
297 parts.insert(0, req.env['SCRIPT_NAME'])
292 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
298 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
293
299
294 # show either a directory entry or a repository
300 # show either a directory entry or a repository
295 if directory:
301 if directory:
296 # get the directory's time information
302 # get the directory's time information
297 try:
303 try:
298 d = (get_mtime(path), util.makedate()[1])
304 d = (get_mtime(path), util.makedate()[1])
299 except OSError:
305 except OSError:
300 continue
306 continue
301
307
302 # add '/' to the name to make it obvious that
308 # add '/' to the name to make it obvious that
303 # the entry is a directory, not a regular repository
309 # the entry is a directory, not a regular repository
304 row = dict(contact="",
310 row = dict(contact="",
305 contact_sort="",
311 contact_sort="",
306 name=name + '/',
312 name=name + '/',
307 name_sort=name,
313 name_sort=name,
308 url=url,
314 url=url,
309 description="",
315 description="",
310 description_sort="",
316 description_sort="",
311 lastchange=d,
317 lastchange=d,
312 lastchange_sort=d[1]-d[0],
318 lastchange_sort=d[1]-d[0],
313 archives=[],
319 archives=[],
314 isdirectory=True)
320 isdirectory=True)
315
321
316 seendirs.add(name)
322 seendirs.add(name)
317 yield row
323 yield row
318 continue
324 continue
319
325
320 u = self.ui.copy()
326 u = self.ui.copy()
321 try:
327 try:
322 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
328 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
323 except Exception, e:
329 except Exception, e:
324 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
330 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
325 continue
331 continue
326 def get(section, name, default=None):
332 def get(section, name, default=None):
327 return u.config(section, name, default, untrusted=True)
333 return u.config(section, name, default, untrusted=True)
328
334
329 if u.configbool("web", "hidden", untrusted=True):
335 if u.configbool("web", "hidden", untrusted=True):
330 continue
336 continue
331
337
332 if not self.read_allowed(u, req):
338 if not self.read_allowed(u, req):
333 continue
339 continue
334
340
335 # update time with local timezone
341 # update time with local timezone
336 try:
342 try:
337 r = hg.repository(self.ui, path)
343 r = hg.repository(self.ui, path)
338 except IOError:
344 except IOError:
339 u.warn(_('error accessing repository at %s\n') % path)
345 u.warn(_('error accessing repository at %s\n') % path)
340 continue
346 continue
341 except error.RepoError:
347 except error.RepoError:
342 u.warn(_('error accessing repository at %s\n') % path)
348 u.warn(_('error accessing repository at %s\n') % path)
343 continue
349 continue
344 try:
350 try:
345 d = (get_mtime(r.spath), util.makedate()[1])
351 d = (get_mtime(r.spath), util.makedate()[1])
346 except OSError:
352 except OSError:
347 continue
353 continue
348
354
349 contact = get_contact(get)
355 contact = get_contact(get)
350 description = get("web", "description", "")
356 description = get("web", "description", "")
351 name = get("web", "name", name)
357 name = get("web", "name", name)
352 row = dict(contact=contact or "unknown",
358 row = dict(contact=contact or "unknown",
353 contact_sort=contact.upper() or "unknown",
359 contact_sort=contact.upper() or "unknown",
354 name=name,
360 name=name,
355 name_sort=name,
361 name_sort=name,
356 url=url,
362 url=url,
357 description=description or "unknown",
363 description=description or "unknown",
358 description_sort=description.upper() or "unknown",
364 description_sort=description.upper() or "unknown",
359 lastchange=d,
365 lastchange=d,
360 lastchange_sort=d[1]-d[0],
366 lastchange_sort=d[1]-d[0],
361 archives=archivelist(u, "tip", url))
367 archives=archivelist(u, "tip", url))
362
368
363 seenrepos.add(name)
369 seenrepos.add(name)
364 yield row
370 yield row
365
371
366 sortdefault = None, False
372 sortdefault = None, False
367 def entries(sortcolumn="", descending=False, subdir="", **map):
373 def entries(sortcolumn="", descending=False, subdir="", **map):
368 rows = rawentries(subdir=subdir, **map)
374 rows = rawentries(subdir=subdir, **map)
369
375
370 if sortcolumn and sortdefault != (sortcolumn, descending):
376 if sortcolumn and sortdefault != (sortcolumn, descending):
371 sortkey = '%s_sort' % sortcolumn
377 sortkey = '%s_sort' % sortcolumn
372 rows = sorted(rows, key=lambda x: x[sortkey],
378 rows = sorted(rows, key=lambda x: x[sortkey],
373 reverse=descending)
379 reverse=descending)
374 for row, parity in zip(rows, paritygen(self.stripecount)):
380 for row, parity in zip(rows, paritygen(self.stripecount)):
375 row['parity'] = parity
381 row['parity'] = parity
376 yield row
382 yield row
377
383
378 self.refresh()
384 self.refresh()
379 sortable = ["name", "description", "contact", "lastchange"]
385 sortable = ["name", "description", "contact", "lastchange"]
380 sortcolumn, descending = sortdefault
386 sortcolumn, descending = sortdefault
381 if 'sort' in req.form:
387 if 'sort' in req.form:
382 sortcolumn = req.form['sort'][0]
388 sortcolumn = req.form['sort'][0]
383 descending = sortcolumn.startswith('-')
389 descending = sortcolumn.startswith('-')
384 if descending:
390 if descending:
385 sortcolumn = sortcolumn[1:]
391 sortcolumn = sortcolumn[1:]
386 if sortcolumn not in sortable:
392 if sortcolumn not in sortable:
387 sortcolumn = ""
393 sortcolumn = ""
388
394
389 sort = [("sort_%s" % column,
395 sort = [("sort_%s" % column,
390 "%s%s" % ((not descending and column == sortcolumn)
396 "%s%s" % ((not descending and column == sortcolumn)
391 and "-" or "", column))
397 and "-" or "", column))
392 for column in sortable]
398 for column in sortable]
393
399
394 self.refresh()
400 self.refresh()
395 self.updatereqenv(req.env)
401 self.updatereqenv(req.env)
396
402
397 return tmpl("index", entries=entries, subdir=subdir,
403 return tmpl("index", entries=entries, subdir=subdir,
398 pathdef=makebreadcrumb('/' + subdir),
404 pathdef=makebreadcrumb('/' + subdir, self.prefix),
399 sortcolumn=sortcolumn, descending=descending,
405 sortcolumn=sortcolumn, descending=descending,
400 **dict(sort))
406 **dict(sort))
401
407
402 def templater(self, req):
408 def templater(self, req):
403
409
404 def header(**map):
410 def header(**map):
405 yield tmpl('header', encoding=encoding.encoding, **map)
411 yield tmpl('header', encoding=encoding.encoding, **map)
406
412
407 def footer(**map):
413 def footer(**map):
408 yield tmpl("footer", **map)
414 yield tmpl("footer", **map)
409
415
410 def motd(**map):
416 def motd(**map):
411 if self.motd is not None:
417 if self.motd is not None:
412 yield self.motd
418 yield self.motd
413 else:
419 else:
414 yield config('web', 'motd', '')
420 yield config('web', 'motd', '')
415
421
416 def config(section, name, default=None, untrusted=True):
422 def config(section, name, default=None, untrusted=True):
417 return self.ui.config(section, name, default, untrusted)
423 return self.ui.config(section, name, default, untrusted)
418
424
419 self.updatereqenv(req.env)
425 self.updatereqenv(req.env)
420
426
421 url = req.env.get('SCRIPT_NAME', '')
427 url = req.env.get('SCRIPT_NAME', '')
422 if not url.endswith('/'):
428 if not url.endswith('/'):
423 url += '/'
429 url += '/'
424
430
425 vars = {}
431 vars = {}
426 styles = (
432 styles = (
427 req.form.get('style', [None])[0],
433 req.form.get('style', [None])[0],
428 config('web', 'style'),
434 config('web', 'style'),
429 'paper'
435 'paper'
430 )
436 )
431 style, mapfile = templater.stylemap(styles, self.templatepath)
437 style, mapfile = templater.stylemap(styles, self.templatepath)
432 if style == styles[0]:
438 if style == styles[0]:
433 vars['style'] = style
439 vars['style'] = style
434
440
435 start = url[-1] == '?' and '&' or '?'
441 start = url[-1] == '?' and '&' or '?'
436 sessionvars = webutil.sessionvars(vars, start)
442 sessionvars = webutil.sessionvars(vars, start)
437 logourl = config('web', 'logourl', 'http://mercurial.selenic.com/')
443 logourl = config('web', 'logourl', 'http://mercurial.selenic.com/')
438 logoimg = config('web', 'logoimg', 'hglogo.png')
444 logoimg = config('web', 'logoimg', 'hglogo.png')
439 staticurl = config('web', 'staticurl') or url + 'static/'
445 staticurl = config('web', 'staticurl') or url + 'static/'
440 if not staticurl.endswith('/'):
446 if not staticurl.endswith('/'):
441 staticurl += '/'
447 staticurl += '/'
442
448
443 tmpl = templater.templater(mapfile,
449 tmpl = templater.templater(mapfile,
444 defaults={"header": header,
450 defaults={"header": header,
445 "footer": footer,
451 "footer": footer,
446 "motd": motd,
452 "motd": motd,
447 "url": url,
453 "url": url,
448 "logourl": logourl,
454 "logourl": logourl,
449 "logoimg": logoimg,
455 "logoimg": logoimg,
450 "staticurl": staticurl,
456 "staticurl": staticurl,
451 "sessionvars": sessionvars})
457 "sessionvars": sessionvars})
452 return tmpl
458 return tmpl
453
459
454 def updatereqenv(self, env):
460 def updatereqenv(self, env):
455 if self._baseurl is not None:
461 if self._baseurl is not None:
456 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
462 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
457 env['SERVER_NAME'] = name
463 env['SERVER_NAME'] = name
458 env['SERVER_PORT'] = port
464 env['SERVER_PORT'] = port
459 env['SCRIPT_NAME'] = path
465 env['SCRIPT_NAME'] = path
General Comments 0
You need to be logged in to leave comments. Login now