##// END OF EJS Templates
hgweb: another fix for the help termwidth bug
Matt Mackall -
r12696:ef969e58 default
parent child Browse files
Show More
@@ -1,286 +1,286 b''
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import os
9 import os
10 from mercurial import ui, hg, hook, error, encoding, templater
10 from mercurial import ui, hg, hook, error, encoding, templater
11 from common import get_mtime, ErrorResponse, permhooks, caching
11 from common import get_mtime, ErrorResponse, permhooks, caching
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 request import wsgirequest
13 from request import wsgirequest
14 import webcommands, protocol, webutil
14 import webcommands, protocol, webutil
15
15
16 perms = {
16 perms = {
17 'changegroup': 'pull',
17 'changegroup': 'pull',
18 'changegroupsubset': 'pull',
18 'changegroupsubset': 'pull',
19 'stream_out': 'pull',
19 'stream_out': 'pull',
20 'listkeys': 'pull',
20 'listkeys': 'pull',
21 'unbundle': 'push',
21 'unbundle': 'push',
22 'pushkey': 'push',
22 'pushkey': 'push',
23 }
23 }
24
24
25 class hgweb(object):
25 class hgweb(object):
26 def __init__(self, repo, name=None, baseui=None):
26 def __init__(self, repo, name=None, baseui=None):
27 if isinstance(repo, str):
27 if isinstance(repo, str):
28 if baseui:
28 if baseui:
29 u = baseui.copy()
29 u = baseui.copy()
30 else:
30 else:
31 u = webutil.wsgiui()
31 u = ui.ui()
32 self.repo = hg.repository(u, repo)
32 self.repo = hg.repository(u, repo)
33 else:
33 else:
34 self.repo = repo
34 self.repo = repo
35
35
36 self.repo.ui.setconfig('ui', 'report_untrusted', 'off')
36 self.repo.ui.setconfig('ui', 'report_untrusted', 'off')
37 self.repo.ui.setconfig('ui', 'interactive', 'off')
37 self.repo.ui.setconfig('ui', 'interactive', 'off')
38 hook.redirect(True)
38 hook.redirect(True)
39 self.mtime = -1
39 self.mtime = -1
40 self.reponame = name
40 self.reponame = name
41 self.archives = 'zip', 'gz', 'bz2'
41 self.archives = 'zip', 'gz', 'bz2'
42 self.stripecount = 1
42 self.stripecount = 1
43 # a repo owner may set web.templates in .hg/hgrc to get any file
43 # a repo owner may set web.templates in .hg/hgrc to get any file
44 # readable by the user running the CGI script
44 # readable by the user running the CGI script
45 self.templatepath = self.config('web', 'templates')
45 self.templatepath = self.config('web', 'templates')
46
46
47 # The CGI scripts are often run by a user different from the repo owner.
47 # The CGI scripts are often run by a user different from the repo owner.
48 # Trust the settings from the .hg/hgrc files by default.
48 # Trust the settings from the .hg/hgrc files by default.
49 def config(self, section, name, default=None, untrusted=True):
49 def config(self, section, name, default=None, untrusted=True):
50 return self.repo.ui.config(section, name, default,
50 return self.repo.ui.config(section, name, default,
51 untrusted=untrusted)
51 untrusted=untrusted)
52
52
53 def configbool(self, section, name, default=False, untrusted=True):
53 def configbool(self, section, name, default=False, untrusted=True):
54 return self.repo.ui.configbool(section, name, default,
54 return self.repo.ui.configbool(section, name, default,
55 untrusted=untrusted)
55 untrusted=untrusted)
56
56
57 def configlist(self, section, name, default=None, untrusted=True):
57 def configlist(self, section, name, default=None, untrusted=True):
58 return self.repo.ui.configlist(section, name, default,
58 return self.repo.ui.configlist(section, name, default,
59 untrusted=untrusted)
59 untrusted=untrusted)
60
60
61 def refresh(self, request=None):
61 def refresh(self, request=None):
62 if request:
62 if request:
63 self.repo.ui.environ = request.env
63 self.repo.ui.environ = request.env
64 mtime = get_mtime(self.repo.spath)
64 mtime = get_mtime(self.repo.spath)
65 if mtime != self.mtime:
65 if mtime != self.mtime:
66 self.mtime = mtime
66 self.mtime = mtime
67 self.repo = hg.repository(self.repo.ui, self.repo.root)
67 self.repo = hg.repository(self.repo.ui, self.repo.root)
68 self.maxchanges = int(self.config("web", "maxchanges", 10))
68 self.maxchanges = int(self.config("web", "maxchanges", 10))
69 self.stripecount = int(self.config("web", "stripes", 1))
69 self.stripecount = int(self.config("web", "stripes", 1))
70 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
70 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
71 self.maxfiles = int(self.config("web", "maxfiles", 10))
71 self.maxfiles = int(self.config("web", "maxfiles", 10))
72 self.allowpull = self.configbool("web", "allowpull", True)
72 self.allowpull = self.configbool("web", "allowpull", True)
73 encoding.encoding = self.config("web", "encoding",
73 encoding.encoding = self.config("web", "encoding",
74 encoding.encoding)
74 encoding.encoding)
75
75
76 def run(self):
76 def run(self):
77 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
77 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
78 raise RuntimeError("This function is only intended to be "
78 raise RuntimeError("This function is only intended to be "
79 "called while running as a CGI script.")
79 "called while running as a CGI script.")
80 import mercurial.hgweb.wsgicgi as wsgicgi
80 import mercurial.hgweb.wsgicgi as wsgicgi
81 wsgicgi.launch(self)
81 wsgicgi.launch(self)
82
82
83 def __call__(self, env, respond):
83 def __call__(self, env, respond):
84 req = wsgirequest(env, respond)
84 req = wsgirequest(env, respond)
85 return self.run_wsgi(req)
85 return self.run_wsgi(req)
86
86
87 def run_wsgi(self, req):
87 def run_wsgi(self, req):
88
88
89 self.refresh(req)
89 self.refresh(req)
90
90
91 # work with CGI variables to create coherent structure
91 # work with CGI variables to create coherent structure
92 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
92 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
93
93
94 req.url = req.env['SCRIPT_NAME']
94 req.url = req.env['SCRIPT_NAME']
95 if not req.url.endswith('/'):
95 if not req.url.endswith('/'):
96 req.url += '/'
96 req.url += '/'
97 if 'REPO_NAME' in req.env:
97 if 'REPO_NAME' in req.env:
98 req.url += req.env['REPO_NAME'] + '/'
98 req.url += req.env['REPO_NAME'] + '/'
99
99
100 if 'PATH_INFO' in req.env:
100 if 'PATH_INFO' in req.env:
101 parts = req.env['PATH_INFO'].strip('/').split('/')
101 parts = req.env['PATH_INFO'].strip('/').split('/')
102 repo_parts = req.env.get('REPO_NAME', '').split('/')
102 repo_parts = req.env.get('REPO_NAME', '').split('/')
103 if parts[:len(repo_parts)] == repo_parts:
103 if parts[:len(repo_parts)] == repo_parts:
104 parts = parts[len(repo_parts):]
104 parts = parts[len(repo_parts):]
105 query = '/'.join(parts)
105 query = '/'.join(parts)
106 else:
106 else:
107 query = req.env['QUERY_STRING'].split('&', 1)[0]
107 query = req.env['QUERY_STRING'].split('&', 1)[0]
108 query = query.split(';', 1)[0]
108 query = query.split(';', 1)[0]
109
109
110 # process this if it's a protocol request
110 # process this if it's a protocol request
111 # protocol bits don't need to create any URLs
111 # protocol bits don't need to create any URLs
112 # and the clients always use the old URL structure
112 # and the clients always use the old URL structure
113
113
114 cmd = req.form.get('cmd', [''])[0]
114 cmd = req.form.get('cmd', [''])[0]
115 if protocol.iscmd(cmd):
115 if protocol.iscmd(cmd):
116 if query:
116 if query:
117 raise ErrorResponse(HTTP_NOT_FOUND)
117 raise ErrorResponse(HTTP_NOT_FOUND)
118 if cmd in perms:
118 if cmd in perms:
119 try:
119 try:
120 self.check_perm(req, perms[cmd])
120 self.check_perm(req, perms[cmd])
121 except ErrorResponse, inst:
121 except ErrorResponse, inst:
122 if cmd == 'unbundle':
122 if cmd == 'unbundle':
123 req.drain()
123 req.drain()
124 req.respond(inst, protocol.HGTYPE)
124 req.respond(inst, protocol.HGTYPE)
125 return '0\n%s\n' % inst.message
125 return '0\n%s\n' % inst.message
126 return protocol.call(self.repo, req, cmd)
126 return protocol.call(self.repo, req, cmd)
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 caching(self, req) # sets ETag header or raises NOT_MODIFIED
181 caching(self, req) # sets ETag header or raises NOT_MODIFIED
182 if cmd not in webcommands.__all__:
182 if cmd not in webcommands.__all__:
183 msg = 'no such method: %s' % cmd
183 msg = 'no such method: %s' % cmd
184 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
184 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
185 elif cmd == 'file' and 'raw' in req.form.get('style', []):
185 elif cmd == 'file' and 'raw' in req.form.get('style', []):
186 self.ctype = ctype
186 self.ctype = ctype
187 content = webcommands.rawfile(self, req, tmpl)
187 content = webcommands.rawfile(self, req, tmpl)
188 else:
188 else:
189 content = getattr(webcommands, cmd)(self, req, tmpl)
189 content = getattr(webcommands, cmd)(self, req, tmpl)
190 req.respond(HTTP_OK, ctype)
190 req.respond(HTTP_OK, ctype)
191
191
192 return content
192 return content
193
193
194 except error.LookupError, err:
194 except error.LookupError, err:
195 req.respond(HTTP_NOT_FOUND, ctype)
195 req.respond(HTTP_NOT_FOUND, ctype)
196 msg = str(err)
196 msg = str(err)
197 if 'manifest' not in msg:
197 if 'manifest' not in msg:
198 msg = 'revision not found: %s' % err.name
198 msg = 'revision not found: %s' % err.name
199 return tmpl('error', error=msg)
199 return tmpl('error', error=msg)
200 except (error.RepoError, error.RevlogError), inst:
200 except (error.RepoError, error.RevlogError), inst:
201 req.respond(HTTP_SERVER_ERROR, ctype)
201 req.respond(HTTP_SERVER_ERROR, ctype)
202 return tmpl('error', error=str(inst))
202 return tmpl('error', error=str(inst))
203 except ErrorResponse, inst:
203 except ErrorResponse, inst:
204 req.respond(inst, ctype)
204 req.respond(inst, ctype)
205 return tmpl('error', error=inst.message)
205 return tmpl('error', error=inst.message)
206
206
207 def templater(self, req):
207 def templater(self, req):
208
208
209 # determine scheme, port and server name
209 # determine scheme, port and server name
210 # this is needed to create absolute urls
210 # this is needed to create absolute urls
211
211
212 proto = req.env.get('wsgi.url_scheme')
212 proto = req.env.get('wsgi.url_scheme')
213 if proto == 'https':
213 if proto == 'https':
214 proto = 'https'
214 proto = 'https'
215 default_port = "443"
215 default_port = "443"
216 else:
216 else:
217 proto = 'http'
217 proto = 'http'
218 default_port = "80"
218 default_port = "80"
219
219
220 port = req.env["SERVER_PORT"]
220 port = req.env["SERVER_PORT"]
221 port = port != default_port and (":" + port) or ""
221 port = port != default_port and (":" + port) or ""
222 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
222 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
223 staticurl = self.config("web", "staticurl") or req.url + 'static/'
223 staticurl = self.config("web", "staticurl") or req.url + 'static/'
224 if not staticurl.endswith('/'):
224 if not staticurl.endswith('/'):
225 staticurl += '/'
225 staticurl += '/'
226
226
227 # some functions for the templater
227 # some functions for the templater
228
228
229 def header(**map):
229 def header(**map):
230 yield tmpl('header', encoding=encoding.encoding, **map)
230 yield tmpl('header', encoding=encoding.encoding, **map)
231
231
232 def footer(**map):
232 def footer(**map):
233 yield tmpl("footer", **map)
233 yield tmpl("footer", **map)
234
234
235 def motd(**map):
235 def motd(**map):
236 yield self.config("web", "motd", "")
236 yield self.config("web", "motd", "")
237
237
238 # figure out which style to use
238 # figure out which style to use
239
239
240 vars = {}
240 vars = {}
241 styles = (
241 styles = (
242 req.form.get('style', [None])[0],
242 req.form.get('style', [None])[0],
243 self.config('web', 'style'),
243 self.config('web', 'style'),
244 'paper',
244 'paper',
245 )
245 )
246 style, mapfile = templater.stylemap(styles, self.templatepath)
246 style, mapfile = templater.stylemap(styles, self.templatepath)
247 if style == styles[0]:
247 if style == styles[0]:
248 vars['style'] = style
248 vars['style'] = style
249
249
250 start = req.url[-1] == '?' and '&' or '?'
250 start = req.url[-1] == '?' and '&' or '?'
251 sessionvars = webutil.sessionvars(vars, start)
251 sessionvars = webutil.sessionvars(vars, start)
252
252
253 if not self.reponame:
253 if not self.reponame:
254 self.reponame = (self.config("web", "name")
254 self.reponame = (self.config("web", "name")
255 or req.env.get('REPO_NAME')
255 or req.env.get('REPO_NAME')
256 or req.url.strip('/') or self.repo.root)
256 or req.url.strip('/') or self.repo.root)
257
257
258 # create the templater
258 # create the templater
259
259
260 tmpl = templater.templater(mapfile,
260 tmpl = templater.templater(mapfile,
261 defaults={"url": req.url,
261 defaults={"url": req.url,
262 "staticurl": staticurl,
262 "staticurl": staticurl,
263 "urlbase": urlbase,
263 "urlbase": urlbase,
264 "repo": self.reponame,
264 "repo": self.reponame,
265 "header": header,
265 "header": header,
266 "footer": footer,
266 "footer": footer,
267 "motd": motd,
267 "motd": motd,
268 "sessionvars": sessionvars
268 "sessionvars": sessionvars
269 })
269 })
270 return tmpl
270 return tmpl
271
271
272 def archivelist(self, nodeid):
272 def archivelist(self, nodeid):
273 allowed = self.configlist("web", "allow_archive")
273 allowed = self.configlist("web", "allow_archive")
274 for i, spec in self.archive_specs.iteritems():
274 for i, spec in self.archive_specs.iteritems():
275 if i in allowed or self.configbool("web", "allow" + i):
275 if i in allowed or self.configbool("web", "allow" + i):
276 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
276 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
277
277
278 archive_specs = {
278 archive_specs = {
279 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
279 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
280 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
280 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
281 'zip': ('application/zip', 'zip', '.zip', None),
281 'zip': ('application/zip', 'zip', '.zip', None),
282 }
282 }
283
283
284 def check_perm(self, req, op):
284 def check_perm(self, req, op):
285 for hook in permhooks:
285 for hook in permhooks:
286 hook(self, req, op)
286 hook(self, req, op)
@@ -1,356 +1,356 b''
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import os, re, time, urlparse
9 import os, re, time, urlparse
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(os.path.abspath(roothead))
35 roothead = os.path.normpath(os.path.abspath(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.motd = None
51 self.motd = None
52 self.refresh()
52 self.refresh()
53
53
54 def refresh(self):
54 def refresh(self):
55 if self.lastrefresh + self.refreshinterval > time.time():
55 if self.lastrefresh + self.refreshinterval > time.time():
56 return
56 return
57
57
58 if self.baseui:
58 if self.baseui:
59 u = self.baseui.copy()
59 u = self.baseui.copy()
60 else:
60 else:
61 u = webutil.wsgiui()
61 u = ui.ui()
62 u.setconfig('ui', 'report_untrusted', 'off')
62 u.setconfig('ui', 'report_untrusted', 'off')
63 u.setconfig('ui', 'interactive', 'off')
63 u.setconfig('ui', 'interactive', 'off')
64
64
65 if not isinstance(self.conf, (dict, list, tuple)):
65 if not isinstance(self.conf, (dict, list, tuple)):
66 map = {'paths': 'hgweb-paths'}
66 map = {'paths': 'hgweb-paths'}
67 u.readconfig(self.conf, remap=map, trust=True)
67 u.readconfig(self.conf, remap=map, trust=True)
68 paths = u.configitems('hgweb-paths')
68 paths = u.configitems('hgweb-paths')
69 elif isinstance(self.conf, (list, tuple)):
69 elif isinstance(self.conf, (list, tuple)):
70 paths = self.conf
70 paths = self.conf
71 elif isinstance(self.conf, dict):
71 elif isinstance(self.conf, dict):
72 paths = self.conf.items()
72 paths = self.conf.items()
73
73
74 repos = findrepos(paths)
74 repos = findrepos(paths)
75 for prefix, root in u.configitems('collections'):
75 for prefix, root in u.configitems('collections'):
76 prefix = util.pconvert(prefix)
76 prefix = util.pconvert(prefix)
77 for path in util.walkrepos(root, followsym=True):
77 for path in util.walkrepos(root, followsym=True):
78 repo = os.path.normpath(path)
78 repo = os.path.normpath(path)
79 name = util.pconvert(repo)
79 name = util.pconvert(repo)
80 if name.startswith(prefix):
80 if name.startswith(prefix):
81 name = name[len(prefix):]
81 name = name[len(prefix):]
82 repos.append((name.lstrip('/'), repo))
82 repos.append((name.lstrip('/'), repo))
83
83
84 self.repos = repos
84 self.repos = repos
85 self.ui = u
85 self.ui = u
86 encoding.encoding = self.ui.config('web', 'encoding',
86 encoding.encoding = self.ui.config('web', 'encoding',
87 encoding.encoding)
87 encoding.encoding)
88 self.style = self.ui.config('web', 'style', 'paper')
88 self.style = self.ui.config('web', 'style', 'paper')
89 self.templatepath = self.ui.config('web', 'templates', None)
89 self.templatepath = self.ui.config('web', 'templates', None)
90 self.stripecount = self.ui.config('web', 'stripes', 1)
90 self.stripecount = self.ui.config('web', 'stripes', 1)
91 if self.stripecount:
91 if self.stripecount:
92 self.stripecount = int(self.stripecount)
92 self.stripecount = int(self.stripecount)
93 self._baseurl = self.ui.config('web', 'baseurl')
93 self._baseurl = self.ui.config('web', 'baseurl')
94 self.lastrefresh = time.time()
94 self.lastrefresh = time.time()
95
95
96 def run(self):
96 def run(self):
97 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
97 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
98 raise RuntimeError("This function is only intended to be "
98 raise RuntimeError("This function is only intended to be "
99 "called while running as a CGI script.")
99 "called while running as a CGI script.")
100 import mercurial.hgweb.wsgicgi as wsgicgi
100 import mercurial.hgweb.wsgicgi as wsgicgi
101 wsgicgi.launch(self)
101 wsgicgi.launch(self)
102
102
103 def __call__(self, env, respond):
103 def __call__(self, env, respond):
104 req = wsgirequest(env, respond)
104 req = wsgirequest(env, respond)
105 return self.run_wsgi(req)
105 return self.run_wsgi(req)
106
106
107 def read_allowed(self, ui, req):
107 def read_allowed(self, ui, req):
108 """Check allow_read and deny_read config options of a repo's ui object
108 """Check allow_read and deny_read config options of a repo's ui object
109 to determine user permissions. By default, with neither option set (or
109 to determine user permissions. By default, with neither option set (or
110 both empty), allow all users to read the repo. There are two ways a
110 both empty), allow all users to read the repo. There are two ways a
111 user can be denied read access: (1) deny_read is not empty, and the
111 user can be denied read access: (1) deny_read is not empty, and the
112 user is unauthenticated or deny_read contains user (or *), and (2)
112 user is unauthenticated or deny_read contains user (or *), and (2)
113 allow_read is not empty and the user is not in allow_read. Return True
113 allow_read is not empty and the user is not in allow_read. Return True
114 if user is allowed to read the repo, else return False."""
114 if user is allowed to read the repo, else return False."""
115
115
116 user = req.env.get('REMOTE_USER')
116 user = req.env.get('REMOTE_USER')
117
117
118 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
118 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
119 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
119 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
120 return False
120 return False
121
121
122 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
122 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
123 # by default, allow reading if no allow_read option has been set
123 # by default, allow reading if no allow_read option has been set
124 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
124 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
125 return True
125 return True
126
126
127 return False
127 return False
128
128
129 def run_wsgi(self, req):
129 def run_wsgi(self, req):
130 try:
130 try:
131 try:
131 try:
132 self.refresh()
132 self.refresh()
133
133
134 virtual = req.env.get("PATH_INFO", "").strip('/')
134 virtual = req.env.get("PATH_INFO", "").strip('/')
135 tmpl = self.templater(req)
135 tmpl = self.templater(req)
136 ctype = tmpl('mimetype', encoding=encoding.encoding)
136 ctype = tmpl('mimetype', encoding=encoding.encoding)
137 ctype = templater.stringify(ctype)
137 ctype = templater.stringify(ctype)
138
138
139 # a static file
139 # a static file
140 if virtual.startswith('static/') or 'static' in req.form:
140 if virtual.startswith('static/') or 'static' in req.form:
141 if virtual.startswith('static/'):
141 if virtual.startswith('static/'):
142 fname = virtual[7:]
142 fname = virtual[7:]
143 else:
143 else:
144 fname = req.form['static'][0]
144 fname = req.form['static'][0]
145 static = templater.templatepath('static')
145 static = templater.templatepath('static')
146 return (staticfile(static, fname, req),)
146 return (staticfile(static, fname, req),)
147
147
148 # top-level index
148 # top-level index
149 elif not virtual:
149 elif not virtual:
150 req.respond(HTTP_OK, ctype)
150 req.respond(HTTP_OK, ctype)
151 return self.makeindex(req, tmpl)
151 return self.makeindex(req, tmpl)
152
152
153 # nested indexes and hgwebs
153 # nested indexes and hgwebs
154
154
155 repos = dict(self.repos)
155 repos = dict(self.repos)
156 while virtual:
156 while virtual:
157 real = repos.get(virtual)
157 real = repos.get(virtual)
158 if real:
158 if real:
159 req.env['REPO_NAME'] = virtual
159 req.env['REPO_NAME'] = virtual
160 try:
160 try:
161 repo = hg.repository(self.ui, real)
161 repo = hg.repository(self.ui, real)
162 return hgweb(repo).run_wsgi(req)
162 return hgweb(repo).run_wsgi(req)
163 except IOError, inst:
163 except IOError, inst:
164 msg = inst.strerror
164 msg = inst.strerror
165 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
165 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
166 except error.RepoError, inst:
166 except error.RepoError, inst:
167 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
167 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
168
168
169 # browse subdirectories
169 # browse subdirectories
170 subdir = virtual + '/'
170 subdir = virtual + '/'
171 if [r for r in repos if r.startswith(subdir)]:
171 if [r for r in repos if r.startswith(subdir)]:
172 req.respond(HTTP_OK, ctype)
172 req.respond(HTTP_OK, ctype)
173 return self.makeindex(req, tmpl, subdir)
173 return self.makeindex(req, tmpl, subdir)
174
174
175 up = virtual.rfind('/')
175 up = virtual.rfind('/')
176 if up < 0:
176 if up < 0:
177 break
177 break
178 virtual = virtual[:up]
178 virtual = virtual[:up]
179
179
180 # prefixes not found
180 # prefixes not found
181 req.respond(HTTP_NOT_FOUND, ctype)
181 req.respond(HTTP_NOT_FOUND, ctype)
182 return tmpl("notfound", repo=virtual)
182 return tmpl("notfound", repo=virtual)
183
183
184 except ErrorResponse, err:
184 except ErrorResponse, err:
185 req.respond(err, ctype)
185 req.respond(err, ctype)
186 return tmpl('error', error=err.message or '')
186 return tmpl('error', error=err.message or '')
187 finally:
187 finally:
188 tmpl = None
188 tmpl = None
189
189
190 def makeindex(self, req, tmpl, subdir=""):
190 def makeindex(self, req, tmpl, subdir=""):
191
191
192 def archivelist(ui, nodeid, url):
192 def archivelist(ui, nodeid, url):
193 allowed = ui.configlist("web", "allow_archive", untrusted=True)
193 allowed = ui.configlist("web", "allow_archive", untrusted=True)
194 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
194 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
195 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
195 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
196 untrusted=True):
196 untrusted=True):
197 yield {"type" : i[0], "extension": i[1],
197 yield {"type" : i[0], "extension": i[1],
198 "node": nodeid, "url": url}
198 "node": nodeid, "url": url}
199
199
200 def rawentries(subdir="", **map):
200 def rawentries(subdir="", **map):
201
201
202 descend = self.ui.configbool('web', 'descend', True)
202 descend = self.ui.configbool('web', 'descend', True)
203 for name, path in self.repos:
203 for name, path in self.repos:
204
204
205 if not name.startswith(subdir):
205 if not name.startswith(subdir):
206 continue
206 continue
207 name = name[len(subdir):]
207 name = name[len(subdir):]
208 if not descend and '/' in name:
208 if not descend and '/' in name:
209 continue
209 continue
210
210
211 u = self.ui.copy()
211 u = self.ui.copy()
212 try:
212 try:
213 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
213 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
214 except Exception, e:
214 except Exception, e:
215 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
215 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
216 continue
216 continue
217 def get(section, name, default=None):
217 def get(section, name, default=None):
218 return u.config(section, name, default, untrusted=True)
218 return u.config(section, name, default, untrusted=True)
219
219
220 if u.configbool("web", "hidden", untrusted=True):
220 if u.configbool("web", "hidden", untrusted=True):
221 continue
221 continue
222
222
223 if not self.read_allowed(u, req):
223 if not self.read_allowed(u, req):
224 continue
224 continue
225
225
226 parts = [name]
226 parts = [name]
227 if 'PATH_INFO' in req.env:
227 if 'PATH_INFO' in req.env:
228 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
228 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
229 if req.env['SCRIPT_NAME']:
229 if req.env['SCRIPT_NAME']:
230 parts.insert(0, req.env['SCRIPT_NAME'])
230 parts.insert(0, req.env['SCRIPT_NAME'])
231 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
231 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
232
232
233 # update time with local timezone
233 # update time with local timezone
234 try:
234 try:
235 r = hg.repository(self.ui, path)
235 r = hg.repository(self.ui, path)
236 except error.RepoError:
236 except error.RepoError:
237 u.warn(_('error accessing repository at %s\n') % path)
237 u.warn(_('error accessing repository at %s\n') % path)
238 continue
238 continue
239 try:
239 try:
240 d = (get_mtime(r.spath), util.makedate()[1])
240 d = (get_mtime(r.spath), util.makedate()[1])
241 except OSError:
241 except OSError:
242 continue
242 continue
243
243
244 contact = get_contact(get)
244 contact = get_contact(get)
245 description = get("web", "description", "")
245 description = get("web", "description", "")
246 name = get("web", "name", name)
246 name = get("web", "name", name)
247 row = dict(contact=contact or "unknown",
247 row = dict(contact=contact or "unknown",
248 contact_sort=contact.upper() or "unknown",
248 contact_sort=contact.upper() or "unknown",
249 name=name,
249 name=name,
250 name_sort=name,
250 name_sort=name,
251 url=url,
251 url=url,
252 description=description or "unknown",
252 description=description or "unknown",
253 description_sort=description.upper() or "unknown",
253 description_sort=description.upper() or "unknown",
254 lastchange=d,
254 lastchange=d,
255 lastchange_sort=d[1]-d[0],
255 lastchange_sort=d[1]-d[0],
256 archives=archivelist(u, "tip", url))
256 archives=archivelist(u, "tip", url))
257 yield row
257 yield row
258
258
259 sortdefault = None, False
259 sortdefault = None, False
260 def entries(sortcolumn="", descending=False, subdir="", **map):
260 def entries(sortcolumn="", descending=False, subdir="", **map):
261 rows = rawentries(subdir=subdir, **map)
261 rows = rawentries(subdir=subdir, **map)
262
262
263 if sortcolumn and sortdefault != (sortcolumn, descending):
263 if sortcolumn and sortdefault != (sortcolumn, descending):
264 sortkey = '%s_sort' % sortcolumn
264 sortkey = '%s_sort' % sortcolumn
265 rows = sorted(rows, key=lambda x: x[sortkey],
265 rows = sorted(rows, key=lambda x: x[sortkey],
266 reverse=descending)
266 reverse=descending)
267 for row, parity in zip(rows, paritygen(self.stripecount)):
267 for row, parity in zip(rows, paritygen(self.stripecount)):
268 row['parity'] = parity
268 row['parity'] = parity
269 yield row
269 yield row
270
270
271 self.refresh()
271 self.refresh()
272 sortable = ["name", "description", "contact", "lastchange"]
272 sortable = ["name", "description", "contact", "lastchange"]
273 sortcolumn, descending = sortdefault
273 sortcolumn, descending = sortdefault
274 if 'sort' in req.form:
274 if 'sort' in req.form:
275 sortcolumn = req.form['sort'][0]
275 sortcolumn = req.form['sort'][0]
276 descending = sortcolumn.startswith('-')
276 descending = sortcolumn.startswith('-')
277 if descending:
277 if descending:
278 sortcolumn = sortcolumn[1:]
278 sortcolumn = sortcolumn[1:]
279 if sortcolumn not in sortable:
279 if sortcolumn not in sortable:
280 sortcolumn = ""
280 sortcolumn = ""
281
281
282 sort = [("sort_%s" % column,
282 sort = [("sort_%s" % column,
283 "%s%s" % ((not descending and column == sortcolumn)
283 "%s%s" % ((not descending and column == sortcolumn)
284 and "-" or "", column))
284 and "-" or "", column))
285 for column in sortable]
285 for column in sortable]
286
286
287 self.refresh()
287 self.refresh()
288 self.updatereqenv(req.env)
288 self.updatereqenv(req.env)
289
289
290 return tmpl("index", entries=entries, subdir=subdir,
290 return tmpl("index", entries=entries, subdir=subdir,
291 sortcolumn=sortcolumn, descending=descending,
291 sortcolumn=sortcolumn, descending=descending,
292 **dict(sort))
292 **dict(sort))
293
293
294 def templater(self, req):
294 def templater(self, req):
295
295
296 def header(**map):
296 def header(**map):
297 yield tmpl('header', encoding=encoding.encoding, **map)
297 yield tmpl('header', encoding=encoding.encoding, **map)
298
298
299 def footer(**map):
299 def footer(**map):
300 yield tmpl("footer", **map)
300 yield tmpl("footer", **map)
301
301
302 def motd(**map):
302 def motd(**map):
303 if self.motd is not None:
303 if self.motd is not None:
304 yield self.motd
304 yield self.motd
305 else:
305 else:
306 yield config('web', 'motd', '')
306 yield config('web', 'motd', '')
307
307
308 def config(section, name, default=None, untrusted=True):
308 def config(section, name, default=None, untrusted=True):
309 return self.ui.config(section, name, default, untrusted)
309 return self.ui.config(section, name, default, untrusted)
310
310
311 self.updatereqenv(req.env)
311 self.updatereqenv(req.env)
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 styles = (
318 styles = (
319 req.form.get('style', [None])[0],
319 req.form.get('style', [None])[0],
320 config('web', 'style'),
320 config('web', 'style'),
321 'paper'
321 'paper'
322 )
322 )
323 style, mapfile = templater.stylemap(styles, self.templatepath)
323 style, mapfile = templater.stylemap(styles, self.templatepath)
324 if style == styles[0]:
324 if style == styles[0]:
325 vars['style'] = style
325 vars['style'] = style
326
326
327 start = url[-1] == '?' and '&' or '?'
327 start = url[-1] == '?' and '&' or '?'
328 sessionvars = webutil.sessionvars(vars, start)
328 sessionvars = webutil.sessionvars(vars, start)
329 staticurl = config('web', 'staticurl') or url + 'static/'
329 staticurl = config('web', 'staticurl') or url + 'static/'
330 if not staticurl.endswith('/'):
330 if not staticurl.endswith('/'):
331 staticurl += '/'
331 staticurl += '/'
332
332
333 tmpl = templater.templater(mapfile,
333 tmpl = templater.templater(mapfile,
334 defaults={"header": header,
334 defaults={"header": header,
335 "footer": footer,
335 "footer": footer,
336 "motd": motd,
336 "motd": motd,
337 "url": url,
337 "url": url,
338 "staticurl": staticurl,
338 "staticurl": staticurl,
339 "sessionvars": sessionvars})
339 "sessionvars": sessionvars})
340 return tmpl
340 return tmpl
341
341
342 def updatereqenv(self, env):
342 def updatereqenv(self, env):
343 def splitnetloc(netloc):
343 def splitnetloc(netloc):
344 if ':' in netloc:
344 if ':' in netloc:
345 return netloc.split(':', 1)
345 return netloc.split(':', 1)
346 else:
346 else:
347 return (netloc, None)
347 return (netloc, None)
348
348
349 if self._baseurl is not None:
349 if self._baseurl is not None:
350 urlcomp = urlparse.urlparse(self._baseurl)
350 urlcomp = urlparse.urlparse(self._baseurl)
351 host, port = splitnetloc(urlcomp[1])
351 host, port = splitnetloc(urlcomp[1])
352 path = urlcomp[2]
352 path = urlcomp[2]
353 env['SERVER_NAME'] = host
353 env['SERVER_NAME'] = host
354 if port:
354 if port:
355 env['SERVER_PORT'] = port
355 env['SERVER_PORT'] = port
356 env['SCRIPT_NAME'] = path
356 env['SCRIPT_NAME'] = path
@@ -1,783 +1,783 b''
1 #
1 #
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 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 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import os, mimetypes, re, cgi, copy
8 import os, mimetypes, re, cgi, copy
9 import webutil
9 import webutil
10 from mercurial import error, encoding, archival, templater, templatefilters
10 from mercurial import error, encoding, archival, templater, templatefilters
11 from mercurial.node import short, hex
11 from mercurial.node import short, hex
12 from mercurial.util import binary
12 from mercurial.util import binary
13 from common import paritygen, staticfile, get_contact, ErrorResponse
13 from common import paritygen, staticfile, get_contact, ErrorResponse
14 from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND
14 from common import HTTP_OK, HTTP_FORBIDDEN, HTTP_NOT_FOUND
15 from mercurial import graphmod
15 from mercurial import graphmod
16 from mercurial import help as helpmod
16 from mercurial import help as helpmod
17 from mercurial.i18n import _
17 from mercurial.i18n import _
18
18
19 # __all__ is populated with the allowed commands. Be sure to add to it if
19 # __all__ is populated with the allowed commands. Be sure to add to it if
20 # you're adding a new command, or the new command won't work.
20 # you're adding a new command, or the new command won't work.
21
21
22 __all__ = [
22 __all__ = [
23 'log', 'rawfile', 'file', 'changelog', 'shortlog', 'changeset', 'rev',
23 'log', 'rawfile', 'file', 'changelog', 'shortlog', 'changeset', 'rev',
24 'manifest', 'tags', 'branches', 'summary', 'filediff', 'diff', 'annotate',
24 'manifest', 'tags', 'branches', 'summary', 'filediff', 'diff', 'annotate',
25 'filelog', 'archive', 'static', 'graph', 'help',
25 'filelog', 'archive', 'static', 'graph', 'help',
26 ]
26 ]
27
27
28 def log(web, req, tmpl):
28 def log(web, req, tmpl):
29 if 'file' in req.form and req.form['file'][0]:
29 if 'file' in req.form and req.form['file'][0]:
30 return filelog(web, req, tmpl)
30 return filelog(web, req, tmpl)
31 else:
31 else:
32 return changelog(web, req, tmpl)
32 return changelog(web, req, tmpl)
33
33
34 def rawfile(web, req, tmpl):
34 def rawfile(web, req, tmpl):
35 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
35 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
36 if not path:
36 if not path:
37 content = manifest(web, req, tmpl)
37 content = manifest(web, req, tmpl)
38 req.respond(HTTP_OK, web.ctype)
38 req.respond(HTTP_OK, web.ctype)
39 return content
39 return content
40
40
41 try:
41 try:
42 fctx = webutil.filectx(web.repo, req)
42 fctx = webutil.filectx(web.repo, req)
43 except error.LookupError, inst:
43 except error.LookupError, inst:
44 try:
44 try:
45 content = manifest(web, req, tmpl)
45 content = manifest(web, req, tmpl)
46 req.respond(HTTP_OK, web.ctype)
46 req.respond(HTTP_OK, web.ctype)
47 return content
47 return content
48 except ErrorResponse:
48 except ErrorResponse:
49 raise inst
49 raise inst
50
50
51 path = fctx.path()
51 path = fctx.path()
52 text = fctx.data()
52 text = fctx.data()
53 mt = mimetypes.guess_type(path)[0]
53 mt = mimetypes.guess_type(path)[0]
54 if mt is None:
54 if mt is None:
55 mt = binary(text) and 'application/octet-stream' or 'text/plain'
55 mt = binary(text) and 'application/octet-stream' or 'text/plain'
56 if mt.startswith('text/'):
56 if mt.startswith('text/'):
57 mt += '; charset="%s"' % encoding.encoding
57 mt += '; charset="%s"' % encoding.encoding
58
58
59 req.respond(HTTP_OK, mt, path, len(text))
59 req.respond(HTTP_OK, mt, path, len(text))
60 return [text]
60 return [text]
61
61
62 def _filerevision(web, tmpl, fctx):
62 def _filerevision(web, tmpl, fctx):
63 f = fctx.path()
63 f = fctx.path()
64 text = fctx.data()
64 text = fctx.data()
65 parity = paritygen(web.stripecount)
65 parity = paritygen(web.stripecount)
66
66
67 if binary(text):
67 if binary(text):
68 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
68 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
69 text = '(binary:%s)' % mt
69 text = '(binary:%s)' % mt
70
70
71 def lines():
71 def lines():
72 for lineno, t in enumerate(text.splitlines(True)):
72 for lineno, t in enumerate(text.splitlines(True)):
73 yield {"line": t,
73 yield {"line": t,
74 "lineid": "l%d" % (lineno + 1),
74 "lineid": "l%d" % (lineno + 1),
75 "linenumber": "% 6d" % (lineno + 1),
75 "linenumber": "% 6d" % (lineno + 1),
76 "parity": parity.next()}
76 "parity": parity.next()}
77
77
78 return tmpl("filerevision",
78 return tmpl("filerevision",
79 file=f,
79 file=f,
80 path=webutil.up(f),
80 path=webutil.up(f),
81 text=lines(),
81 text=lines(),
82 rev=fctx.rev(),
82 rev=fctx.rev(),
83 node=hex(fctx.node()),
83 node=hex(fctx.node()),
84 author=fctx.user(),
84 author=fctx.user(),
85 date=fctx.date(),
85 date=fctx.date(),
86 desc=fctx.description(),
86 desc=fctx.description(),
87 branch=webutil.nodebranchnodefault(fctx),
87 branch=webutil.nodebranchnodefault(fctx),
88 parent=webutil.parents(fctx),
88 parent=webutil.parents(fctx),
89 child=webutil.children(fctx),
89 child=webutil.children(fctx),
90 rename=webutil.renamelink(fctx),
90 rename=webutil.renamelink(fctx),
91 permissions=fctx.manifest().flags(f))
91 permissions=fctx.manifest().flags(f))
92
92
93 def file(web, req, tmpl):
93 def file(web, req, tmpl):
94 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
94 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
95 if not path:
95 if not path:
96 return manifest(web, req, tmpl)
96 return manifest(web, req, tmpl)
97 try:
97 try:
98 return _filerevision(web, tmpl, webutil.filectx(web.repo, req))
98 return _filerevision(web, tmpl, webutil.filectx(web.repo, req))
99 except error.LookupError, inst:
99 except error.LookupError, inst:
100 try:
100 try:
101 return manifest(web, req, tmpl)
101 return manifest(web, req, tmpl)
102 except ErrorResponse:
102 except ErrorResponse:
103 raise inst
103 raise inst
104
104
105 def _search(web, req, tmpl):
105 def _search(web, req, tmpl):
106
106
107 query = req.form['rev'][0]
107 query = req.form['rev'][0]
108 revcount = web.maxchanges
108 revcount = web.maxchanges
109 if 'revcount' in req.form:
109 if 'revcount' in req.form:
110 revcount = int(req.form.get('revcount', [revcount])[0])
110 revcount = int(req.form.get('revcount', [revcount])[0])
111 tmpl.defaults['sessionvars']['revcount'] = revcount
111 tmpl.defaults['sessionvars']['revcount'] = revcount
112
112
113 lessvars = copy.copy(tmpl.defaults['sessionvars'])
113 lessvars = copy.copy(tmpl.defaults['sessionvars'])
114 lessvars['revcount'] = revcount / 2
114 lessvars['revcount'] = revcount / 2
115 lessvars['rev'] = query
115 lessvars['rev'] = query
116 morevars = copy.copy(tmpl.defaults['sessionvars'])
116 morevars = copy.copy(tmpl.defaults['sessionvars'])
117 morevars['revcount'] = revcount * 2
117 morevars['revcount'] = revcount * 2
118 morevars['rev'] = query
118 morevars['rev'] = query
119
119
120 def changelist(**map):
120 def changelist(**map):
121 count = 0
121 count = 0
122 qw = query.lower().split()
122 qw = query.lower().split()
123
123
124 def revgen():
124 def revgen():
125 for i in xrange(len(web.repo) - 1, 0, -100):
125 for i in xrange(len(web.repo) - 1, 0, -100):
126 l = []
126 l = []
127 for j in xrange(max(0, i - 100), i + 1):
127 for j in xrange(max(0, i - 100), i + 1):
128 ctx = web.repo[j]
128 ctx = web.repo[j]
129 l.append(ctx)
129 l.append(ctx)
130 l.reverse()
130 l.reverse()
131 for e in l:
131 for e in l:
132 yield e
132 yield e
133
133
134 for ctx in revgen():
134 for ctx in revgen():
135 miss = 0
135 miss = 0
136 for q in qw:
136 for q in qw:
137 if not (q in ctx.user().lower() or
137 if not (q in ctx.user().lower() or
138 q in ctx.description().lower() or
138 q in ctx.description().lower() or
139 q in " ".join(ctx.files()).lower()):
139 q in " ".join(ctx.files()).lower()):
140 miss = 1
140 miss = 1
141 break
141 break
142 if miss:
142 if miss:
143 continue
143 continue
144
144
145 count += 1
145 count += 1
146 n = ctx.node()
146 n = ctx.node()
147 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
147 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
148 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
148 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
149
149
150 yield tmpl('searchentry',
150 yield tmpl('searchentry',
151 parity=parity.next(),
151 parity=parity.next(),
152 author=ctx.user(),
152 author=ctx.user(),
153 parent=webutil.parents(ctx),
153 parent=webutil.parents(ctx),
154 child=webutil.children(ctx),
154 child=webutil.children(ctx),
155 changelogtag=showtags,
155 changelogtag=showtags,
156 desc=ctx.description(),
156 desc=ctx.description(),
157 date=ctx.date(),
157 date=ctx.date(),
158 files=files,
158 files=files,
159 rev=ctx.rev(),
159 rev=ctx.rev(),
160 node=hex(n),
160 node=hex(n),
161 tags=webutil.nodetagsdict(web.repo, n),
161 tags=webutil.nodetagsdict(web.repo, n),
162 inbranch=webutil.nodeinbranch(web.repo, ctx),
162 inbranch=webutil.nodeinbranch(web.repo, ctx),
163 branches=webutil.nodebranchdict(web.repo, ctx))
163 branches=webutil.nodebranchdict(web.repo, ctx))
164
164
165 if count >= revcount:
165 if count >= revcount:
166 break
166 break
167
167
168 tip = web.repo['tip']
168 tip = web.repo['tip']
169 parity = paritygen(web.stripecount)
169 parity = paritygen(web.stripecount)
170
170
171 return tmpl('search', query=query, node=tip.hex(),
171 return tmpl('search', query=query, node=tip.hex(),
172 entries=changelist, archives=web.archivelist("tip"),
172 entries=changelist, archives=web.archivelist("tip"),
173 morevars=morevars, lessvars=lessvars)
173 morevars=morevars, lessvars=lessvars)
174
174
175 def changelog(web, req, tmpl, shortlog=False):
175 def changelog(web, req, tmpl, shortlog=False):
176
176
177 if 'node' in req.form:
177 if 'node' in req.form:
178 ctx = webutil.changectx(web.repo, req)
178 ctx = webutil.changectx(web.repo, req)
179 else:
179 else:
180 if 'rev' in req.form:
180 if 'rev' in req.form:
181 hi = req.form['rev'][0]
181 hi = req.form['rev'][0]
182 else:
182 else:
183 hi = len(web.repo) - 1
183 hi = len(web.repo) - 1
184 try:
184 try:
185 ctx = web.repo[hi]
185 ctx = web.repo[hi]
186 except error.RepoError:
186 except error.RepoError:
187 return _search(web, req, tmpl) # XXX redirect to 404 page?
187 return _search(web, req, tmpl) # XXX redirect to 404 page?
188
188
189 def changelist(limit=0, **map):
189 def changelist(limit=0, **map):
190 l = [] # build a list in forward order for efficiency
190 l = [] # build a list in forward order for efficiency
191 for i in xrange(start, end):
191 for i in xrange(start, end):
192 ctx = web.repo[i]
192 ctx = web.repo[i]
193 n = ctx.node()
193 n = ctx.node()
194 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
194 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
195 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
195 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
196
196
197 l.insert(0, {"parity": parity.next(),
197 l.insert(0, {"parity": parity.next(),
198 "author": ctx.user(),
198 "author": ctx.user(),
199 "parent": webutil.parents(ctx, i - 1),
199 "parent": webutil.parents(ctx, i - 1),
200 "child": webutil.children(ctx, i + 1),
200 "child": webutil.children(ctx, i + 1),
201 "changelogtag": showtags,
201 "changelogtag": showtags,
202 "desc": ctx.description(),
202 "desc": ctx.description(),
203 "date": ctx.date(),
203 "date": ctx.date(),
204 "files": files,
204 "files": files,
205 "rev": i,
205 "rev": i,
206 "node": hex(n),
206 "node": hex(n),
207 "tags": webutil.nodetagsdict(web.repo, n),
207 "tags": webutil.nodetagsdict(web.repo, n),
208 "inbranch": webutil.nodeinbranch(web.repo, ctx),
208 "inbranch": webutil.nodeinbranch(web.repo, ctx),
209 "branches": webutil.nodebranchdict(web.repo, ctx)
209 "branches": webutil.nodebranchdict(web.repo, ctx)
210 })
210 })
211
211
212 if limit > 0:
212 if limit > 0:
213 l = l[:limit]
213 l = l[:limit]
214
214
215 for e in l:
215 for e in l:
216 yield e
216 yield e
217
217
218 revcount = shortlog and web.maxshortchanges or web.maxchanges
218 revcount = shortlog and web.maxshortchanges or web.maxchanges
219 if 'revcount' in req.form:
219 if 'revcount' in req.form:
220 revcount = int(req.form.get('revcount', [revcount])[0])
220 revcount = int(req.form.get('revcount', [revcount])[0])
221 tmpl.defaults['sessionvars']['revcount'] = revcount
221 tmpl.defaults['sessionvars']['revcount'] = revcount
222
222
223 lessvars = copy.copy(tmpl.defaults['sessionvars'])
223 lessvars = copy.copy(tmpl.defaults['sessionvars'])
224 lessvars['revcount'] = revcount / 2
224 lessvars['revcount'] = revcount / 2
225 morevars = copy.copy(tmpl.defaults['sessionvars'])
225 morevars = copy.copy(tmpl.defaults['sessionvars'])
226 morevars['revcount'] = revcount * 2
226 morevars['revcount'] = revcount * 2
227
227
228 count = len(web.repo)
228 count = len(web.repo)
229 pos = ctx.rev()
229 pos = ctx.rev()
230 start = max(0, pos - revcount + 1)
230 start = max(0, pos - revcount + 1)
231 end = min(count, start + revcount)
231 end = min(count, start + revcount)
232 pos = end - 1
232 pos = end - 1
233 parity = paritygen(web.stripecount, offset=start - end)
233 parity = paritygen(web.stripecount, offset=start - end)
234
234
235 changenav = webutil.revnavgen(pos, revcount, count, web.repo.changectx)
235 changenav = webutil.revnavgen(pos, revcount, count, web.repo.changectx)
236
236
237 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
237 return tmpl(shortlog and 'shortlog' or 'changelog', changenav=changenav,
238 node=hex(ctx.node()), rev=pos, changesets=count,
238 node=hex(ctx.node()), rev=pos, changesets=count,
239 entries=lambda **x: changelist(limit=0,**x),
239 entries=lambda **x: changelist(limit=0,**x),
240 latestentry=lambda **x: changelist(limit=1,**x),
240 latestentry=lambda **x: changelist(limit=1,**x),
241 archives=web.archivelist("tip"), revcount=revcount,
241 archives=web.archivelist("tip"), revcount=revcount,
242 morevars=morevars, lessvars=lessvars)
242 morevars=morevars, lessvars=lessvars)
243
243
244 def shortlog(web, req, tmpl):
244 def shortlog(web, req, tmpl):
245 return changelog(web, req, tmpl, shortlog = True)
245 return changelog(web, req, tmpl, shortlog = True)
246
246
247 def changeset(web, req, tmpl):
247 def changeset(web, req, tmpl):
248 ctx = webutil.changectx(web.repo, req)
248 ctx = webutil.changectx(web.repo, req)
249 showtags = webutil.showtag(web.repo, tmpl, 'changesettag', ctx.node())
249 showtags = webutil.showtag(web.repo, tmpl, 'changesettag', ctx.node())
250 showbranch = webutil.nodebranchnodefault(ctx)
250 showbranch = webutil.nodebranchnodefault(ctx)
251
251
252 files = []
252 files = []
253 parity = paritygen(web.stripecount)
253 parity = paritygen(web.stripecount)
254 for f in ctx.files():
254 for f in ctx.files():
255 template = f in ctx and 'filenodelink' or 'filenolink'
255 template = f in ctx and 'filenodelink' or 'filenolink'
256 files.append(tmpl(template,
256 files.append(tmpl(template,
257 node=ctx.hex(), file=f,
257 node=ctx.hex(), file=f,
258 parity=parity.next()))
258 parity=parity.next()))
259
259
260 parity = paritygen(web.stripecount)
260 parity = paritygen(web.stripecount)
261 style = web.config('web', 'style', 'paper')
261 style = web.config('web', 'style', 'paper')
262 if 'style' in req.form:
262 if 'style' in req.form:
263 style = req.form['style'][0]
263 style = req.form['style'][0]
264
264
265 diffs = webutil.diffs(web.repo, tmpl, ctx, None, parity, style)
265 diffs = webutil.diffs(web.repo, tmpl, ctx, None, parity, style)
266 return tmpl('changeset',
266 return tmpl('changeset',
267 diff=diffs,
267 diff=diffs,
268 rev=ctx.rev(),
268 rev=ctx.rev(),
269 node=ctx.hex(),
269 node=ctx.hex(),
270 parent=webutil.parents(ctx),
270 parent=webutil.parents(ctx),
271 child=webutil.children(ctx),
271 child=webutil.children(ctx),
272 changesettag=showtags,
272 changesettag=showtags,
273 changesetbranch=showbranch,
273 changesetbranch=showbranch,
274 author=ctx.user(),
274 author=ctx.user(),
275 desc=ctx.description(),
275 desc=ctx.description(),
276 date=ctx.date(),
276 date=ctx.date(),
277 files=files,
277 files=files,
278 archives=web.archivelist(ctx.hex()),
278 archives=web.archivelist(ctx.hex()),
279 tags=webutil.nodetagsdict(web.repo, ctx.node()),
279 tags=webutil.nodetagsdict(web.repo, ctx.node()),
280 branch=webutil.nodebranchnodefault(ctx),
280 branch=webutil.nodebranchnodefault(ctx),
281 inbranch=webutil.nodeinbranch(web.repo, ctx),
281 inbranch=webutil.nodeinbranch(web.repo, ctx),
282 branches=webutil.nodebranchdict(web.repo, ctx))
282 branches=webutil.nodebranchdict(web.repo, ctx))
283
283
284 rev = changeset
284 rev = changeset
285
285
286 def manifest(web, req, tmpl):
286 def manifest(web, req, tmpl):
287 ctx = webutil.changectx(web.repo, req)
287 ctx = webutil.changectx(web.repo, req)
288 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
288 path = webutil.cleanpath(web.repo, req.form.get('file', [''])[0])
289 mf = ctx.manifest()
289 mf = ctx.manifest()
290 node = ctx.node()
290 node = ctx.node()
291
291
292 files = {}
292 files = {}
293 dirs = {}
293 dirs = {}
294 parity = paritygen(web.stripecount)
294 parity = paritygen(web.stripecount)
295
295
296 if path and path[-1] != "/":
296 if path and path[-1] != "/":
297 path += "/"
297 path += "/"
298 l = len(path)
298 l = len(path)
299 abspath = "/" + path
299 abspath = "/" + path
300
300
301 for f, n in mf.iteritems():
301 for f, n in mf.iteritems():
302 if f[:l] != path:
302 if f[:l] != path:
303 continue
303 continue
304 remain = f[l:]
304 remain = f[l:]
305 elements = remain.split('/')
305 elements = remain.split('/')
306 if len(elements) == 1:
306 if len(elements) == 1:
307 files[remain] = f
307 files[remain] = f
308 else:
308 else:
309 h = dirs # need to retain ref to dirs (root)
309 h = dirs # need to retain ref to dirs (root)
310 for elem in elements[0:-1]:
310 for elem in elements[0:-1]:
311 if elem not in h:
311 if elem not in h:
312 h[elem] = {}
312 h[elem] = {}
313 h = h[elem]
313 h = h[elem]
314 if len(h) > 1:
314 if len(h) > 1:
315 break
315 break
316 h[None] = None # denotes files present
316 h[None] = None # denotes files present
317
317
318 if mf and not files and not dirs:
318 if mf and not files and not dirs:
319 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
319 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
320
320
321 def filelist(**map):
321 def filelist(**map):
322 for f in sorted(files):
322 for f in sorted(files):
323 full = files[f]
323 full = files[f]
324
324
325 fctx = ctx.filectx(full)
325 fctx = ctx.filectx(full)
326 yield {"file": full,
326 yield {"file": full,
327 "parity": parity.next(),
327 "parity": parity.next(),
328 "basename": f,
328 "basename": f,
329 "date": fctx.date(),
329 "date": fctx.date(),
330 "size": fctx.size(),
330 "size": fctx.size(),
331 "permissions": mf.flags(full)}
331 "permissions": mf.flags(full)}
332
332
333 def dirlist(**map):
333 def dirlist(**map):
334 for d in sorted(dirs):
334 for d in sorted(dirs):
335
335
336 emptydirs = []
336 emptydirs = []
337 h = dirs[d]
337 h = dirs[d]
338 while isinstance(h, dict) and len(h) == 1:
338 while isinstance(h, dict) and len(h) == 1:
339 k, v = h.items()[0]
339 k, v = h.items()[0]
340 if v:
340 if v:
341 emptydirs.append(k)
341 emptydirs.append(k)
342 h = v
342 h = v
343
343
344 path = "%s%s" % (abspath, d)
344 path = "%s%s" % (abspath, d)
345 yield {"parity": parity.next(),
345 yield {"parity": parity.next(),
346 "path": path,
346 "path": path,
347 "emptydirs": "/".join(emptydirs),
347 "emptydirs": "/".join(emptydirs),
348 "basename": d}
348 "basename": d}
349
349
350 return tmpl("manifest",
350 return tmpl("manifest",
351 rev=ctx.rev(),
351 rev=ctx.rev(),
352 node=hex(node),
352 node=hex(node),
353 path=abspath,
353 path=abspath,
354 up=webutil.up(abspath),
354 up=webutil.up(abspath),
355 upparity=parity.next(),
355 upparity=parity.next(),
356 fentries=filelist,
356 fentries=filelist,
357 dentries=dirlist,
357 dentries=dirlist,
358 archives=web.archivelist(hex(node)),
358 archives=web.archivelist(hex(node)),
359 tags=webutil.nodetagsdict(web.repo, node),
359 tags=webutil.nodetagsdict(web.repo, node),
360 inbranch=webutil.nodeinbranch(web.repo, ctx),
360 inbranch=webutil.nodeinbranch(web.repo, ctx),
361 branches=webutil.nodebranchdict(web.repo, ctx))
361 branches=webutil.nodebranchdict(web.repo, ctx))
362
362
363 def tags(web, req, tmpl):
363 def tags(web, req, tmpl):
364 i = web.repo.tagslist()
364 i = web.repo.tagslist()
365 i.reverse()
365 i.reverse()
366 parity = paritygen(web.stripecount)
366 parity = paritygen(web.stripecount)
367
367
368 def entries(notip=False, limit=0, **map):
368 def entries(notip=False, limit=0, **map):
369 count = 0
369 count = 0
370 for k, n in i:
370 for k, n in i:
371 if notip and k == "tip":
371 if notip and k == "tip":
372 continue
372 continue
373 if limit > 0 and count >= limit:
373 if limit > 0 and count >= limit:
374 continue
374 continue
375 count = count + 1
375 count = count + 1
376 yield {"parity": parity.next(),
376 yield {"parity": parity.next(),
377 "tag": k,
377 "tag": k,
378 "date": web.repo[n].date(),
378 "date": web.repo[n].date(),
379 "node": hex(n)}
379 "node": hex(n)}
380
380
381 return tmpl("tags",
381 return tmpl("tags",
382 node=hex(web.repo.changelog.tip()),
382 node=hex(web.repo.changelog.tip()),
383 entries=lambda **x: entries(False, 0, **x),
383 entries=lambda **x: entries(False, 0, **x),
384 entriesnotip=lambda **x: entries(True, 0, **x),
384 entriesnotip=lambda **x: entries(True, 0, **x),
385 latestentry=lambda **x: entries(True, 1, **x))
385 latestentry=lambda **x: entries(True, 1, **x))
386
386
387 def branches(web, req, tmpl):
387 def branches(web, req, tmpl):
388 tips = (web.repo[n] for t, n in web.repo.branchtags().iteritems())
388 tips = (web.repo[n] for t, n in web.repo.branchtags().iteritems())
389 heads = web.repo.heads()
389 heads = web.repo.heads()
390 parity = paritygen(web.stripecount)
390 parity = paritygen(web.stripecount)
391 sortkey = lambda ctx: ('close' not in ctx.extra(), ctx.rev())
391 sortkey = lambda ctx: ('close' not in ctx.extra(), ctx.rev())
392
392
393 def entries(limit, **map):
393 def entries(limit, **map):
394 count = 0
394 count = 0
395 for ctx in sorted(tips, key=sortkey, reverse=True):
395 for ctx in sorted(tips, key=sortkey, reverse=True):
396 if limit > 0 and count >= limit:
396 if limit > 0 and count >= limit:
397 return
397 return
398 count += 1
398 count += 1
399 if ctx.node() not in heads:
399 if ctx.node() not in heads:
400 status = 'inactive'
400 status = 'inactive'
401 elif not web.repo.branchheads(ctx.branch()):
401 elif not web.repo.branchheads(ctx.branch()):
402 status = 'closed'
402 status = 'closed'
403 else:
403 else:
404 status = 'open'
404 status = 'open'
405 yield {'parity': parity.next(),
405 yield {'parity': parity.next(),
406 'branch': ctx.branch(),
406 'branch': ctx.branch(),
407 'status': status,
407 'status': status,
408 'node': ctx.hex(),
408 'node': ctx.hex(),
409 'date': ctx.date()}
409 'date': ctx.date()}
410
410
411 return tmpl('branches', node=hex(web.repo.changelog.tip()),
411 return tmpl('branches', node=hex(web.repo.changelog.tip()),
412 entries=lambda **x: entries(0, **x),
412 entries=lambda **x: entries(0, **x),
413 latestentry=lambda **x: entries(1, **x))
413 latestentry=lambda **x: entries(1, **x))
414
414
415 def summary(web, req, tmpl):
415 def summary(web, req, tmpl):
416 i = web.repo.tagslist()
416 i = web.repo.tagslist()
417 i.reverse()
417 i.reverse()
418
418
419 def tagentries(**map):
419 def tagentries(**map):
420 parity = paritygen(web.stripecount)
420 parity = paritygen(web.stripecount)
421 count = 0
421 count = 0
422 for k, n in i:
422 for k, n in i:
423 if k == "tip": # skip tip
423 if k == "tip": # skip tip
424 continue
424 continue
425
425
426 count += 1
426 count += 1
427 if count > 10: # limit to 10 tags
427 if count > 10: # limit to 10 tags
428 break
428 break
429
429
430 yield tmpl("tagentry",
430 yield tmpl("tagentry",
431 parity=parity.next(),
431 parity=parity.next(),
432 tag=k,
432 tag=k,
433 node=hex(n),
433 node=hex(n),
434 date=web.repo[n].date())
434 date=web.repo[n].date())
435
435
436 def branches(**map):
436 def branches(**map):
437 parity = paritygen(web.stripecount)
437 parity = paritygen(web.stripecount)
438
438
439 b = web.repo.branchtags()
439 b = web.repo.branchtags()
440 l = [(-web.repo.changelog.rev(n), n, t) for t, n in b.iteritems()]
440 l = [(-web.repo.changelog.rev(n), n, t) for t, n in b.iteritems()]
441 for r, n, t in sorted(l):
441 for r, n, t in sorted(l):
442 yield {'parity': parity.next(),
442 yield {'parity': parity.next(),
443 'branch': t,
443 'branch': t,
444 'node': hex(n),
444 'node': hex(n),
445 'date': web.repo[n].date()}
445 'date': web.repo[n].date()}
446
446
447 def changelist(**map):
447 def changelist(**map):
448 parity = paritygen(web.stripecount, offset=start - end)
448 parity = paritygen(web.stripecount, offset=start - end)
449 l = [] # build a list in forward order for efficiency
449 l = [] # build a list in forward order for efficiency
450 for i in xrange(start, end):
450 for i in xrange(start, end):
451 ctx = web.repo[i]
451 ctx = web.repo[i]
452 n = ctx.node()
452 n = ctx.node()
453 hn = hex(n)
453 hn = hex(n)
454
454
455 l.insert(0, tmpl(
455 l.insert(0, tmpl(
456 'shortlogentry',
456 'shortlogentry',
457 parity=parity.next(),
457 parity=parity.next(),
458 author=ctx.user(),
458 author=ctx.user(),
459 desc=ctx.description(),
459 desc=ctx.description(),
460 date=ctx.date(),
460 date=ctx.date(),
461 rev=i,
461 rev=i,
462 node=hn,
462 node=hn,
463 tags=webutil.nodetagsdict(web.repo, n),
463 tags=webutil.nodetagsdict(web.repo, n),
464 inbranch=webutil.nodeinbranch(web.repo, ctx),
464 inbranch=webutil.nodeinbranch(web.repo, ctx),
465 branches=webutil.nodebranchdict(web.repo, ctx)))
465 branches=webutil.nodebranchdict(web.repo, ctx)))
466
466
467 yield l
467 yield l
468
468
469 tip = web.repo['tip']
469 tip = web.repo['tip']
470 count = len(web.repo)
470 count = len(web.repo)
471 start = max(0, count - web.maxchanges)
471 start = max(0, count - web.maxchanges)
472 end = min(count, start + web.maxchanges)
472 end = min(count, start + web.maxchanges)
473
473
474 return tmpl("summary",
474 return tmpl("summary",
475 desc=web.config("web", "description", "unknown"),
475 desc=web.config("web", "description", "unknown"),
476 owner=get_contact(web.config) or "unknown",
476 owner=get_contact(web.config) or "unknown",
477 lastchange=tip.date(),
477 lastchange=tip.date(),
478 tags=tagentries,
478 tags=tagentries,
479 branches=branches,
479 branches=branches,
480 shortlog=changelist,
480 shortlog=changelist,
481 node=tip.hex(),
481 node=tip.hex(),
482 archives=web.archivelist("tip"))
482 archives=web.archivelist("tip"))
483
483
484 def filediff(web, req, tmpl):
484 def filediff(web, req, tmpl):
485 fctx, ctx = None, None
485 fctx, ctx = None, None
486 try:
486 try:
487 fctx = webutil.filectx(web.repo, req)
487 fctx = webutil.filectx(web.repo, req)
488 except LookupError:
488 except LookupError:
489 ctx = webutil.changectx(web.repo, req)
489 ctx = webutil.changectx(web.repo, req)
490 path = webutil.cleanpath(web.repo, req.form['file'][0])
490 path = webutil.cleanpath(web.repo, req.form['file'][0])
491 if path not in ctx.files():
491 if path not in ctx.files():
492 raise
492 raise
493
493
494 if fctx is not None:
494 if fctx is not None:
495 n = fctx.node()
495 n = fctx.node()
496 path = fctx.path()
496 path = fctx.path()
497 else:
497 else:
498 n = ctx.node()
498 n = ctx.node()
499 # path already defined in except clause
499 # path already defined in except clause
500
500
501 parity = paritygen(web.stripecount)
501 parity = paritygen(web.stripecount)
502 style = web.config('web', 'style', 'paper')
502 style = web.config('web', 'style', 'paper')
503 if 'style' in req.form:
503 if 'style' in req.form:
504 style = req.form['style'][0]
504 style = req.form['style'][0]
505
505
506 diffs = webutil.diffs(web.repo, tmpl, fctx or ctx, [path], parity, style)
506 diffs = webutil.diffs(web.repo, tmpl, fctx or ctx, [path], parity, style)
507 rename = fctx and webutil.renamelink(fctx) or []
507 rename = fctx and webutil.renamelink(fctx) or []
508 ctx = fctx and fctx or ctx
508 ctx = fctx and fctx or ctx
509 return tmpl("filediff",
509 return tmpl("filediff",
510 file=path,
510 file=path,
511 node=hex(n),
511 node=hex(n),
512 rev=ctx.rev(),
512 rev=ctx.rev(),
513 date=ctx.date(),
513 date=ctx.date(),
514 desc=ctx.description(),
514 desc=ctx.description(),
515 author=ctx.user(),
515 author=ctx.user(),
516 rename=rename,
516 rename=rename,
517 branch=webutil.nodebranchnodefault(ctx),
517 branch=webutil.nodebranchnodefault(ctx),
518 parent=webutil.parents(ctx),
518 parent=webutil.parents(ctx),
519 child=webutil.children(ctx),
519 child=webutil.children(ctx),
520 diff=diffs)
520 diff=diffs)
521
521
522 diff = filediff
522 diff = filediff
523
523
524 def annotate(web, req, tmpl):
524 def annotate(web, req, tmpl):
525 fctx = webutil.filectx(web.repo, req)
525 fctx = webutil.filectx(web.repo, req)
526 f = fctx.path()
526 f = fctx.path()
527 parity = paritygen(web.stripecount)
527 parity = paritygen(web.stripecount)
528
528
529 def annotate(**map):
529 def annotate(**map):
530 last = None
530 last = None
531 if binary(fctx.data()):
531 if binary(fctx.data()):
532 mt = (mimetypes.guess_type(fctx.path())[0]
532 mt = (mimetypes.guess_type(fctx.path())[0]
533 or 'application/octet-stream')
533 or 'application/octet-stream')
534 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
534 lines = enumerate([((fctx.filectx(fctx.filerev()), 1),
535 '(binary:%s)' % mt)])
535 '(binary:%s)' % mt)])
536 else:
536 else:
537 lines = enumerate(fctx.annotate(follow=True, linenumber=True))
537 lines = enumerate(fctx.annotate(follow=True, linenumber=True))
538 for lineno, ((f, targetline), l) in lines:
538 for lineno, ((f, targetline), l) in lines:
539 fnode = f.filenode()
539 fnode = f.filenode()
540
540
541 if last != fnode:
541 if last != fnode:
542 last = fnode
542 last = fnode
543
543
544 yield {"parity": parity.next(),
544 yield {"parity": parity.next(),
545 "node": hex(f.node()),
545 "node": hex(f.node()),
546 "rev": f.rev(),
546 "rev": f.rev(),
547 "author": f.user(),
547 "author": f.user(),
548 "desc": f.description(),
548 "desc": f.description(),
549 "file": f.path(),
549 "file": f.path(),
550 "targetline": targetline,
550 "targetline": targetline,
551 "line": l,
551 "line": l,
552 "lineid": "l%d" % (lineno + 1),
552 "lineid": "l%d" % (lineno + 1),
553 "linenumber": "% 6d" % (lineno + 1)}
553 "linenumber": "% 6d" % (lineno + 1)}
554
554
555 return tmpl("fileannotate",
555 return tmpl("fileannotate",
556 file=f,
556 file=f,
557 annotate=annotate,
557 annotate=annotate,
558 path=webutil.up(f),
558 path=webutil.up(f),
559 rev=fctx.rev(),
559 rev=fctx.rev(),
560 node=hex(fctx.node()),
560 node=hex(fctx.node()),
561 author=fctx.user(),
561 author=fctx.user(),
562 date=fctx.date(),
562 date=fctx.date(),
563 desc=fctx.description(),
563 desc=fctx.description(),
564 rename=webutil.renamelink(fctx),
564 rename=webutil.renamelink(fctx),
565 branch=webutil.nodebranchnodefault(fctx),
565 branch=webutil.nodebranchnodefault(fctx),
566 parent=webutil.parents(fctx),
566 parent=webutil.parents(fctx),
567 child=webutil.children(fctx),
567 child=webutil.children(fctx),
568 permissions=fctx.manifest().flags(f))
568 permissions=fctx.manifest().flags(f))
569
569
570 def filelog(web, req, tmpl):
570 def filelog(web, req, tmpl):
571
571
572 try:
572 try:
573 fctx = webutil.filectx(web.repo, req)
573 fctx = webutil.filectx(web.repo, req)
574 f = fctx.path()
574 f = fctx.path()
575 fl = fctx.filelog()
575 fl = fctx.filelog()
576 except error.LookupError:
576 except error.LookupError:
577 f = webutil.cleanpath(web.repo, req.form['file'][0])
577 f = webutil.cleanpath(web.repo, req.form['file'][0])
578 fl = web.repo.file(f)
578 fl = web.repo.file(f)
579 numrevs = len(fl)
579 numrevs = len(fl)
580 if not numrevs: # file doesn't exist at all
580 if not numrevs: # file doesn't exist at all
581 raise
581 raise
582 rev = webutil.changectx(web.repo, req).rev()
582 rev = webutil.changectx(web.repo, req).rev()
583 first = fl.linkrev(0)
583 first = fl.linkrev(0)
584 if rev < first: # current rev is from before file existed
584 if rev < first: # current rev is from before file existed
585 raise
585 raise
586 frev = numrevs - 1
586 frev = numrevs - 1
587 while fl.linkrev(frev) > rev:
587 while fl.linkrev(frev) > rev:
588 frev -= 1
588 frev -= 1
589 fctx = web.repo.filectx(f, fl.linkrev(frev))
589 fctx = web.repo.filectx(f, fl.linkrev(frev))
590
590
591 revcount = web.maxshortchanges
591 revcount = web.maxshortchanges
592 if 'revcount' in req.form:
592 if 'revcount' in req.form:
593 revcount = int(req.form.get('revcount', [revcount])[0])
593 revcount = int(req.form.get('revcount', [revcount])[0])
594 tmpl.defaults['sessionvars']['revcount'] = revcount
594 tmpl.defaults['sessionvars']['revcount'] = revcount
595
595
596 lessvars = copy.copy(tmpl.defaults['sessionvars'])
596 lessvars = copy.copy(tmpl.defaults['sessionvars'])
597 lessvars['revcount'] = revcount / 2
597 lessvars['revcount'] = revcount / 2
598 morevars = copy.copy(tmpl.defaults['sessionvars'])
598 morevars = copy.copy(tmpl.defaults['sessionvars'])
599 morevars['revcount'] = revcount * 2
599 morevars['revcount'] = revcount * 2
600
600
601 count = fctx.filerev() + 1
601 count = fctx.filerev() + 1
602 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
602 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
603 end = min(count, start + revcount) # last rev on this page
603 end = min(count, start + revcount) # last rev on this page
604 parity = paritygen(web.stripecount, offset=start - end)
604 parity = paritygen(web.stripecount, offset=start - end)
605
605
606 def entries(limit=0, **map):
606 def entries(limit=0, **map):
607 l = []
607 l = []
608
608
609 repo = web.repo
609 repo = web.repo
610 for i in xrange(start, end):
610 for i in xrange(start, end):
611 iterfctx = fctx.filectx(i)
611 iterfctx = fctx.filectx(i)
612
612
613 l.insert(0, {"parity": parity.next(),
613 l.insert(0, {"parity": parity.next(),
614 "filerev": i,
614 "filerev": i,
615 "file": f,
615 "file": f,
616 "node": hex(iterfctx.node()),
616 "node": hex(iterfctx.node()),
617 "author": iterfctx.user(),
617 "author": iterfctx.user(),
618 "date": iterfctx.date(),
618 "date": iterfctx.date(),
619 "rename": webutil.renamelink(iterfctx),
619 "rename": webutil.renamelink(iterfctx),
620 "parent": webutil.parents(iterfctx),
620 "parent": webutil.parents(iterfctx),
621 "child": webutil.children(iterfctx),
621 "child": webutil.children(iterfctx),
622 "desc": iterfctx.description(),
622 "desc": iterfctx.description(),
623 "tags": webutil.nodetagsdict(repo, iterfctx.node()),
623 "tags": webutil.nodetagsdict(repo, iterfctx.node()),
624 "branch": webutil.nodebranchnodefault(iterfctx),
624 "branch": webutil.nodebranchnodefault(iterfctx),
625 "inbranch": webutil.nodeinbranch(repo, iterfctx),
625 "inbranch": webutil.nodeinbranch(repo, iterfctx),
626 "branches": webutil.nodebranchdict(repo, iterfctx)})
626 "branches": webutil.nodebranchdict(repo, iterfctx)})
627
627
628 if limit > 0:
628 if limit > 0:
629 l = l[:limit]
629 l = l[:limit]
630
630
631 for e in l:
631 for e in l:
632 yield e
632 yield e
633
633
634 nodefunc = lambda x: fctx.filectx(fileid=x)
634 nodefunc = lambda x: fctx.filectx(fileid=x)
635 nav = webutil.revnavgen(end - 1, revcount, count, nodefunc)
635 nav = webutil.revnavgen(end - 1, revcount, count, nodefunc)
636 return tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav,
636 return tmpl("filelog", file=f, node=hex(fctx.node()), nav=nav,
637 entries=lambda **x: entries(limit=0, **x),
637 entries=lambda **x: entries(limit=0, **x),
638 latestentry=lambda **x: entries(limit=1, **x),
638 latestentry=lambda **x: entries(limit=1, **x),
639 revcount=revcount, morevars=morevars, lessvars=lessvars)
639 revcount=revcount, morevars=morevars, lessvars=lessvars)
640
640
641 def archive(web, req, tmpl):
641 def archive(web, req, tmpl):
642 type_ = req.form.get('type', [None])[0]
642 type_ = req.form.get('type', [None])[0]
643 allowed = web.configlist("web", "allow_archive")
643 allowed = web.configlist("web", "allow_archive")
644 key = req.form['node'][0]
644 key = req.form['node'][0]
645
645
646 if type_ not in web.archives:
646 if type_ not in web.archives:
647 msg = 'Unsupported archive type: %s' % type_
647 msg = 'Unsupported archive type: %s' % type_
648 raise ErrorResponse(HTTP_NOT_FOUND, msg)
648 raise ErrorResponse(HTTP_NOT_FOUND, msg)
649
649
650 if not ((type_ in allowed or
650 if not ((type_ in allowed or
651 web.configbool("web", "allow" + type_, False))):
651 web.configbool("web", "allow" + type_, False))):
652 msg = 'Archive type not allowed: %s' % type_
652 msg = 'Archive type not allowed: %s' % type_
653 raise ErrorResponse(HTTP_FORBIDDEN, msg)
653 raise ErrorResponse(HTTP_FORBIDDEN, msg)
654
654
655 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
655 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
656 cnode = web.repo.lookup(key)
656 cnode = web.repo.lookup(key)
657 arch_version = key
657 arch_version = key
658 if cnode == key or key == 'tip':
658 if cnode == key or key == 'tip':
659 arch_version = short(cnode)
659 arch_version = short(cnode)
660 name = "%s-%s" % (reponame, arch_version)
660 name = "%s-%s" % (reponame, arch_version)
661 mimetype, artype, extension, encoding = web.archive_specs[type_]
661 mimetype, artype, extension, encoding = web.archive_specs[type_]
662 headers = [
662 headers = [
663 ('Content-Type', mimetype),
663 ('Content-Type', mimetype),
664 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
664 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
665 ]
665 ]
666 if encoding:
666 if encoding:
667 headers.append(('Content-Encoding', encoding))
667 headers.append(('Content-Encoding', encoding))
668 req.header(headers)
668 req.header(headers)
669 req.respond(HTTP_OK)
669 req.respond(HTTP_OK)
670 archival.archive(web.repo, req, cnode, artype, prefix=name)
670 archival.archive(web.repo, req, cnode, artype, prefix=name)
671 return []
671 return []
672
672
673
673
674 def static(web, req, tmpl):
674 def static(web, req, tmpl):
675 fname = req.form['file'][0]
675 fname = req.form['file'][0]
676 # a repo owner may set web.static in .hg/hgrc to get any file
676 # a repo owner may set web.static in .hg/hgrc to get any file
677 # readable by the user running the CGI script
677 # readable by the user running the CGI script
678 static = web.config("web", "static", None, untrusted=False)
678 static = web.config("web", "static", None, untrusted=False)
679 if not static:
679 if not static:
680 tp = web.templatepath or templater.templatepath()
680 tp = web.templatepath or templater.templatepath()
681 if isinstance(tp, str):
681 if isinstance(tp, str):
682 tp = [tp]
682 tp = [tp]
683 static = [os.path.join(p, 'static') for p in tp]
683 static = [os.path.join(p, 'static') for p in tp]
684 return [staticfile(static, fname, req)]
684 return [staticfile(static, fname, req)]
685
685
686 def graph(web, req, tmpl):
686 def graph(web, req, tmpl):
687
687
688 rev = webutil.changectx(web.repo, req).rev()
688 rev = webutil.changectx(web.repo, req).rev()
689 bg_height = 39
689 bg_height = 39
690 revcount = web.maxshortchanges
690 revcount = web.maxshortchanges
691 if 'revcount' in req.form:
691 if 'revcount' in req.form:
692 revcount = int(req.form.get('revcount', [revcount])[0])
692 revcount = int(req.form.get('revcount', [revcount])[0])
693 tmpl.defaults['sessionvars']['revcount'] = revcount
693 tmpl.defaults['sessionvars']['revcount'] = revcount
694
694
695 lessvars = copy.copy(tmpl.defaults['sessionvars'])
695 lessvars = copy.copy(tmpl.defaults['sessionvars'])
696 lessvars['revcount'] = revcount / 2
696 lessvars['revcount'] = revcount / 2
697 morevars = copy.copy(tmpl.defaults['sessionvars'])
697 morevars = copy.copy(tmpl.defaults['sessionvars'])
698 morevars['revcount'] = revcount * 2
698 morevars['revcount'] = revcount * 2
699
699
700 max_rev = len(web.repo) - 1
700 max_rev = len(web.repo) - 1
701 revcount = min(max_rev, revcount)
701 revcount = min(max_rev, revcount)
702 revnode = web.repo.changelog.node(rev)
702 revnode = web.repo.changelog.node(rev)
703 revnode_hex = hex(revnode)
703 revnode_hex = hex(revnode)
704 uprev = min(max_rev, rev + revcount)
704 uprev = min(max_rev, rev + revcount)
705 downrev = max(0, rev - revcount)
705 downrev = max(0, rev - revcount)
706 count = len(web.repo)
706 count = len(web.repo)
707 changenav = webutil.revnavgen(rev, revcount, count, web.repo.changectx)
707 changenav = webutil.revnavgen(rev, revcount, count, web.repo.changectx)
708
708
709 dag = graphmod.revisions(web.repo, rev, downrev)
709 dag = graphmod.revisions(web.repo, rev, downrev)
710 tree = list(graphmod.colored(dag))
710 tree = list(graphmod.colored(dag))
711 canvasheight = (len(tree) + 1) * bg_height - 27
711 canvasheight = (len(tree) + 1) * bg_height - 27
712 data = []
712 data = []
713 for (id, type, ctx, vtx, edges) in tree:
713 for (id, type, ctx, vtx, edges) in tree:
714 if type != graphmod.CHANGESET:
714 if type != graphmod.CHANGESET:
715 continue
715 continue
716 node = short(ctx.node())
716 node = short(ctx.node())
717 age = templatefilters.age(ctx.date())
717 age = templatefilters.age(ctx.date())
718 desc = templatefilters.firstline(ctx.description())
718 desc = templatefilters.firstline(ctx.description())
719 desc = cgi.escape(templatefilters.nonempty(desc))
719 desc = cgi.escape(templatefilters.nonempty(desc))
720 user = cgi.escape(templatefilters.person(ctx.user()))
720 user = cgi.escape(templatefilters.person(ctx.user()))
721 branch = ctx.branch()
721 branch = ctx.branch()
722 branch = branch, web.repo.branchtags().get(branch) == ctx.node()
722 branch = branch, web.repo.branchtags().get(branch) == ctx.node()
723 data.append((node, vtx, edges, desc, user, age, branch, ctx.tags()))
723 data.append((node, vtx, edges, desc, user, age, branch, ctx.tags()))
724
724
725 return tmpl('graph', rev=rev, revcount=revcount, uprev=uprev,
725 return tmpl('graph', rev=rev, revcount=revcount, uprev=uprev,
726 lessvars=lessvars, morevars=morevars, downrev=downrev,
726 lessvars=lessvars, morevars=morevars, downrev=downrev,
727 canvasheight=canvasheight, jsdata=data, bg_height=bg_height,
727 canvasheight=canvasheight, jsdata=data, bg_height=bg_height,
728 node=revnode_hex, changenav=changenav)
728 node=revnode_hex, changenav=changenav)
729
729
730 def _getdoc(e):
730 def _getdoc(e):
731 doc = e[0].__doc__
731 doc = e[0].__doc__
732 if doc:
732 if doc:
733 doc = doc.split('\n')[0]
733 doc = doc.split('\n')[0]
734 else:
734 else:
735 doc = _('(no help text available)')
735 doc = _('(no help text available)')
736 return doc
736 return doc
737
737
738 def help(web, req, tmpl):
738 def help(web, req, tmpl):
739 from mercurial import commands # avoid cycle
739 from mercurial import commands # avoid cycle
740
740
741 topicname = req.form.get('node', [None])[0]
741 topicname = req.form.get('node', [None])[0]
742 if not topicname:
742 if not topicname:
743 topic = []
743 topic = []
744
744
745 def topics(**map):
745 def topics(**map):
746 for entries, summary, _ in helpmod.helptable:
746 for entries, summary, _ in helpmod.helptable:
747 entries = sorted(entries, key=len)
747 entries = sorted(entries, key=len)
748 yield {'topic': entries[-1], 'summary': summary}
748 yield {'topic': entries[-1], 'summary': summary}
749
749
750 early, other = [], []
750 early, other = [], []
751 primary = lambda s: s.split('|')[0]
751 primary = lambda s: s.split('|')[0]
752 for c, e in commands.table.iteritems():
752 for c, e in commands.table.iteritems():
753 doc = _getdoc(e)
753 doc = _getdoc(e)
754 if 'DEPRECATED' in doc or c.startswith('debug'):
754 if 'DEPRECATED' in doc or c.startswith('debug'):
755 continue
755 continue
756 cmd = primary(c)
756 cmd = primary(c)
757 if cmd.startswith('^'):
757 if cmd.startswith('^'):
758 early.append((cmd[1:], doc))
758 early.append((cmd[1:], doc))
759 else:
759 else:
760 other.append((cmd, doc))
760 other.append((cmd, doc))
761
761
762 early.sort()
762 early.sort()
763 other.sort()
763 other.sort()
764
764
765 def earlycommands(**map):
765 def earlycommands(**map):
766 for c, doc in early:
766 for c, doc in early:
767 yield {'topic': c, 'summary': doc}
767 yield {'topic': c, 'summary': doc}
768
768
769 def othercommands(**map):
769 def othercommands(**map):
770 for c, doc in other:
770 for c, doc in other:
771 yield {'topic': c, 'summary': doc}
771 yield {'topic': c, 'summary': doc}
772
772
773 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
773 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
774 othercommands=othercommands, title='Index')
774 othercommands=othercommands, title='Index')
775
775
776 u = web.repo.ui
776 u = webutil.wsgiui()
777 u.pushbuffer()
777 u.pushbuffer()
778 try:
778 try:
779 commands.help_(u, topicname)
779 commands.help_(u, topicname)
780 except error.UnknownCommand:
780 except error.UnknownCommand:
781 raise ErrorResponse(HTTP_NOT_FOUND)
781 raise ErrorResponse(HTTP_NOT_FOUND)
782 doc = u.popbuffer()
782 doc = u.popbuffer()
783 return tmpl('help', topic=topicname, doc=doc)
783 return tmpl('help', topic=topicname, doc=doc)
General Comments 0
You need to be logged in to leave comments. Login now