##// END OF EJS Templates
hgweb: don't choke when an inexistent style is requested (issue1901)
Dirkjan Ochtman -
r9842:d3dbdca9 default
parent child Browse files
Show More
@@ -1,319 +1,322 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, 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 import ui, hg, hook, error, encoding, templater
10 from mercurial import ui, hg, hook, error, encoding, templater
11 from common import get_mtime, ErrorResponse
11 from common import get_mtime, ErrorResponse
12 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
12 from common import HTTP_OK, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
13 from common import HTTP_UNAUTHORIZED, HTTP_METHOD_NOT_ALLOWED
13 from common import HTTP_UNAUTHORIZED, HTTP_METHOD_NOT_ALLOWED
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 'unbundle': 'push',
20 'unbundle': 'push',
21 'stream_out': 'pull',
21 'stream_out': 'pull',
22 }
22 }
23
23
24 class hgweb(object):
24 class hgweb(object):
25 def __init__(self, repo, name=None):
25 def __init__(self, repo, name=None):
26 if isinstance(repo, str):
26 if isinstance(repo, str):
27 u = ui.ui()
27 u = ui.ui()
28 u.setconfig('ui', 'report_untrusted', 'off')
28 u.setconfig('ui', 'report_untrusted', 'off')
29 u.setconfig('ui', 'interactive', 'off')
29 u.setconfig('ui', 'interactive', 'off')
30 self.repo = hg.repository(u, repo)
30 self.repo = hg.repository(u, repo)
31 else:
31 else:
32 self.repo = repo
32 self.repo = repo
33
33
34 hook.redirect(True)
34 hook.redirect(True)
35 self.mtime = -1
35 self.mtime = -1
36 self.reponame = name
36 self.reponame = name
37 self.archives = 'zip', 'gz', 'bz2'
37 self.archives = 'zip', 'gz', 'bz2'
38 self.stripecount = 1
38 self.stripecount = 1
39 # a repo owner may set web.templates in .hg/hgrc to get any file
39 # a repo owner may set web.templates in .hg/hgrc to get any file
40 # readable by the user running the CGI script
40 # readable by the user running the CGI script
41 self.templatepath = self.config('web', 'templates')
41 self.templatepath = self.config('web', 'templates')
42
42
43 # The CGI scripts are often run by a user different from the repo owner.
43 # The CGI scripts are often run by a user different from the repo owner.
44 # Trust the settings from the .hg/hgrc files by default.
44 # Trust the settings from the .hg/hgrc files by default.
45 def config(self, section, name, default=None, untrusted=True):
45 def config(self, section, name, default=None, untrusted=True):
46 return self.repo.ui.config(section, name, default,
46 return self.repo.ui.config(section, name, default,
47 untrusted=untrusted)
47 untrusted=untrusted)
48
48
49 def configbool(self, section, name, default=False, untrusted=True):
49 def configbool(self, section, name, default=False, untrusted=True):
50 return self.repo.ui.configbool(section, name, default,
50 return self.repo.ui.configbool(section, name, default,
51 untrusted=untrusted)
51 untrusted=untrusted)
52
52
53 def configlist(self, section, name, default=None, untrusted=True):
53 def configlist(self, section, name, default=None, untrusted=True):
54 return self.repo.ui.configlist(section, name, default,
54 return self.repo.ui.configlist(section, name, default,
55 untrusted=untrusted)
55 untrusted=untrusted)
56
56
57 def refresh(self):
57 def refresh(self):
58 mtime = get_mtime(self.repo.root)
58 mtime = get_mtime(self.repo.root)
59 if mtime != self.mtime:
59 if mtime != self.mtime:
60 self.mtime = mtime
60 self.mtime = mtime
61 self.repo = hg.repository(self.repo.ui, self.repo.root)
61 self.repo = hg.repository(self.repo.ui, self.repo.root)
62 self.maxchanges = int(self.config("web", "maxchanges", 10))
62 self.maxchanges = int(self.config("web", "maxchanges", 10))
63 self.stripecount = int(self.config("web", "stripes", 1))
63 self.stripecount = int(self.config("web", "stripes", 1))
64 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
64 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
65 self.maxfiles = int(self.config("web", "maxfiles", 10))
65 self.maxfiles = int(self.config("web", "maxfiles", 10))
66 self.allowpull = self.configbool("web", "allowpull", True)
66 self.allowpull = self.configbool("web", "allowpull", True)
67 encoding.encoding = self.config("web", "encoding",
67 encoding.encoding = self.config("web", "encoding",
68 encoding.encoding)
68 encoding.encoding)
69
69
70 def run(self):
70 def run(self):
71 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
71 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
72 raise RuntimeError("This function is only intended to be "
72 raise RuntimeError("This function is only intended to be "
73 "called while running as a CGI script.")
73 "called while running as a CGI script.")
74 import mercurial.hgweb.wsgicgi as wsgicgi
74 import mercurial.hgweb.wsgicgi as wsgicgi
75 wsgicgi.launch(self)
75 wsgicgi.launch(self)
76
76
77 def __call__(self, env, respond):
77 def __call__(self, env, respond):
78 req = wsgirequest(env, respond)
78 req = wsgirequest(env, respond)
79 return self.run_wsgi(req)
79 return self.run_wsgi(req)
80
80
81 def run_wsgi(self, req):
81 def run_wsgi(self, req):
82
82
83 self.refresh()
83 self.refresh()
84
84
85 # work with CGI variables to create coherent structure
85 # work with CGI variables to create coherent structure
86 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
86 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
87
87
88 req.url = req.env['SCRIPT_NAME']
88 req.url = req.env['SCRIPT_NAME']
89 if not req.url.endswith('/'):
89 if not req.url.endswith('/'):
90 req.url += '/'
90 req.url += '/'
91 if 'REPO_NAME' in req.env:
91 if 'REPO_NAME' in req.env:
92 req.url += req.env['REPO_NAME'] + '/'
92 req.url += req.env['REPO_NAME'] + '/'
93
93
94 if 'PATH_INFO' in req.env:
94 if 'PATH_INFO' in req.env:
95 parts = req.env['PATH_INFO'].strip('/').split('/')
95 parts = req.env['PATH_INFO'].strip('/').split('/')
96 repo_parts = req.env.get('REPO_NAME', '').split('/')
96 repo_parts = req.env.get('REPO_NAME', '').split('/')
97 if parts[:len(repo_parts)] == repo_parts:
97 if parts[:len(repo_parts)] == repo_parts:
98 parts = parts[len(repo_parts):]
98 parts = parts[len(repo_parts):]
99 query = '/'.join(parts)
99 query = '/'.join(parts)
100 else:
100 else:
101 query = req.env['QUERY_STRING'].split('&', 1)[0]
101 query = req.env['QUERY_STRING'].split('&', 1)[0]
102 query = query.split(';', 1)[0]
102 query = query.split(';', 1)[0]
103
103
104 # process this if it's a protocol request
104 # process this if it's a protocol request
105 # protocol bits don't need to create any URLs
105 # protocol bits don't need to create any URLs
106 # and the clients always use the old URL structure
106 # and the clients always use the old URL structure
107
107
108 cmd = req.form.get('cmd', [''])[0]
108 cmd = req.form.get('cmd', [''])[0]
109 if cmd and cmd in protocol.__all__:
109 if cmd and cmd in protocol.__all__:
110 if query:
110 if query:
111 raise ErrorResponse(HTTP_NOT_FOUND)
111 raise ErrorResponse(HTTP_NOT_FOUND)
112 try:
112 try:
113 if cmd in perms:
113 if cmd in perms:
114 try:
114 try:
115 self.check_perm(req, perms[cmd])
115 self.check_perm(req, perms[cmd])
116 except ErrorResponse, inst:
116 except ErrorResponse, inst:
117 if cmd == 'unbundle':
117 if cmd == 'unbundle':
118 req.drain()
118 req.drain()
119 raise
119 raise
120 method = getattr(protocol, cmd)
120 method = getattr(protocol, cmd)
121 return method(self.repo, req)
121 return method(self.repo, req)
122 except ErrorResponse, inst:
122 except ErrorResponse, inst:
123 req.respond(inst, protocol.HGTYPE)
123 req.respond(inst, protocol.HGTYPE)
124 if not inst.message:
124 if not inst.message:
125 return []
125 return []
126 return '0\n%s\n' % inst.message,
126 return '0\n%s\n' % inst.message,
127
127
128 # translate user-visible url structure to internal structure
128 # translate user-visible url structure to internal structure
129
129
130 args = query.split('/', 2)
130 args = query.split('/', 2)
131 if 'cmd' not in req.form and args and args[0]:
131 if 'cmd' not in req.form and args and args[0]:
132
132
133 cmd = args.pop(0)
133 cmd = args.pop(0)
134 style = cmd.rfind('-')
134 style = cmd.rfind('-')
135 if style != -1:
135 if style != -1:
136 req.form['style'] = [cmd[:style]]
136 req.form['style'] = [cmd[:style]]
137 cmd = cmd[style+1:]
137 cmd = cmd[style+1:]
138
138
139 # avoid accepting e.g. style parameter as command
139 # avoid accepting e.g. style parameter as command
140 if hasattr(webcommands, cmd):
140 if hasattr(webcommands, cmd):
141 req.form['cmd'] = [cmd]
141 req.form['cmd'] = [cmd]
142 else:
142 else:
143 cmd = ''
143 cmd = ''
144
144
145 if cmd == 'static':
145 if cmd == 'static':
146 req.form['file'] = ['/'.join(args)]
146 req.form['file'] = ['/'.join(args)]
147 else:
147 else:
148 if args and args[0]:
148 if args and args[0]:
149 node = args.pop(0)
149 node = args.pop(0)
150 req.form['node'] = [node]
150 req.form['node'] = [node]
151 if args:
151 if args:
152 req.form['file'] = args
152 req.form['file'] = args
153
153
154 ua = req.env.get('HTTP_USER_AGENT', '')
154 ua = req.env.get('HTTP_USER_AGENT', '')
155 if cmd == 'rev' and 'mercurial' in ua:
155 if cmd == 'rev' and 'mercurial' in ua:
156 req.form['style'] = ['raw']
156 req.form['style'] = ['raw']
157
157
158 if cmd == 'archive':
158 if cmd == 'archive':
159 fn = req.form['node'][0]
159 fn = req.form['node'][0]
160 for type_, spec in self.archive_specs.iteritems():
160 for type_, spec in self.archive_specs.iteritems():
161 ext = spec[2]
161 ext = spec[2]
162 if fn.endswith(ext):
162 if fn.endswith(ext):
163 req.form['node'] = [fn[:-len(ext)]]
163 req.form['node'] = [fn[:-len(ext)]]
164 req.form['type'] = [type_]
164 req.form['type'] = [type_]
165
165
166 # process the web interface request
166 # process the web interface request
167
167
168 try:
168 try:
169 tmpl = self.templater(req)
169 tmpl = self.templater(req)
170 ctype = tmpl('mimetype', encoding=encoding.encoding)
170 ctype = tmpl('mimetype', encoding=encoding.encoding)
171 ctype = templater.stringify(ctype)
171 ctype = templater.stringify(ctype)
172
172
173 # check read permissions non-static content
173 # check read permissions non-static content
174 if cmd != 'static':
174 if cmd != 'static':
175 self.check_perm(req, None)
175 self.check_perm(req, None)
176
176
177 if cmd == '':
177 if cmd == '':
178 req.form['cmd'] = [tmpl.cache['default']]
178 req.form['cmd'] = [tmpl.cache['default']]
179 cmd = req.form['cmd'][0]
179 cmd = req.form['cmd'][0]
180
180
181 if cmd not in webcommands.__all__:
181 if cmd not in webcommands.__all__:
182 msg = 'no such method: %s' % cmd
182 msg = 'no such method: %s' % cmd
183 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
183 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
184 elif cmd == 'file' and 'raw' in req.form.get('style', []):
184 elif cmd == 'file' and 'raw' in req.form.get('style', []):
185 self.ctype = ctype
185 self.ctype = ctype
186 content = webcommands.rawfile(self, req, tmpl)
186 content = webcommands.rawfile(self, req, tmpl)
187 else:
187 else:
188 content = getattr(webcommands, cmd)(self, req, tmpl)
188 content = getattr(webcommands, cmd)(self, req, tmpl)
189 req.respond(HTTP_OK, ctype)
189 req.respond(HTTP_OK, ctype)
190
190
191 return content
191 return content
192
192
193 except error.LookupError, err:
193 except error.LookupError, err:
194 req.respond(HTTP_NOT_FOUND, ctype)
194 req.respond(HTTP_NOT_FOUND, ctype)
195 msg = str(err)
195 msg = str(err)
196 if 'manifest' not in msg:
196 if 'manifest' not in msg:
197 msg = 'revision not found: %s' % err.name
197 msg = 'revision not found: %s' % err.name
198 return tmpl('error', error=msg)
198 return tmpl('error', error=msg)
199 except (error.RepoError, error.RevlogError), inst:
199 except (error.RepoError, error.RevlogError), inst:
200 req.respond(HTTP_SERVER_ERROR, ctype)
200 req.respond(HTTP_SERVER_ERROR, ctype)
201 return tmpl('error', error=str(inst))
201 return tmpl('error', error=str(inst))
202 except ErrorResponse, inst:
202 except ErrorResponse, inst:
203 req.respond(inst, ctype)
203 req.respond(inst, ctype)
204 return tmpl('error', error=inst.message)
204 return tmpl('error', error=inst.message)
205
205
206 def templater(self, req):
206 def templater(self, req):
207
207
208 # determine scheme, port and server name
208 # determine scheme, port and server name
209 # this is needed to create absolute urls
209 # this is needed to create absolute urls
210
210
211 proto = req.env.get('wsgi.url_scheme')
211 proto = req.env.get('wsgi.url_scheme')
212 if proto == 'https':
212 if proto == 'https':
213 proto = 'https'
213 proto = 'https'
214 default_port = "443"
214 default_port = "443"
215 else:
215 else:
216 proto = 'http'
216 proto = 'http'
217 default_port = "80"
217 default_port = "80"
218
218
219 port = req.env["SERVER_PORT"]
219 port = req.env["SERVER_PORT"]
220 port = port != default_port and (":" + port) or ""
220 port = port != default_port and (":" + port) or ""
221 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
221 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
222 staticurl = self.config("web", "staticurl") or req.url + 'static/'
222 staticurl = self.config("web", "staticurl") or req.url + 'static/'
223 if not staticurl.endswith('/'):
223 if not staticurl.endswith('/'):
224 staticurl += '/'
224 staticurl += '/'
225
225
226 # some functions for the templater
226 # some functions for the templater
227
227
228 def header(**map):
228 def header(**map):
229 yield tmpl('header', encoding=encoding.encoding, **map)
229 yield tmpl('header', encoding=encoding.encoding, **map)
230
230
231 def footer(**map):
231 def footer(**map):
232 yield tmpl("footer", **map)
232 yield tmpl("footer", **map)
233
233
234 def motd(**map):
234 def motd(**map):
235 yield self.config("web", "motd", "")
235 yield self.config("web", "motd", "")
236
236
237 # figure out which style to use
237 # figure out which style to use
238
238
239 vars = {}
239 vars = {}
240 style = self.config("web", "style", "paper")
240 styles = (
241 if 'style' in req.form:
241 req.form.get('style', [None])[0],
242 style = req.form['style'][0]
242 self.config('web', 'style'),
243 'paper',
244 )
245 style, mapfile = templater.stylemap(styles, self.templatepath)
246 if style == styles[0]:
243 vars['style'] = style
247 vars['style'] = style
244
248
245 start = req.url[-1] == '?' and '&' or '?'
249 start = req.url[-1] == '?' and '&' or '?'
246 sessionvars = webutil.sessionvars(vars, start)
250 sessionvars = webutil.sessionvars(vars, start)
247 mapfile = templater.stylemap(style, self.templatepath)
248
251
249 if not self.reponame:
252 if not self.reponame:
250 self.reponame = (self.config("web", "name")
253 self.reponame = (self.config("web", "name")
251 or req.env.get('REPO_NAME')
254 or req.env.get('REPO_NAME')
252 or req.url.strip('/') or self.repo.root)
255 or req.url.strip('/') or self.repo.root)
253
256
254 # create the templater
257 # create the templater
255
258
256 tmpl = templater.templater(mapfile,
259 tmpl = templater.templater(mapfile,
257 defaults={"url": req.url,
260 defaults={"url": req.url,
258 "staticurl": staticurl,
261 "staticurl": staticurl,
259 "urlbase": urlbase,
262 "urlbase": urlbase,
260 "repo": self.reponame,
263 "repo": self.reponame,
261 "header": header,
264 "header": header,
262 "footer": footer,
265 "footer": footer,
263 "motd": motd,
266 "motd": motd,
264 "sessionvars": sessionvars
267 "sessionvars": sessionvars
265 })
268 })
266 return tmpl
269 return tmpl
267
270
268 def archivelist(self, nodeid):
271 def archivelist(self, nodeid):
269 allowed = self.configlist("web", "allow_archive")
272 allowed = self.configlist("web", "allow_archive")
270 for i, spec in self.archive_specs.iteritems():
273 for i, spec in self.archive_specs.iteritems():
271 if i in allowed or self.configbool("web", "allow" + i):
274 if i in allowed or self.configbool("web", "allow" + i):
272 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
275 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
273
276
274 archive_specs = {
277 archive_specs = {
275 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
278 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
276 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
279 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
277 'zip': ('application/zip', 'zip', '.zip', None),
280 'zip': ('application/zip', 'zip', '.zip', None),
278 }
281 }
279
282
280 def check_perm(self, req, op):
283 def check_perm(self, req, op):
281 '''Check permission for operation based on request data (including
284 '''Check permission for operation based on request data (including
282 authentication info). Return if op allowed, else raise an ErrorResponse
285 authentication info). Return if op allowed, else raise an ErrorResponse
283 exception.'''
286 exception.'''
284
287
285 user = req.env.get('REMOTE_USER')
288 user = req.env.get('REMOTE_USER')
286
289
287 deny_read = self.configlist('web', 'deny_read')
290 deny_read = self.configlist('web', 'deny_read')
288 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
291 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
289 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
292 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
290
293
291 allow_read = self.configlist('web', 'allow_read')
294 allow_read = self.configlist('web', 'allow_read')
292 result = (not allow_read) or (allow_read == ['*'])
295 result = (not allow_read) or (allow_read == ['*'])
293 if not (result or user in allow_read):
296 if not (result or user in allow_read):
294 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
297 raise ErrorResponse(HTTP_UNAUTHORIZED, 'read not authorized')
295
298
296 if op == 'pull' and not self.allowpull:
299 if op == 'pull' and not self.allowpull:
297 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
300 raise ErrorResponse(HTTP_UNAUTHORIZED, 'pull not authorized')
298 elif op == 'pull' or op is None: # op is None for interface requests
301 elif op == 'pull' or op is None: # op is None for interface requests
299 return
302 return
300
303
301 # enforce that you can only push using POST requests
304 # enforce that you can only push using POST requests
302 if req.env['REQUEST_METHOD'] != 'POST':
305 if req.env['REQUEST_METHOD'] != 'POST':
303 msg = 'push requires POST request'
306 msg = 'push requires POST request'
304 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
307 raise ErrorResponse(HTTP_METHOD_NOT_ALLOWED, msg)
305
308
306 # require ssl by default for pushing, auth info cannot be sniffed
309 # require ssl by default for pushing, auth info cannot be sniffed
307 # and replayed
310 # and replayed
308 scheme = req.env.get('wsgi.url_scheme')
311 scheme = req.env.get('wsgi.url_scheme')
309 if self.configbool('web', 'push_ssl', True) and scheme != 'https':
312 if self.configbool('web', 'push_ssl', True) and scheme != 'https':
310 raise ErrorResponse(HTTP_OK, 'ssl required')
313 raise ErrorResponse(HTTP_OK, 'ssl required')
311
314
312 deny = self.configlist('web', 'deny_push')
315 deny = self.configlist('web', 'deny_push')
313 if deny and (not user or deny == ['*'] or user in deny):
316 if deny and (not user or deny == ['*'] or user in deny):
314 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
317 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
315
318
316 allow = self.configlist('web', 'allow_push')
319 allow = self.configlist('web', 'allow_push')
317 result = allow and (allow == ['*'] or user in allow)
320 result = allow and (allow == ['*'] or user in allow)
318 if not result:
321 if not result:
319 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
322 raise ErrorResponse(HTTP_UNAUTHORIZED, 'push not authorized')
@@ -1,337 +1,340 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, re, time
9 import os, re, 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):
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/*" makes every subrepo of /bar/ to be
26 # "foo = /bar/*" makes every subrepo of /bar/ to be
27 # mounted as foo/subrepo
27 # mounted as foo/subrepo
28 # and "foo = /bar/**" also recurses into the subdirectories,
28 # and "foo = /bar/**" also recurses into the subdirectories,
29 # remember to use it without working dir.
29 # remember to use it without working dir.
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(roothead)
35 roothead = os.path.normpath(roothead)
36 for path in util.walkrepos(roothead, followsym=True, recurse=recurse):
36 for path in util.walkrepos(roothead, followsym=True, recurse=recurse):
37 path = os.path.normpath(path)
37 path = os.path.normpath(path)
38 name = util.pconvert(path[len(roothead):]).strip('/')
38 name = util.pconvert(path[len(roothead):]).strip('/')
39 if prefix:
39 if prefix:
40 name = prefix + '/' + name
40 name = prefix + '/' + name
41 repos.append((name, path))
41 repos.append((name, path))
42 return repos
42 return repos
43
43
44 class hgwebdir(object):
44 class hgwebdir(object):
45 refreshinterval = 20
45 refreshinterval = 20
46
46
47 def __init__(self, conf, baseui=None):
47 def __init__(self, conf, baseui=None):
48 self.conf = conf
48 self.conf = conf
49 self.baseui = baseui
49 self.baseui = baseui
50 self.lastrefresh = 0
50 self.lastrefresh = 0
51 self.refresh()
51 self.refresh()
52
52
53 def refresh(self):
53 def refresh(self):
54 if self.lastrefresh + self.refreshinterval > time.time():
54 if self.lastrefresh + self.refreshinterval > time.time():
55 return
55 return
56
56
57 if self.baseui:
57 if self.baseui:
58 self.ui = self.baseui.copy()
58 self.ui = self.baseui.copy()
59 else:
59 else:
60 self.ui = ui.ui()
60 self.ui = ui.ui()
61 self.ui.setconfig('ui', 'report_untrusted', 'off')
61 self.ui.setconfig('ui', 'report_untrusted', 'off')
62 self.ui.setconfig('ui', 'interactive', 'off')
62 self.ui.setconfig('ui', 'interactive', 'off')
63
63
64 if not isinstance(self.conf, (dict, list, tuple)):
64 if not isinstance(self.conf, (dict, list, tuple)):
65 map = {'paths': 'hgweb-paths'}
65 map = {'paths': 'hgweb-paths'}
66 self.ui.readconfig(self.conf, remap=map, trust=True)
66 self.ui.readconfig(self.conf, remap=map, trust=True)
67 paths = self.ui.configitems('hgweb-paths')
67 paths = self.ui.configitems('hgweb-paths')
68 elif isinstance(self.conf, (list, tuple)):
68 elif isinstance(self.conf, (list, tuple)):
69 paths = self.conf
69 paths = self.conf
70 elif isinstance(self.conf, dict):
70 elif isinstance(self.conf, dict):
71 paths = self.conf.items()
71 paths = self.conf.items()
72
72
73 encoding.encoding = self.ui.config('web', 'encoding',
73 encoding.encoding = self.ui.config('web', 'encoding',
74 encoding.encoding)
74 encoding.encoding)
75 self.motd = self.ui.config('web', 'motd')
75 self.motd = self.ui.config('web', 'motd')
76 self.style = self.ui.config('web', 'style', 'paper')
76 self.style = self.ui.config('web', 'style', 'paper')
77 self.stripecount = self.ui.config('web', 'stripes', 1)
77 self.stripecount = self.ui.config('web', 'stripes', 1)
78 if self.stripecount:
78 if self.stripecount:
79 self.stripecount = int(self.stripecount)
79 self.stripecount = int(self.stripecount)
80 self._baseurl = self.ui.config('web', 'baseurl')
80 self._baseurl = self.ui.config('web', 'baseurl')
81
81
82 self.repos = findrepos(paths)
82 self.repos = findrepos(paths)
83 for prefix, root in self.ui.configitems('collections'):
83 for prefix, root in self.ui.configitems('collections'):
84 prefix = util.pconvert(prefix)
84 prefix = util.pconvert(prefix)
85 for path in util.walkrepos(root, followsym=True):
85 for path in util.walkrepos(root, followsym=True):
86 repo = os.path.normpath(path)
86 repo = os.path.normpath(path)
87 name = util.pconvert(repo)
87 name = util.pconvert(repo)
88 if name.startswith(prefix):
88 if name.startswith(prefix):
89 name = name[len(prefix):]
89 name = name[len(prefix):]
90 self.repos.append((name.lstrip('/'), repo))
90 self.repos.append((name.lstrip('/'), repo))
91
91
92 self.lastrefresh = time.time()
92 self.lastrefresh = time.time()
93
93
94 def run(self):
94 def run(self):
95 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
95 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
96 raise RuntimeError("This function is only intended to be "
96 raise RuntimeError("This function is only intended to be "
97 "called while running as a CGI script.")
97 "called while running as a CGI script.")
98 import mercurial.hgweb.wsgicgi as wsgicgi
98 import mercurial.hgweb.wsgicgi as wsgicgi
99 wsgicgi.launch(self)
99 wsgicgi.launch(self)
100
100
101 def __call__(self, env, respond):
101 def __call__(self, env, respond):
102 req = wsgirequest(env, respond)
102 req = wsgirequest(env, respond)
103 return self.run_wsgi(req)
103 return self.run_wsgi(req)
104
104
105 def read_allowed(self, ui, req):
105 def read_allowed(self, ui, req):
106 """Check allow_read and deny_read config options of a repo's ui object
106 """Check allow_read and deny_read config options of a repo's ui object
107 to determine user permissions. By default, with neither option set (or
107 to determine user permissions. By default, with neither option set (or
108 both empty), allow all users to read the repo. There are two ways a
108 both empty), allow all users to read the repo. There are two ways a
109 user can be denied read access: (1) deny_read is not empty, and the
109 user can be denied read access: (1) deny_read is not empty, and the
110 user is unauthenticated or deny_read contains user (or *), and (2)
110 user is unauthenticated or deny_read contains user (or *), and (2)
111 allow_read is not empty and the user is not in allow_read. Return True
111 allow_read is not empty and the user is not in allow_read. Return True
112 if user is allowed to read the repo, else return False."""
112 if user is allowed to read the repo, else return False."""
113
113
114 user = req.env.get('REMOTE_USER')
114 user = req.env.get('REMOTE_USER')
115
115
116 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
116 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
117 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
117 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
118 return False
118 return False
119
119
120 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
120 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
121 # by default, allow reading if no allow_read option has been set
121 # by default, allow reading if no allow_read option has been set
122 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
122 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
123 return True
123 return True
124
124
125 return False
125 return False
126
126
127 def run_wsgi(self, req):
127 def run_wsgi(self, req):
128 try:
128 try:
129 try:
129 try:
130 self.refresh()
130 self.refresh()
131
131
132 virtual = req.env.get("PATH_INFO", "").strip('/')
132 virtual = req.env.get("PATH_INFO", "").strip('/')
133 tmpl = self.templater(req)
133 tmpl = self.templater(req)
134 ctype = tmpl('mimetype', encoding=encoding.encoding)
134 ctype = tmpl('mimetype', encoding=encoding.encoding)
135 ctype = templater.stringify(ctype)
135 ctype = templater.stringify(ctype)
136
136
137 # a static file
137 # a static file
138 if virtual.startswith('static/') or 'static' in req.form:
138 if virtual.startswith('static/') or 'static' in req.form:
139 if virtual.startswith('static/'):
139 if virtual.startswith('static/'):
140 fname = virtual[7:]
140 fname = virtual[7:]
141 else:
141 else:
142 fname = req.form['static'][0]
142 fname = req.form['static'][0]
143 static = templater.templatepath('static')
143 static = templater.templatepath('static')
144 return (staticfile(static, fname, req),)
144 return (staticfile(static, fname, req),)
145
145
146 # top-level index
146 # top-level index
147 elif not virtual:
147 elif not virtual:
148 req.respond(HTTP_OK, ctype)
148 req.respond(HTTP_OK, ctype)
149 return self.makeindex(req, tmpl)
149 return self.makeindex(req, tmpl)
150
150
151 # nested indexes and hgwebs
151 # nested indexes and hgwebs
152
152
153 repos = dict(self.repos)
153 repos = dict(self.repos)
154 while virtual:
154 while virtual:
155 real = repos.get(virtual)
155 real = repos.get(virtual)
156 if real:
156 if real:
157 req.env['REPO_NAME'] = virtual
157 req.env['REPO_NAME'] = virtual
158 try:
158 try:
159 repo = hg.repository(self.ui, real)
159 repo = hg.repository(self.ui, real)
160 return hgweb(repo).run_wsgi(req)
160 return hgweb(repo).run_wsgi(req)
161 except IOError, inst:
161 except IOError, inst:
162 msg = inst.strerror
162 msg = inst.strerror
163 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
163 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
164 except error.RepoError, inst:
164 except error.RepoError, inst:
165 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
165 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
166
166
167 # browse subdirectories
167 # browse subdirectories
168 subdir = virtual + '/'
168 subdir = virtual + '/'
169 if [r for r in repos if r.startswith(subdir)]:
169 if [r for r in repos if r.startswith(subdir)]:
170 req.respond(HTTP_OK, ctype)
170 req.respond(HTTP_OK, ctype)
171 return self.makeindex(req, tmpl, subdir)
171 return self.makeindex(req, tmpl, subdir)
172
172
173 up = virtual.rfind('/')
173 up = virtual.rfind('/')
174 if up < 0:
174 if up < 0:
175 break
175 break
176 virtual = virtual[:up]
176 virtual = virtual[:up]
177
177
178 # prefixes not found
178 # prefixes not found
179 req.respond(HTTP_NOT_FOUND, ctype)
179 req.respond(HTTP_NOT_FOUND, ctype)
180 return tmpl("notfound", repo=virtual)
180 return tmpl("notfound", repo=virtual)
181
181
182 except ErrorResponse, err:
182 except ErrorResponse, err:
183 req.respond(err, ctype)
183 req.respond(err, ctype)
184 return tmpl('error', error=err.message or '')
184 return tmpl('error', error=err.message or '')
185 finally:
185 finally:
186 tmpl = None
186 tmpl = None
187
187
188 def makeindex(self, req, tmpl, subdir=""):
188 def makeindex(self, req, tmpl, subdir=""):
189
189
190 def archivelist(ui, nodeid, url):
190 def archivelist(ui, nodeid, url):
191 allowed = ui.configlist("web", "allow_archive", untrusted=True)
191 allowed = ui.configlist("web", "allow_archive", untrusted=True)
192 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
192 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
193 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
193 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
194 untrusted=True):
194 untrusted=True):
195 yield {"type" : i[0], "extension": i[1],
195 yield {"type" : i[0], "extension": i[1],
196 "node": nodeid, "url": url}
196 "node": nodeid, "url": url}
197
197
198 sortdefault = None, False
198 sortdefault = None, False
199 def entries(sortcolumn="", descending=False, subdir="", **map):
199 def entries(sortcolumn="", descending=False, subdir="", **map):
200
200
201 rows = []
201 rows = []
202 parity = paritygen(self.stripecount)
202 parity = paritygen(self.stripecount)
203 descend = self.ui.configbool('web', 'descend', True)
203 descend = self.ui.configbool('web', 'descend', True)
204 for name, path in self.repos:
204 for name, path in self.repos:
205
205
206 if not name.startswith(subdir):
206 if not name.startswith(subdir):
207 continue
207 continue
208 name = name[len(subdir):]
208 name = name[len(subdir):]
209 if not descend and '/' in name:
209 if not descend and '/' in name:
210 continue
210 continue
211
211
212 u = self.ui.copy()
212 u = self.ui.copy()
213 try:
213 try:
214 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
214 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
215 except Exception, e:
215 except Exception, e:
216 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
216 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
217 continue
217 continue
218 def get(section, name, default=None):
218 def get(section, name, default=None):
219 return u.config(section, name, default, untrusted=True)
219 return u.config(section, name, default, untrusted=True)
220
220
221 if u.configbool("web", "hidden", untrusted=True):
221 if u.configbool("web", "hidden", untrusted=True):
222 continue
222 continue
223
223
224 if not self.read_allowed(u, req):
224 if not self.read_allowed(u, req):
225 continue
225 continue
226
226
227 parts = [name]
227 parts = [name]
228 if 'PATH_INFO' in req.env:
228 if 'PATH_INFO' in req.env:
229 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
229 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
230 if req.env['SCRIPT_NAME']:
230 if req.env['SCRIPT_NAME']:
231 parts.insert(0, req.env['SCRIPT_NAME'])
231 parts.insert(0, req.env['SCRIPT_NAME'])
232 m = re.match('((?:https?://)?)(.*)', '/'.join(parts))
232 m = re.match('((?:https?://)?)(.*)', '/'.join(parts))
233 # squish repeated slashes out of the path component
233 # squish repeated slashes out of the path component
234 url = m.group(1) + re.sub('/+', '/', m.group(2)) + '/'
234 url = m.group(1) + re.sub('/+', '/', m.group(2)) + '/'
235
235
236 # update time with local timezone
236 # update time with local timezone
237 try:
237 try:
238 d = (get_mtime(path), util.makedate()[1])
238 d = (get_mtime(path), util.makedate()[1])
239 except OSError:
239 except OSError:
240 continue
240 continue
241
241
242 contact = get_contact(get)
242 contact = get_contact(get)
243 description = get("web", "description", "")
243 description = get("web", "description", "")
244 name = get("web", "name", name)
244 name = get("web", "name", name)
245 row = dict(contact=contact or "unknown",
245 row = dict(contact=contact or "unknown",
246 contact_sort=contact.upper() or "unknown",
246 contact_sort=contact.upper() or "unknown",
247 name=name,
247 name=name,
248 name_sort=name,
248 name_sort=name,
249 url=url,
249 url=url,
250 description=description or "unknown",
250 description=description or "unknown",
251 description_sort=description.upper() or "unknown",
251 description_sort=description.upper() or "unknown",
252 lastchange=d,
252 lastchange=d,
253 lastchange_sort=d[1]-d[0],
253 lastchange_sort=d[1]-d[0],
254 archives=archivelist(u, "tip", url))
254 archives=archivelist(u, "tip", url))
255 if (not sortcolumn or (sortcolumn, descending) == sortdefault):
255 if (not sortcolumn or (sortcolumn, descending) == sortdefault):
256 # fast path for unsorted output
256 # fast path for unsorted output
257 row['parity'] = parity.next()
257 row['parity'] = parity.next()
258 yield row
258 yield row
259 else:
259 else:
260 rows.append((row["%s_sort" % sortcolumn], row))
260 rows.append((row["%s_sort" % sortcolumn], row))
261 if rows:
261 if rows:
262 rows.sort()
262 rows.sort()
263 if descending:
263 if descending:
264 rows.reverse()
264 rows.reverse()
265 for key, row in rows:
265 for key, row in rows:
266 row['parity'] = parity.next()
266 row['parity'] = parity.next()
267 yield row
267 yield row
268
268
269 self.refresh()
269 self.refresh()
270 sortable = ["name", "description", "contact", "lastchange"]
270 sortable = ["name", "description", "contact", "lastchange"]
271 sortcolumn, descending = sortdefault
271 sortcolumn, descending = sortdefault
272 if 'sort' in req.form:
272 if 'sort' in req.form:
273 sortcolumn = req.form['sort'][0]
273 sortcolumn = req.form['sort'][0]
274 descending = sortcolumn.startswith('-')
274 descending = sortcolumn.startswith('-')
275 if descending:
275 if descending:
276 sortcolumn = sortcolumn[1:]
276 sortcolumn = sortcolumn[1:]
277 if sortcolumn not in sortable:
277 if sortcolumn not in sortable:
278 sortcolumn = ""
278 sortcolumn = ""
279
279
280 sort = [("sort_%s" % column,
280 sort = [("sort_%s" % column,
281 "%s%s" % ((not descending and column == sortcolumn)
281 "%s%s" % ((not descending and column == sortcolumn)
282 and "-" or "", column))
282 and "-" or "", column))
283 for column in sortable]
283 for column in sortable]
284
284
285 self.refresh()
285 self.refresh()
286 if self._baseurl is not None:
286 if self._baseurl is not None:
287 req.env['SCRIPT_NAME'] = self._baseurl
287 req.env['SCRIPT_NAME'] = self._baseurl
288
288
289 return tmpl("index", entries=entries, subdir=subdir,
289 return tmpl("index", entries=entries, subdir=subdir,
290 sortcolumn=sortcolumn, descending=descending,
290 sortcolumn=sortcolumn, descending=descending,
291 **dict(sort))
291 **dict(sort))
292
292
293 def templater(self, req):
293 def templater(self, req):
294
294
295 def header(**map):
295 def header(**map):
296 yield tmpl('header', encoding=encoding.encoding, **map)
296 yield tmpl('header', encoding=encoding.encoding, **map)
297
297
298 def footer(**map):
298 def footer(**map):
299 yield tmpl("footer", **map)
299 yield tmpl("footer", **map)
300
300
301 def motd(**map):
301 def motd(**map):
302 if self.motd is not None:
302 if self.motd is not None:
303 yield self.motd
303 yield self.motd
304 else:
304 else:
305 yield config('web', 'motd', '')
305 yield config('web', 'motd', '')
306
306
307 def config(section, name, default=None, untrusted=True):
307 def config(section, name, default=None, untrusted=True):
308 return self.ui.config(section, name, default, untrusted)
308 return self.ui.config(section, name, default, untrusted)
309
309
310 if self._baseurl is not None:
310 if self._baseurl is not None:
311 req.env['SCRIPT_NAME'] = self._baseurl
311 req.env['SCRIPT_NAME'] = self._baseurl
312
312
313 url = req.env.get('SCRIPT_NAME', '')
313 url = req.env.get('SCRIPT_NAME', '')
314 if not url.endswith('/'):
314 if not url.endswith('/'):
315 url += '/'
315 url += '/'
316
316
317 vars = {}
317 vars = {}
318 style = self.style
318 styles = (
319 if 'style' in req.form:
319 req.form.get('style', [None])[0],
320 vars['style'] = style = req.form['style'][0]
320 config('web', 'style'),
321 'paper'
322 )
323 style, mapfile = templater.stylemap(styles)
324 if style == styles[0]:
325 vars['style'] = style
326
321 start = url[-1] == '?' and '&' or '?'
327 start = url[-1] == '?' and '&' or '?'
322 sessionvars = webutil.sessionvars(vars, start)
328 sessionvars = webutil.sessionvars(vars, start)
323
324 staticurl = config('web', 'staticurl') or url + 'static/'
329 staticurl = config('web', 'staticurl') or url + 'static/'
325 if not staticurl.endswith('/'):
330 if not staticurl.endswith('/'):
326 staticurl += '/'
331 staticurl += '/'
327
332
328 style = 'style' in req.form and req.form['style'][0] or self.style
329 mapfile = templater.stylemap(style)
330 tmpl = templater.templater(mapfile,
333 tmpl = templater.templater(mapfile,
331 defaults={"header": header,
334 defaults={"header": header,
332 "footer": footer,
335 "footer": footer,
333 "motd": motd,
336 "motd": motd,
334 "url": url,
337 "url": url,
335 "staticurl": staticurl,
338 "staticurl": staticurl,
336 "sessionvars": sessionvars})
339 "sessionvars": sessionvars})
337 return tmpl
340 return tmpl
@@ -1,245 +1,253 b''
1 # templater.py - template expansion for output
1 # templater.py - template expansion for output
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 from i18n import _
8 from i18n import _
9 import re, sys, os
9 import re, sys, os
10 import util, config, templatefilters
10 import util, config, templatefilters
11
11
12 path = ['templates', '../templates']
12 path = ['templates', '../templates']
13 stringify = templatefilters.stringify
13 stringify = templatefilters.stringify
14
14
15 def parsestring(s, quoted=True):
15 def parsestring(s, quoted=True):
16 '''parse a string using simple c-like syntax.
16 '''parse a string using simple c-like syntax.
17 string must be in quotes if quoted is True.'''
17 string must be in quotes if quoted is True.'''
18 if quoted:
18 if quoted:
19 if len(s) < 2 or s[0] != s[-1]:
19 if len(s) < 2 or s[0] != s[-1]:
20 raise SyntaxError(_('unmatched quotes'))
20 raise SyntaxError(_('unmatched quotes'))
21 return s[1:-1].decode('string_escape')
21 return s[1:-1].decode('string_escape')
22
22
23 return s.decode('string_escape')
23 return s.decode('string_escape')
24
24
25 class engine(object):
25 class engine(object):
26 '''template expansion engine.
26 '''template expansion engine.
27
27
28 template expansion works like this. a map file contains key=value
28 template expansion works like this. a map file contains key=value
29 pairs. if value is quoted, it is treated as string. otherwise, it
29 pairs. if value is quoted, it is treated as string. otherwise, it
30 is treated as name of template file.
30 is treated as name of template file.
31
31
32 templater is asked to expand a key in map. it looks up key, and
32 templater is asked to expand a key in map. it looks up key, and
33 looks for strings like this: {foo}. it expands {foo} by looking up
33 looks for strings like this: {foo}. it expands {foo} by looking up
34 foo in map, and substituting it. expansion is recursive: it stops
34 foo in map, and substituting it. expansion is recursive: it stops
35 when there is no more {foo} to replace.
35 when there is no more {foo} to replace.
36
36
37 expansion also allows formatting and filtering.
37 expansion also allows formatting and filtering.
38
38
39 format uses key to expand each item in list. syntax is
39 format uses key to expand each item in list. syntax is
40 {key%format}.
40 {key%format}.
41
41
42 filter uses function to transform value. syntax is
42 filter uses function to transform value. syntax is
43 {key|filter1|filter2|...}.'''
43 {key|filter1|filter2|...}.'''
44
44
45 template_re = re.compile(r'{([\w\|%]+)}')
45 template_re = re.compile(r'{([\w\|%]+)}')
46
46
47 def __init__(self, loader, filters={}, defaults={}):
47 def __init__(self, loader, filters={}, defaults={}):
48 self.loader = loader
48 self.loader = loader
49 self.filters = filters
49 self.filters = filters
50 self.defaults = defaults
50 self.defaults = defaults
51 self.cache = {}
51 self.cache = {}
52
52
53 def process(self, t, map):
53 def process(self, t, map):
54 '''Perform expansion. t is name of map element to expand. map contains
54 '''Perform expansion. t is name of map element to expand. map contains
55 added elements for use during expansion. Is a generator.'''
55 added elements for use during expansion. Is a generator.'''
56 tmpl = self.loader(t)
56 tmpl = self.loader(t)
57 iters = [self._process(tmpl, map)]
57 iters = [self._process(tmpl, map)]
58 while iters:
58 while iters:
59 try:
59 try:
60 item = iters[0].next()
60 item = iters[0].next()
61 except StopIteration:
61 except StopIteration:
62 iters.pop(0)
62 iters.pop(0)
63 continue
63 continue
64 if isinstance(item, str):
64 if isinstance(item, str):
65 yield item
65 yield item
66 elif item is None:
66 elif item is None:
67 yield ''
67 yield ''
68 elif hasattr(item, '__iter__'):
68 elif hasattr(item, '__iter__'):
69 iters.insert(0, iter(item))
69 iters.insert(0, iter(item))
70 else:
70 else:
71 yield str(item)
71 yield str(item)
72
72
73 def _format(self, expr, get, map):
73 def _format(self, expr, get, map):
74 key, format = expr.split('%')
74 key, format = expr.split('%')
75 v = get(key)
75 v = get(key)
76 if not hasattr(v, '__iter__'):
76 if not hasattr(v, '__iter__'):
77 raise SyntaxError(_("error expanding '%s%%%s'") % (key, format))
77 raise SyntaxError(_("error expanding '%s%%%s'") % (key, format))
78 lm = map.copy()
78 lm = map.copy()
79 for i in v:
79 for i in v:
80 lm.update(i)
80 lm.update(i)
81 yield self.process(format, lm)
81 yield self.process(format, lm)
82
82
83 def _filter(self, expr, get, map):
83 def _filter(self, expr, get, map):
84 if expr not in self.cache:
84 if expr not in self.cache:
85 parts = expr.split('|')
85 parts = expr.split('|')
86 val = parts[0]
86 val = parts[0]
87 try:
87 try:
88 filters = [self.filters[f] for f in parts[1:]]
88 filters = [self.filters[f] for f in parts[1:]]
89 except KeyError, i:
89 except KeyError, i:
90 raise SyntaxError(_("unknown filter '%s'") % i[0])
90 raise SyntaxError(_("unknown filter '%s'") % i[0])
91 def apply(get):
91 def apply(get):
92 x = get(val)
92 x = get(val)
93 for f in filters:
93 for f in filters:
94 x = f(x)
94 x = f(x)
95 return x
95 return x
96 self.cache[expr] = apply
96 self.cache[expr] = apply
97 return self.cache[expr](get)
97 return self.cache[expr](get)
98
98
99 def _process(self, tmpl, map):
99 def _process(self, tmpl, map):
100 '''Render a template. Returns a generator.'''
100 '''Render a template. Returns a generator.'''
101
101
102 def get(key):
102 def get(key):
103 v = map.get(key)
103 v = map.get(key)
104 if v is None:
104 if v is None:
105 v = self.defaults.get(key, '')
105 v = self.defaults.get(key, '')
106 if hasattr(v, '__call__'):
106 if hasattr(v, '__call__'):
107 v = v(**map)
107 v = v(**map)
108 return v
108 return v
109
109
110 while tmpl:
110 while tmpl:
111 m = self.template_re.search(tmpl)
111 m = self.template_re.search(tmpl)
112 if not m:
112 if not m:
113 yield tmpl
113 yield tmpl
114 break
114 break
115
115
116 start, end = m.span(0)
116 start, end = m.span(0)
117 variants = m.groups()
117 variants = m.groups()
118 expr = variants[0] or variants[1]
118 expr = variants[0] or variants[1]
119
119
120 if start:
120 if start:
121 yield tmpl[:start]
121 yield tmpl[:start]
122 tmpl = tmpl[end:]
122 tmpl = tmpl[end:]
123
123
124 if '%' in expr:
124 if '%' in expr:
125 yield self._format(expr, get, map)
125 yield self._format(expr, get, map)
126 elif '|' in expr:
126 elif '|' in expr:
127 yield self._filter(expr, get, map)
127 yield self._filter(expr, get, map)
128 else:
128 else:
129 yield get(expr)
129 yield get(expr)
130
130
131 engines = {'default': engine}
131 engines = {'default': engine}
132
132
133 class templater(object):
133 class templater(object):
134
134
135 def __init__(self, mapfile, filters={}, defaults={}, cache={},
135 def __init__(self, mapfile, filters={}, defaults={}, cache={},
136 minchunk=1024, maxchunk=65536):
136 minchunk=1024, maxchunk=65536):
137 '''set up template engine.
137 '''set up template engine.
138 mapfile is name of file to read map definitions from.
138 mapfile is name of file to read map definitions from.
139 filters is dict of functions. each transforms a value into another.
139 filters is dict of functions. each transforms a value into another.
140 defaults is dict of default map definitions.'''
140 defaults is dict of default map definitions.'''
141 self.mapfile = mapfile or 'template'
141 self.mapfile = mapfile or 'template'
142 self.cache = cache.copy()
142 self.cache = cache.copy()
143 self.map = {}
143 self.map = {}
144 self.base = (mapfile and os.path.dirname(mapfile)) or ''
144 self.base = (mapfile and os.path.dirname(mapfile)) or ''
145 self.filters = templatefilters.filters.copy()
145 self.filters = templatefilters.filters.copy()
146 self.filters.update(filters)
146 self.filters.update(filters)
147 self.defaults = defaults
147 self.defaults = defaults
148 self.minchunk, self.maxchunk = minchunk, maxchunk
148 self.minchunk, self.maxchunk = minchunk, maxchunk
149 self.engines = {}
149 self.engines = {}
150
150
151 if not mapfile:
151 if not mapfile:
152 return
152 return
153 if not os.path.exists(mapfile):
153 if not os.path.exists(mapfile):
154 raise util.Abort(_('style not found: %s') % mapfile)
154 raise util.Abort(_('style not found: %s') % mapfile)
155
155
156 conf = config.config()
156 conf = config.config()
157 conf.read(mapfile)
157 conf.read(mapfile)
158
158
159 for key, val in conf[''].items():
159 for key, val in conf[''].items():
160 if val[0] in "'\"":
160 if val[0] in "'\"":
161 try:
161 try:
162 self.cache[key] = parsestring(val)
162 self.cache[key] = parsestring(val)
163 except SyntaxError, inst:
163 except SyntaxError, inst:
164 raise SyntaxError('%s: %s' %
164 raise SyntaxError('%s: %s' %
165 (conf.source('', key), inst.args[0]))
165 (conf.source('', key), inst.args[0]))
166 else:
166 else:
167 val = 'default', val
167 val = 'default', val
168 if ':' in val[1]:
168 if ':' in val[1]:
169 val = val[1].split(':', 1)
169 val = val[1].split(':', 1)
170 self.map[key] = val[0], os.path.join(self.base, val[1])
170 self.map[key] = val[0], os.path.join(self.base, val[1])
171
171
172 def __contains__(self, key):
172 def __contains__(self, key):
173 return key in self.cache or key in self.map
173 return key in self.cache or key in self.map
174
174
175 def load(self, t):
175 def load(self, t):
176 '''Get the template for the given template name. Use a local cache.'''
176 '''Get the template for the given template name. Use a local cache.'''
177 if not t in self.cache:
177 if not t in self.cache:
178 try:
178 try:
179 self.cache[t] = open(self.map[t][1]).read()
179 self.cache[t] = open(self.map[t][1]).read()
180 except IOError, inst:
180 except IOError, inst:
181 raise IOError(inst.args[0], _('template file %s: %s') %
181 raise IOError(inst.args[0], _('template file %s: %s') %
182 (self.map[t][1], inst.args[1]))
182 (self.map[t][1], inst.args[1]))
183 return self.cache[t]
183 return self.cache[t]
184
184
185 def __call__(self, t, **map):
185 def __call__(self, t, **map):
186 ttype = t in self.map and self.map[t][0] or 'default'
186 ttype = t in self.map and self.map[t][0] or 'default'
187 proc = self.engines.get(ttype)
187 proc = self.engines.get(ttype)
188 if proc is None:
188 if proc is None:
189 proc = engines[ttype](self.load, self.filters, self.defaults)
189 proc = engines[ttype](self.load, self.filters, self.defaults)
190 self.engines[ttype] = proc
190 self.engines[ttype] = proc
191
191
192 stream = proc.process(t, map)
192 stream = proc.process(t, map)
193 if self.minchunk:
193 if self.minchunk:
194 stream = util.increasingchunks(stream, min=self.minchunk,
194 stream = util.increasingchunks(stream, min=self.minchunk,
195 max=self.maxchunk)
195 max=self.maxchunk)
196 return stream
196 return stream
197
197
198 def templatepath(name=None):
198 def templatepath(name=None):
199 '''return location of template file or directory (if no name).
199 '''return location of template file or directory (if no name).
200 returns None if not found.'''
200 returns None if not found.'''
201 normpaths = []
201 normpaths = []
202
202
203 # executable version (py2exe) doesn't support __file__
203 # executable version (py2exe) doesn't support __file__
204 if hasattr(sys, 'frozen'):
204 if hasattr(sys, 'frozen'):
205 module = sys.executable
205 module = sys.executable
206 else:
206 else:
207 module = __file__
207 module = __file__
208 for f in path:
208 for f in path:
209 if f.startswith('/'):
209 if f.startswith('/'):
210 p = f
210 p = f
211 else:
211 else:
212 fl = f.split('/')
212 fl = f.split('/')
213 p = os.path.join(os.path.dirname(module), *fl)
213 p = os.path.join(os.path.dirname(module), *fl)
214 if name:
214 if name:
215 p = os.path.join(p, name)
215 p = os.path.join(p, name)
216 if name and os.path.exists(p):
216 if name and os.path.exists(p):
217 return os.path.normpath(p)
217 return os.path.normpath(p)
218 elif os.path.isdir(p):
218 elif os.path.isdir(p):
219 normpaths.append(os.path.normpath(p))
219 normpaths.append(os.path.normpath(p))
220
220
221 return normpaths
221 return normpaths
222
222
223 def stylemap(style, paths=None):
223 def stylemap(styles, paths=None):
224 """Return path to mapfile for a given style.
224 """Return path to mapfile for a given style.
225
225
226 Searches mapfile in the following locations:
226 Searches mapfile in the following locations:
227 1. templatepath/style/map
227 1. templatepath/style/map
228 2. templatepath/map-style
228 2. templatepath/map-style
229 3. templatepath/map
229 3. templatepath/map
230 """
230 """
231
231
232 if paths is None:
232 if paths is None:
233 paths = templatepath()
233 paths = templatepath()
234 elif isinstance(paths, str):
234 elif isinstance(paths, str):
235 paths = [paths]
235 paths = [paths]
236
236
237 locations = style and [os.path.join(style, "map"), "map-" + style] or []
237 if isinstance(styles, str):
238 locations.append("map")
238 styles = [styles]
239
240 for style in styles:
241
242 if not style:
243 continue
244 locations = [os.path.join(style, 'map'), 'map-' + style]
245 locations.append('map')
246
239 for path in paths:
247 for path in paths:
240 for location in locations:
248 for location in locations:
241 mapfile = os.path.join(path, location)
249 mapfile = os.path.join(path, location)
242 if os.path.isfile(mapfile):
250 if os.path.isfile(mapfile):
243 return mapfile
251 return style, mapfile
244
252
245 raise RuntimeError("No hgweb templates found in %r" % paths)
253 raise RuntimeError("No hgweb templates found in %r" % paths)
@@ -1,44 +1,49 b''
1 #!/bin/sh
1 #!/bin/sh
2 # Some tests for hgweb. Tests static files, plain files and different 404's.
2 # Some tests for hgweb. Tests static files, plain files and different 404's.
3
3
4 hg init test
4 hg init test
5 cd test
5 cd test
6 mkdir da
6 mkdir da
7 echo foo > da/foo
7 echo foo > da/foo
8 echo foo > foo
8 echo foo > foo
9 hg ci -Ambase
9 hg ci -Ambase
10
10 hg serve -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
11 hg serve -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
11 cat hg.pid >> $DAEMON_PIDS
12 cat hg.pid >> $DAEMON_PIDS
13
12 echo % manifest
14 echo % manifest
13 ("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/?style=raw')
15 ("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/?style=raw')
14 ("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/da?style=raw')
16 ("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/da?style=raw')
15
17
16 echo % plain file
18 echo % plain file
17 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/foo?style=raw'
19 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/foo?style=raw'
18
20
19 echo % should give a 404 - static file that does not exist
21 echo % should give a 404 - static file that does not exist
20 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/static/bogus'
22 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/static/bogus'
21
23
22 echo % should give a 404 - bad revision
24 echo % should give a 404 - bad revision
23 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/spam/foo?style=raw'
25 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/spam/foo?style=raw'
24
26
25 echo % should give a 400 - bad command
27 echo % should give a 400 - bad command
26 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/foo?cmd=spam&style=raw' | sed 's/400.*/400/'
28 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/foo?cmd=spam&style=raw' | sed 's/400.*/400/'
27
29
28 echo % should give a 404 - file does not exist
30 echo % should give a 404 - file does not exist
29 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/bork?style=raw'
31 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/bork?style=raw'
30 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/bork'
32 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/bork'
31 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/diff/tip/bork?style=raw'
33 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/diff/tip/bork?style=raw'
32
34
35 echo % try bad style
36 ("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/?style=foobar')
37
33 echo % stop and restart
38 echo % stop and restart
34 "$TESTDIR/killdaemons.py"
39 "$TESTDIR/killdaemons.py"
35 hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log
40 hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log
36 cat hg.pid >> $DAEMON_PIDS
41 cat hg.pid >> $DAEMON_PIDS
37 # Test the access/error files are opened in append mode
42 # Test the access/error files are opened in append mode
38 python -c "print len(file('access.log').readlines()), 'log lines written'"
43 python -c "print len(file('access.log').readlines()), 'log lines written'"
39
44
40 echo % static file
45 echo % static file
41 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/static/style-gitweb.css'
46 "$TESTDIR/get-with-headers.py" localhost:$HGPORT '/static/style-gitweb.css'
42
47
43 echo % errors
48 echo % errors
44 cat errors.log
49 cat errors.log
@@ -1,279 +1,367 b''
1 adding da/foo
1 adding da/foo
2 adding foo
2 adding foo
3 % manifest
3 % manifest
4 200 Script output follows
4 200 Script output follows
5
5
6
6
7 drwxr-xr-x da
7 drwxr-xr-x da
8 -rw-r--r-- 4 foo
8 -rw-r--r-- 4 foo
9
9
10
10
11 200 Script output follows
11 200 Script output follows
12
12
13
13
14 -rw-r--r-- 4 foo
14 -rw-r--r-- 4 foo
15
15
16
16
17 % plain file
17 % plain file
18 200 Script output follows
18 200 Script output follows
19
19
20 foo
20 foo
21 % should give a 404 - static file that does not exist
21 % should give a 404 - static file that does not exist
22 404 Not Found
22 404 Not Found
23
23
24 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
24 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
25 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
25 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
26 <head>
26 <head>
27 <link rel="icon" href="/static/hgicon.png" type="image/png" />
27 <link rel="icon" href="/static/hgicon.png" type="image/png" />
28 <meta name="robots" content="index, nofollow" />
28 <meta name="robots" content="index, nofollow" />
29 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
29 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
30
30
31 <title>test: error</title>
31 <title>test: error</title>
32 </head>
32 </head>
33 <body>
33 <body>
34
34
35 <div class="container">
35 <div class="container">
36 <div class="menu">
36 <div class="menu">
37 <div class="logo">
37 <div class="logo">
38 <a href="http://mercurial.selenic.com/">
38 <a href="http://mercurial.selenic.com/">
39 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
39 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
40 </div>
40 </div>
41 <ul>
41 <ul>
42 <li><a href="/shortlog">log</a></li>
42 <li><a href="/shortlog">log</a></li>
43 <li><a href="/graph">graph</a></li>
43 <li><a href="/graph">graph</a></li>
44 <li><a href="/tags">tags</a></li>
44 <li><a href="/tags">tags</a></li>
45 <li><a href="/branches">branches</a></li>
45 <li><a href="/branches">branches</a></li>
46 </ul>
46 </ul>
47 </div>
47 </div>
48
48
49 <div class="main">
49 <div class="main">
50
50
51 <h2><a href="/">test</a></h2>
51 <h2><a href="/">test</a></h2>
52 <h3>error</h3>
52 <h3>error</h3>
53
53
54 <form class="search" action="/log">
54 <form class="search" action="/log">
55
55
56 <p><input name="rev" id="search1" type="text" size="30"></p>
56 <p><input name="rev" id="search1" type="text" size="30"></p>
57 <div id="hint">find changesets by author, revision,
57 <div id="hint">find changesets by author, revision,
58 files, or words in the commit message</div>
58 files, or words in the commit message</div>
59 </form>
59 </form>
60
60
61 <div class="description">
61 <div class="description">
62 <p>
62 <p>
63 An error occurred while processing your request:
63 An error occurred while processing your request:
64 </p>
64 </p>
65 <p>
65 <p>
66 Not Found
66 Not Found
67 </p>
67 </p>
68 </div>
68 </div>
69 </div>
69 </div>
70 </div>
70 </div>
71
71
72
72
73
73
74 </body>
74 </body>
75 </html>
75 </html>
76
76
77 % should give a 404 - bad revision
77 % should give a 404 - bad revision
78 404 Not Found
78 404 Not Found
79
79
80
80
81 error: revision not found: spam
81 error: revision not found: spam
82 % should give a 400 - bad command
82 % should give a 400 - bad command
83 400
83 400
84
84
85
85
86 error: no such method: spam
86 error: no such method: spam
87 % should give a 404 - file does not exist
87 % should give a 404 - file does not exist
88 404 Not Found
88 404 Not Found
89
89
90
90
91 error: bork@2ef0ac749a14: not found in manifest
91 error: bork@2ef0ac749a14: not found in manifest
92 404 Not Found
92 404 Not Found
93
93
94 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
94 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
95 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
95 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
96 <head>
96 <head>
97 <link rel="icon" href="/static/hgicon.png" type="image/png" />
97 <link rel="icon" href="/static/hgicon.png" type="image/png" />
98 <meta name="robots" content="index, nofollow" />
98 <meta name="robots" content="index, nofollow" />
99 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
99 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
100
100
101 <title>test: error</title>
101 <title>test: error</title>
102 </head>
102 </head>
103 <body>
103 <body>
104
104
105 <div class="container">
105 <div class="container">
106 <div class="menu">
106 <div class="menu">
107 <div class="logo">
107 <div class="logo">
108 <a href="http://mercurial.selenic.com/">
108 <a href="http://mercurial.selenic.com/">
109 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
109 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
110 </div>
110 </div>
111 <ul>
111 <ul>
112 <li><a href="/shortlog">log</a></li>
112 <li><a href="/shortlog">log</a></li>
113 <li><a href="/graph">graph</a></li>
113 <li><a href="/graph">graph</a></li>
114 <li><a href="/tags">tags</a></li>
114 <li><a href="/tags">tags</a></li>
115 <li><a href="/branches">branches</a></li>
115 <li><a href="/branches">branches</a></li>
116 </ul>
116 </ul>
117 </div>
117 </div>
118
118
119 <div class="main">
119 <div class="main">
120
120
121 <h2><a href="/">test</a></h2>
121 <h2><a href="/">test</a></h2>
122 <h3>error</h3>
122 <h3>error</h3>
123
123
124 <form class="search" action="/log">
124 <form class="search" action="/log">
125
125
126 <p><input name="rev" id="search1" type="text" size="30"></p>
126 <p><input name="rev" id="search1" type="text" size="30"></p>
127 <div id="hint">find changesets by author, revision,
127 <div id="hint">find changesets by author, revision,
128 files, or words in the commit message</div>
128 files, or words in the commit message</div>
129 </form>
129 </form>
130
130
131 <div class="description">
131 <div class="description">
132 <p>
132 <p>
133 An error occurred while processing your request:
133 An error occurred while processing your request:
134 </p>
134 </p>
135 <p>
135 <p>
136 bork@2ef0ac749a14: not found in manifest
136 bork@2ef0ac749a14: not found in manifest
137 </p>
137 </p>
138 </div>
138 </div>
139 </div>
139 </div>
140 </div>
140 </div>
141
141
142
142
143
143
144 </body>
144 </body>
145 </html>
145 </html>
146
146
147 404 Not Found
147 404 Not Found
148
148
149
149
150 error: bork@2ef0ac749a14: not found in manifest
150 error: bork@2ef0ac749a14: not found in manifest
151 % try bad style
152 200 Script output follows
153
154 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
155 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
156 <head>
157 <link rel="icon" href="/static/hgicon.png" type="image/png" />
158 <meta name="robots" content="index, nofollow" />
159 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
160
161 <title>test: 2ef0ac749a14 /</title>
162 </head>
163 <body>
164
165 <div class="container">
166 <div class="menu">
167 <div class="logo">
168 <a href="http://mercurial.selenic.com/">
169 <img src="/static/hglogo.png" alt="mercurial" /></a>
170 </div>
171 <ul>
172 <li><a href="/shortlog/2ef0ac749a14">log</a></li>
173 <li><a href="/graph/2ef0ac749a14">graph</a></li>
174 <li><a href="/tags">tags</a></li>
175 <li><a href="/branches">branches</a></li>
176 </ul>
177 <ul>
178 <li><a href="/rev/2ef0ac749a14">changeset</a></li>
179 <li class="active">browse</li>
180 </ul>
181 <ul>
182
183 </ul>
184 </div>
185
186 <div class="main">
187 <h2><a href="/">test</a></h2>
188 <h3>directory / @ 0:2ef0ac749a14 <span class="tag">tip</span> </h3>
189
190 <form class="search" action="/log">
191
192 <p><input name="rev" id="search1" type="text" size="30" /></p>
193 <div id="hint">find changesets by author, revision,
194 files, or words in the commit message</div>
195 </form>
196
197 <table class="bigtable">
198 <tr>
199 <th class="name">name</th>
200 <th class="size">size</th>
201 <th class="permissions">permissions</th>
202 </tr>
203 <tr class="fileline parity0">
204 <td class="name"><a href="/file/2ef0ac749a14/">[up]</a></td>
205 <td class="size"></td>
206 <td class="permissions">drwxr-xr-x</td>
207 </tr>
208
209 <tr class="fileline parity1">
210 <td class="name">
211 <a href="/file/2ef0ac749a14/da">
212 <img src="/static/coal-folder.png" alt="dir."/> da/
213 </a>
214 <a href="/file/2ef0ac749a14/da/">
215
216 </a>
217 </td>
218 <td class="size"></td>
219 <td class="permissions">drwxr-xr-x</td>
220 </tr>
221
222 <tr class="fileline parity0">
223 <td class="filename">
224 <a href="/file/2ef0ac749a14/foo">
225 <img src="/static/coal-file.png" alt="file"/> foo
226 </a>
227 </td>
228 <td class="size">4</td>
229 <td class="permissions">-rw-r--r--</td>
230 </tr>
231 </table>
232 </div>
233 </div>
234
235
236 </body>
237 </html>
238
151 % stop and restart
239 % stop and restart
152 9 log lines written
240 10 log lines written
153 % static file
241 % static file
154 200 Script output follows
242 200 Script output follows
155
243
156 body { font-family: sans-serif; font-size: 12px; margin:0px; border:solid #d9d8d1; border-width:1px; margin:10px; }
244 body { font-family: sans-serif; font-size: 12px; margin:0px; border:solid #d9d8d1; border-width:1px; margin:10px; }
157 a { color:#0000cc; }
245 a { color:#0000cc; }
158 a:hover, a:visited, a:active { color:#880000; }
246 a:hover, a:visited, a:active { color:#880000; }
159 div.page_header { height:25px; padding:8px; font-size:18px; font-weight:bold; background-color:#d9d8d1; }
247 div.page_header { height:25px; padding:8px; font-size:18px; font-weight:bold; background-color:#d9d8d1; }
160 div.page_header a:visited { color:#0000cc; }
248 div.page_header a:visited { color:#0000cc; }
161 div.page_header a:hover { color:#880000; }
249 div.page_header a:hover { color:#880000; }
162 div.page_nav { padding:8px; }
250 div.page_nav { padding:8px; }
163 div.page_nav a:visited { color:#0000cc; }
251 div.page_nav a:visited { color:#0000cc; }
164 div.page_path { padding:8px; border:solid #d9d8d1; border-width:0px 0px 1px}
252 div.page_path { padding:8px; border:solid #d9d8d1; border-width:0px 0px 1px}
165 div.page_footer { padding:4px 8px; background-color: #d9d8d1; }
253 div.page_footer { padding:4px 8px; background-color: #d9d8d1; }
166 div.page_footer_text { float:left; color:#555555; font-style:italic; }
254 div.page_footer_text { float:left; color:#555555; font-style:italic; }
167 div.page_body { padding:8px; }
255 div.page_body { padding:8px; }
168 div.title, a.title {
256 div.title, a.title {
169 display:block; padding:6px 8px;
257 display:block; padding:6px 8px;
170 font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
258 font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
171 }
259 }
172 a.title:hover { background-color: #d9d8d1; }
260 a.title:hover { background-color: #d9d8d1; }
173 div.title_text { padding:6px 0px; border: solid #d9d8d1; border-width:0px 0px 1px; }
261 div.title_text { padding:6px 0px; border: solid #d9d8d1; border-width:0px 0px 1px; }
174 div.log_body { padding:8px 8px 8px 150px; }
262 div.log_body { padding:8px 8px 8px 150px; }
175 .age { white-space:nowrap; }
263 .age { white-space:nowrap; }
176 span.age { position:relative; float:left; width:142px; font-style:italic; }
264 span.age { position:relative; float:left; width:142px; font-style:italic; }
177 div.log_link {
265 div.log_link {
178 padding:0px 8px;
266 padding:0px 8px;
179 font-size:10px; font-family:sans-serif; font-style:normal;
267 font-size:10px; font-family:sans-serif; font-style:normal;
180 position:relative; float:left; width:136px;
268 position:relative; float:left; width:136px;
181 }
269 }
182 div.list_head { padding:6px 8px 4px; border:solid #d9d8d1; border-width:1px 0px 0px; font-style:italic; }
270 div.list_head { padding:6px 8px 4px; border:solid #d9d8d1; border-width:1px 0px 0px; font-style:italic; }
183 a.list { text-decoration:none; color:#000000; }
271 a.list { text-decoration:none; color:#000000; }
184 a.list:hover { text-decoration:underline; color:#880000; }
272 a.list:hover { text-decoration:underline; color:#880000; }
185 table { padding:8px 4px; }
273 table { padding:8px 4px; }
186 th { padding:2px 5px; font-size:12px; text-align:left; }
274 th { padding:2px 5px; font-size:12px; text-align:left; }
187 tr.light:hover, .parity0:hover { background-color:#edece6; }
275 tr.light:hover, .parity0:hover { background-color:#edece6; }
188 tr.dark, .parity1 { background-color:#f6f6f0; }
276 tr.dark, .parity1 { background-color:#f6f6f0; }
189 tr.dark:hover, .parity1:hover { background-color:#edece6; }
277 tr.dark:hover, .parity1:hover { background-color:#edece6; }
190 td { padding:2px 5px; font-size:12px; vertical-align:top; }
278 td { padding:2px 5px; font-size:12px; vertical-align:top; }
191 td.link { padding:2px 5px; font-family:sans-serif; font-size:10px; }
279 td.link { padding:2px 5px; font-family:sans-serif; font-size:10px; }
192 td.indexlinks { white-space: nowrap; }
280 td.indexlinks { white-space: nowrap; }
193 td.indexlinks a {
281 td.indexlinks a {
194 padding: 2px 5px; line-height: 10px;
282 padding: 2px 5px; line-height: 10px;
195 border: 1px solid;
283 border: 1px solid;
196 color: #ffffff; background-color: #7777bb;
284 color: #ffffff; background-color: #7777bb;
197 border-color: #aaaadd #333366 #333366 #aaaadd;
285 border-color: #aaaadd #333366 #333366 #aaaadd;
198 font-weight: bold; text-align: center; text-decoration: none;
286 font-weight: bold; text-align: center; text-decoration: none;
199 font-size: 10px;
287 font-size: 10px;
200 }
288 }
201 td.indexlinks a:hover { background-color: #6666aa; }
289 td.indexlinks a:hover { background-color: #6666aa; }
202 div.pre { font-family:monospace; font-size:12px; white-space:pre; }
290 div.pre { font-family:monospace; font-size:12px; white-space:pre; }
203 div.diff_info { font-family:monospace; color:#000099; background-color:#edece6; font-style:italic; }
291 div.diff_info { font-family:monospace; color:#000099; background-color:#edece6; font-style:italic; }
204 div.index_include { border:solid #d9d8d1; border-width:0px 0px 1px; padding:12px 8px; }
292 div.index_include { border:solid #d9d8d1; border-width:0px 0px 1px; padding:12px 8px; }
205 div.search { margin:4px 8px; position:absolute; top:56px; right:12px }
293 div.search { margin:4px 8px; position:absolute; top:56px; right:12px }
206 .linenr { color:#999999; text-decoration:none }
294 .linenr { color:#999999; text-decoration:none }
207 div.rss_logo { float: right; white-space: nowrap; }
295 div.rss_logo { float: right; white-space: nowrap; }
208 div.rss_logo a {
296 div.rss_logo a {
209 padding:3px 6px; line-height:10px;
297 padding:3px 6px; line-height:10px;
210 border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e;
298 border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e;
211 color:#ffffff; background-color:#ff6600;
299 color:#ffffff; background-color:#ff6600;
212 font-weight:bold; font-family:sans-serif; font-size:10px;
300 font-weight:bold; font-family:sans-serif; font-size:10px;
213 text-align:center; text-decoration:none;
301 text-align:center; text-decoration:none;
214 }
302 }
215 div.rss_logo a:hover { background-color:#ee5500; }
303 div.rss_logo a:hover { background-color:#ee5500; }
216 pre { margin: 0; }
304 pre { margin: 0; }
217 span.logtags span {
305 span.logtags span {
218 padding: 0px 4px;
306 padding: 0px 4px;
219 font-size: 10px;
307 font-size: 10px;
220 font-weight: normal;
308 font-weight: normal;
221 border: 1px solid;
309 border: 1px solid;
222 background-color: #ffaaff;
310 background-color: #ffaaff;
223 border-color: #ffccff #ff00ee #ff00ee #ffccff;
311 border-color: #ffccff #ff00ee #ff00ee #ffccff;
224 }
312 }
225 span.logtags span.tagtag {
313 span.logtags span.tagtag {
226 background-color: #ffffaa;
314 background-color: #ffffaa;
227 border-color: #ffffcc #ffee00 #ffee00 #ffffcc;
315 border-color: #ffffcc #ffee00 #ffee00 #ffffcc;
228 }
316 }
229 span.logtags span.branchtag {
317 span.logtags span.branchtag {
230 background-color: #aaffaa;
318 background-color: #aaffaa;
231 border-color: #ccffcc #00cc33 #00cc33 #ccffcc;
319 border-color: #ccffcc #00cc33 #00cc33 #ccffcc;
232 }
320 }
233 span.logtags span.inbranchtag {
321 span.logtags span.inbranchtag {
234 background-color: #d5dde6;
322 background-color: #d5dde6;
235 border-color: #e3ecf4 #9398f4 #9398f4 #e3ecf4;
323 border-color: #e3ecf4 #9398f4 #9398f4 #e3ecf4;
236 }
324 }
237
325
238 /* Graph */
326 /* Graph */
239 div#wrapper {
327 div#wrapper {
240 position: relative;
328 position: relative;
241 margin: 0;
329 margin: 0;
242 padding: 0;
330 padding: 0;
243 margin-top: 3px;
331 margin-top: 3px;
244 }
332 }
245
333
246 canvas {
334 canvas {
247 position: absolute;
335 position: absolute;
248 z-index: 5;
336 z-index: 5;
249 top: -0.9em;
337 top: -0.9em;
250 margin: 0;
338 margin: 0;
251 }
339 }
252
340
253 ul#nodebgs {
341 ul#nodebgs {
254 list-style: none inside none;
342 list-style: none inside none;
255 padding: 0;
343 padding: 0;
256 margin: 0;
344 margin: 0;
257 top: -0.7em;
345 top: -0.7em;
258 }
346 }
259
347
260 ul#graphnodes li, ul#nodebgs li {
348 ul#graphnodes li, ul#nodebgs li {
261 height: 39px;
349 height: 39px;
262 }
350 }
263
351
264 ul#graphnodes {
352 ul#graphnodes {
265 position: absolute;
353 position: absolute;
266 z-index: 10;
354 z-index: 10;
267 top: -0.8em;
355 top: -0.8em;
268 list-style: none inside none;
356 list-style: none inside none;
269 padding: 0;
357 padding: 0;
270 }
358 }
271
359
272 ul#graphnodes li .info {
360 ul#graphnodes li .info {
273 display: block;
361 display: block;
274 font-size: 100%;
362 font-size: 100%;
275 position: relative;
363 position: relative;
276 top: -3px;
364 top: -3px;
277 font-style: italic;
365 font-style: italic;
278 }
366 }
279 % errors
367 % errors
General Comments 0
You need to be logged in to leave comments. Login now