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