##// END OF EJS Templates
hgweb: add a sendtemplate() helper function...
Gregory Szorc -
r36899:061635d4 default
parent child Browse files
Show More
@@ -1,448 +1,449 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 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import contextlib
11 import contextlib
12 import os
12 import os
13
13
14 from .common import (
14 from .common import (
15 ErrorResponse,
15 ErrorResponse,
16 HTTP_BAD_REQUEST,
16 HTTP_BAD_REQUEST,
17 cspvalues,
17 cspvalues,
18 permhooks,
18 permhooks,
19 statusmessage,
19 statusmessage,
20 )
20 )
21
21
22 from .. import (
22 from .. import (
23 encoding,
23 encoding,
24 error,
24 error,
25 formatter,
25 formatter,
26 hg,
26 hg,
27 hook,
27 hook,
28 profiling,
28 profiling,
29 pycompat,
29 pycompat,
30 repoview,
30 repoview,
31 templatefilters,
31 templatefilters,
32 templater,
32 templater,
33 ui as uimod,
33 ui as uimod,
34 util,
34 util,
35 wireprotoserver,
35 wireprotoserver,
36 )
36 )
37
37
38 from . import (
38 from . import (
39 request as requestmod,
39 request as requestmod,
40 webcommands,
40 webcommands,
41 webutil,
41 webutil,
42 wsgicgi,
42 wsgicgi,
43 )
43 )
44
44
45 archivespecs = util.sortdict((
45 archivespecs = util.sortdict((
46 ('zip', ('application/zip', 'zip', '.zip', None)),
46 ('zip', ('application/zip', 'zip', '.zip', None)),
47 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
47 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
48 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
48 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
49 ))
49 ))
50
50
51 def getstyle(req, configfn, templatepath):
51 def getstyle(req, configfn, templatepath):
52 styles = (
52 styles = (
53 req.qsparams.get('style', None),
53 req.qsparams.get('style', None),
54 configfn('web', 'style'),
54 configfn('web', 'style'),
55 'paper',
55 'paper',
56 )
56 )
57 return styles, templater.stylemap(styles, templatepath)
57 return styles, templater.stylemap(styles, templatepath)
58
58
59 def makebreadcrumb(url, prefix=''):
59 def makebreadcrumb(url, prefix=''):
60 '''Return a 'URL breadcrumb' list
60 '''Return a 'URL breadcrumb' list
61
61
62 A 'URL breadcrumb' is a list of URL-name pairs,
62 A 'URL breadcrumb' is a list of URL-name pairs,
63 corresponding to each of the path items on a URL.
63 corresponding to each of the path items on a URL.
64 This can be used to create path navigation entries.
64 This can be used to create path navigation entries.
65 '''
65 '''
66 if url.endswith('/'):
66 if url.endswith('/'):
67 url = url[:-1]
67 url = url[:-1]
68 if prefix:
68 if prefix:
69 url = '/' + prefix + url
69 url = '/' + prefix + url
70 relpath = url
70 relpath = url
71 if relpath.startswith('/'):
71 if relpath.startswith('/'):
72 relpath = relpath[1:]
72 relpath = relpath[1:]
73
73
74 breadcrumb = []
74 breadcrumb = []
75 urlel = url
75 urlel = url
76 pathitems = [''] + relpath.split('/')
76 pathitems = [''] + relpath.split('/')
77 for pathel in reversed(pathitems):
77 for pathel in reversed(pathitems):
78 if not pathel or not urlel:
78 if not pathel or not urlel:
79 break
79 break
80 breadcrumb.append({'url': urlel, 'name': pathel})
80 breadcrumb.append({'url': urlel, 'name': pathel})
81 urlel = os.path.dirname(urlel)
81 urlel = os.path.dirname(urlel)
82 return reversed(breadcrumb)
82 return reversed(breadcrumb)
83
83
84 class requestcontext(object):
84 class requestcontext(object):
85 """Holds state/context for an individual request.
85 """Holds state/context for an individual request.
86
86
87 Servers can be multi-threaded. Holding state on the WSGI application
87 Servers can be multi-threaded. Holding state on the WSGI application
88 is prone to race conditions. Instances of this class exist to hold
88 is prone to race conditions. Instances of this class exist to hold
89 mutable and race-free state for requests.
89 mutable and race-free state for requests.
90 """
90 """
91 def __init__(self, app, repo, req, res):
91 def __init__(self, app, repo, req, res):
92 self.repo = repo
92 self.repo = repo
93 self.reponame = app.reponame
93 self.reponame = app.reponame
94 self.req = req
94 self.req = req
95 self.res = res
95 self.res = res
96
96
97 self.archivespecs = archivespecs
97 self.archivespecs = archivespecs
98
98
99 self.maxchanges = self.configint('web', 'maxchanges')
99 self.maxchanges = self.configint('web', 'maxchanges')
100 self.stripecount = self.configint('web', 'stripes')
100 self.stripecount = self.configint('web', 'stripes')
101 self.maxshortchanges = self.configint('web', 'maxshortchanges')
101 self.maxshortchanges = self.configint('web', 'maxshortchanges')
102 self.maxfiles = self.configint('web', 'maxfiles')
102 self.maxfiles = self.configint('web', 'maxfiles')
103 self.allowpull = self.configbool('web', 'allow-pull')
103 self.allowpull = self.configbool('web', 'allow-pull')
104
104
105 # we use untrusted=False to prevent a repo owner from using
105 # we use untrusted=False to prevent a repo owner from using
106 # web.templates in .hg/hgrc to get access to any file readable
106 # web.templates in .hg/hgrc to get access to any file readable
107 # by the user running the CGI script
107 # by the user running the CGI script
108 self.templatepath = self.config('web', 'templates', untrusted=False)
108 self.templatepath = self.config('web', 'templates', untrusted=False)
109
109
110 # This object is more expensive to build than simple config values.
110 # This object is more expensive to build than simple config values.
111 # It is shared across requests. The app will replace the object
111 # It is shared across requests. The app will replace the object
112 # if it is updated. Since this is a reference and nothing should
112 # if it is updated. Since this is a reference and nothing should
113 # modify the underlying object, it should be constant for the lifetime
113 # modify the underlying object, it should be constant for the lifetime
114 # of the request.
114 # of the request.
115 self.websubtable = app.websubtable
115 self.websubtable = app.websubtable
116
116
117 self.csp, self.nonce = cspvalues(self.repo.ui)
117 self.csp, self.nonce = cspvalues(self.repo.ui)
118
118
119 # Trust the settings from the .hg/hgrc files by default.
119 # Trust the settings from the .hg/hgrc files by default.
120 def config(self, section, name, default=uimod._unset, untrusted=True):
120 def config(self, section, name, default=uimod._unset, untrusted=True):
121 return self.repo.ui.config(section, name, default,
121 return self.repo.ui.config(section, name, default,
122 untrusted=untrusted)
122 untrusted=untrusted)
123
123
124 def configbool(self, section, name, default=uimod._unset, untrusted=True):
124 def configbool(self, section, name, default=uimod._unset, untrusted=True):
125 return self.repo.ui.configbool(section, name, default,
125 return self.repo.ui.configbool(section, name, default,
126 untrusted=untrusted)
126 untrusted=untrusted)
127
127
128 def configint(self, section, name, default=uimod._unset, untrusted=True):
128 def configint(self, section, name, default=uimod._unset, untrusted=True):
129 return self.repo.ui.configint(section, name, default,
129 return self.repo.ui.configint(section, name, default,
130 untrusted=untrusted)
130 untrusted=untrusted)
131
131
132 def configlist(self, section, name, default=uimod._unset, untrusted=True):
132 def configlist(self, section, name, default=uimod._unset, untrusted=True):
133 return self.repo.ui.configlist(section, name, default,
133 return self.repo.ui.configlist(section, name, default,
134 untrusted=untrusted)
134 untrusted=untrusted)
135
135
136 def archivelist(self, nodeid):
136 def archivelist(self, nodeid):
137 allowed = self.configlist('web', 'allow_archive')
137 allowed = self.configlist('web', 'allow_archive')
138 for typ, spec in self.archivespecs.iteritems():
138 for typ, spec in self.archivespecs.iteritems():
139 if typ in allowed or self.configbool('web', 'allow%s' % typ):
139 if typ in allowed or self.configbool('web', 'allow%s' % typ):
140 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
140 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
141
141
142 def templater(self, req):
142 def templater(self, req):
143 # determine scheme, port and server name
143 # determine scheme, port and server name
144 # this is needed to create absolute urls
144 # this is needed to create absolute urls
145 logourl = self.config('web', 'logourl')
145 logourl = self.config('web', 'logourl')
146 logoimg = self.config('web', 'logoimg')
146 logoimg = self.config('web', 'logoimg')
147 staticurl = (self.config('web', 'staticurl')
147 staticurl = (self.config('web', 'staticurl')
148 or req.apppath + '/static/')
148 or req.apppath + '/static/')
149 if not staticurl.endswith('/'):
149 if not staticurl.endswith('/'):
150 staticurl += '/'
150 staticurl += '/'
151
151
152 # some functions for the templater
152 # some functions for the templater
153
153
154 def motd(**map):
154 def motd(**map):
155 yield self.config('web', 'motd')
155 yield self.config('web', 'motd')
156
156
157 # figure out which style to use
157 # figure out which style to use
158
158
159 vars = {}
159 vars = {}
160 styles, (style, mapfile) = getstyle(req, self.config,
160 styles, (style, mapfile) = getstyle(req, self.config,
161 self.templatepath)
161 self.templatepath)
162 if style == styles[0]:
162 if style == styles[0]:
163 vars['style'] = style
163 vars['style'] = style
164
164
165 sessionvars = webutil.sessionvars(vars, '?')
165 sessionvars = webutil.sessionvars(vars, '?')
166
166
167 if not self.reponame:
167 if not self.reponame:
168 self.reponame = (self.config('web', 'name', '')
168 self.reponame = (self.config('web', 'name', '')
169 or req.reponame
169 or req.reponame
170 or req.apppath
170 or req.apppath
171 or self.repo.root)
171 or self.repo.root)
172
172
173 def websubfilter(text):
173 def websubfilter(text):
174 return templatefilters.websub(text, self.websubtable)
174 return templatefilters.websub(text, self.websubtable)
175
175
176 # create the templater
176 # create the templater
177 # TODO: export all keywords: defaults = templatekw.keywords.copy()
177 # TODO: export all keywords: defaults = templatekw.keywords.copy()
178 defaults = {
178 defaults = {
179 'url': req.apppath + '/',
179 'url': req.apppath + '/',
180 'logourl': logourl,
180 'logourl': logourl,
181 'logoimg': logoimg,
181 'logoimg': logoimg,
182 'staticurl': staticurl,
182 'staticurl': staticurl,
183 'urlbase': req.advertisedbaseurl,
183 'urlbase': req.advertisedbaseurl,
184 'repo': self.reponame,
184 'repo': self.reponame,
185 'encoding': encoding.encoding,
185 'encoding': encoding.encoding,
186 'motd': motd,
186 'motd': motd,
187 'sessionvars': sessionvars,
187 'sessionvars': sessionvars,
188 'pathdef': makebreadcrumb(req.apppath),
188 'pathdef': makebreadcrumb(req.apppath),
189 'style': style,
189 'style': style,
190 'nonce': self.nonce,
190 'nonce': self.nonce,
191 }
191 }
192 tres = formatter.templateresources(self.repo.ui, self.repo)
192 tres = formatter.templateresources(self.repo.ui, self.repo)
193 tmpl = templater.templater.frommapfile(mapfile,
193 tmpl = templater.templater.frommapfile(mapfile,
194 filters={'websub': websubfilter},
194 filters={'websub': websubfilter},
195 defaults=defaults,
195 defaults=defaults,
196 resources=tres)
196 resources=tres)
197 return tmpl
197 return tmpl
198
198
199 def sendtemplate(self, name, **kwargs):
200 """Helper function to send a response generated from a template."""
201 self.res.setbodygen(self.tmpl(name, **kwargs))
202 return self.res.sendresponse()
199
203
200 class hgweb(object):
204 class hgweb(object):
201 """HTTP server for individual repositories.
205 """HTTP server for individual repositories.
202
206
203 Instances of this class serve HTTP responses for a particular
207 Instances of this class serve HTTP responses for a particular
204 repository.
208 repository.
205
209
206 Instances are typically used as WSGI applications.
210 Instances are typically used as WSGI applications.
207
211
208 Some servers are multi-threaded. On these servers, there may
212 Some servers are multi-threaded. On these servers, there may
209 be multiple active threads inside __call__.
213 be multiple active threads inside __call__.
210 """
214 """
211 def __init__(self, repo, name=None, baseui=None):
215 def __init__(self, repo, name=None, baseui=None):
212 if isinstance(repo, str):
216 if isinstance(repo, str):
213 if baseui:
217 if baseui:
214 u = baseui.copy()
218 u = baseui.copy()
215 else:
219 else:
216 u = uimod.ui.load()
220 u = uimod.ui.load()
217 r = hg.repository(u, repo)
221 r = hg.repository(u, repo)
218 else:
222 else:
219 # we trust caller to give us a private copy
223 # we trust caller to give us a private copy
220 r = repo
224 r = repo
221
225
222 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
226 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
223 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
227 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
224 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
228 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
225 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
229 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
226 # resolve file patterns relative to repo root
230 # resolve file patterns relative to repo root
227 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
231 r.ui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
228 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
232 r.baseui.setconfig('ui', 'forcecwd', r.root, 'hgweb')
229 # displaying bundling progress bar while serving feel wrong and may
233 # displaying bundling progress bar while serving feel wrong and may
230 # break some wsgi implementation.
234 # break some wsgi implementation.
231 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
235 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
232 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
236 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
233 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
237 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
234 self._lastrepo = self._repos[0]
238 self._lastrepo = self._repos[0]
235 hook.redirect(True)
239 hook.redirect(True)
236 self.reponame = name
240 self.reponame = name
237
241
238 def _webifyrepo(self, repo):
242 def _webifyrepo(self, repo):
239 repo = getwebview(repo)
243 repo = getwebview(repo)
240 self.websubtable = webutil.getwebsubs(repo)
244 self.websubtable = webutil.getwebsubs(repo)
241 return repo
245 return repo
242
246
243 @contextlib.contextmanager
247 @contextlib.contextmanager
244 def _obtainrepo(self):
248 def _obtainrepo(self):
245 """Obtain a repo unique to the caller.
249 """Obtain a repo unique to the caller.
246
250
247 Internally we maintain a stack of cachedlocalrepo instances
251 Internally we maintain a stack of cachedlocalrepo instances
248 to be handed out. If one is available, we pop it and return it,
252 to be handed out. If one is available, we pop it and return it,
249 ensuring it is up to date in the process. If one is not available,
253 ensuring it is up to date in the process. If one is not available,
250 we clone the most recently used repo instance and return it.
254 we clone the most recently used repo instance and return it.
251
255
252 It is currently possible for the stack to grow without bounds
256 It is currently possible for the stack to grow without bounds
253 if the server allows infinite threads. However, servers should
257 if the server allows infinite threads. However, servers should
254 have a thread limit, thus establishing our limit.
258 have a thread limit, thus establishing our limit.
255 """
259 """
256 if self._repos:
260 if self._repos:
257 cached = self._repos.pop()
261 cached = self._repos.pop()
258 r, created = cached.fetch()
262 r, created = cached.fetch()
259 else:
263 else:
260 cached = self._lastrepo.copy()
264 cached = self._lastrepo.copy()
261 r, created = cached.fetch()
265 r, created = cached.fetch()
262 if created:
266 if created:
263 r = self._webifyrepo(r)
267 r = self._webifyrepo(r)
264
268
265 self._lastrepo = cached
269 self._lastrepo = cached
266 self.mtime = cached.mtime
270 self.mtime = cached.mtime
267 try:
271 try:
268 yield r
272 yield r
269 finally:
273 finally:
270 self._repos.append(cached)
274 self._repos.append(cached)
271
275
272 def run(self):
276 def run(self):
273 """Start a server from CGI environment.
277 """Start a server from CGI environment.
274
278
275 Modern servers should be using WSGI and should avoid this
279 Modern servers should be using WSGI and should avoid this
276 method, if possible.
280 method, if possible.
277 """
281 """
278 if not encoding.environ.get('GATEWAY_INTERFACE',
282 if not encoding.environ.get('GATEWAY_INTERFACE',
279 '').startswith("CGI/1."):
283 '').startswith("CGI/1."):
280 raise RuntimeError("This function is only intended to be "
284 raise RuntimeError("This function is only intended to be "
281 "called while running as a CGI script.")
285 "called while running as a CGI script.")
282 wsgicgi.launch(self)
286 wsgicgi.launch(self)
283
287
284 def __call__(self, env, respond):
288 def __call__(self, env, respond):
285 """Run the WSGI application.
289 """Run the WSGI application.
286
290
287 This may be called by multiple threads.
291 This may be called by multiple threads.
288 """
292 """
289 req = requestmod.wsgirequest(env, respond)
293 req = requestmod.wsgirequest(env, respond)
290 return self.run_wsgi(req)
294 return self.run_wsgi(req)
291
295
292 def run_wsgi(self, wsgireq):
296 def run_wsgi(self, wsgireq):
293 """Internal method to run the WSGI application.
297 """Internal method to run the WSGI application.
294
298
295 This is typically only called by Mercurial. External consumers
299 This is typically only called by Mercurial. External consumers
296 should be using instances of this class as the WSGI application.
300 should be using instances of this class as the WSGI application.
297 """
301 """
298 with self._obtainrepo() as repo:
302 with self._obtainrepo() as repo:
299 profile = repo.ui.configbool('profiling', 'enabled')
303 profile = repo.ui.configbool('profiling', 'enabled')
300 with profiling.profile(repo.ui, enabled=profile):
304 with profiling.profile(repo.ui, enabled=profile):
301 for r in self._runwsgi(wsgireq, repo):
305 for r in self._runwsgi(wsgireq, repo):
302 yield r
306 yield r
303
307
304 def _runwsgi(self, wsgireq, repo):
308 def _runwsgi(self, wsgireq, repo):
305 req = wsgireq.req
309 req = wsgireq.req
306 res = wsgireq.res
310 res = wsgireq.res
307 rctx = requestcontext(self, repo, req, res)
311 rctx = requestcontext(self, repo, req, res)
308
312
309 # This state is global across all threads.
313 # This state is global across all threads.
310 encoding.encoding = rctx.config('web', 'encoding')
314 encoding.encoding = rctx.config('web', 'encoding')
311 rctx.repo.ui.environ = wsgireq.env
315 rctx.repo.ui.environ = wsgireq.env
312
316
313 if rctx.csp:
317 if rctx.csp:
314 # hgwebdir may have added CSP header. Since we generate our own,
318 # hgwebdir may have added CSP header. Since we generate our own,
315 # replace it.
319 # replace it.
316 res.headers['Content-Security-Policy'] = rctx.csp
320 res.headers['Content-Security-Policy'] = rctx.csp
317
321
318 handled = wireprotoserver.handlewsgirequest(
322 handled = wireprotoserver.handlewsgirequest(
319 rctx, req, res, self.check_perm)
323 rctx, req, res, self.check_perm)
320 if handled:
324 if handled:
321 return res.sendresponse()
325 return res.sendresponse()
322
326
323 if req.havepathinfo:
327 if req.havepathinfo:
324 query = req.dispatchpath
328 query = req.dispatchpath
325 else:
329 else:
326 query = req.querystring.partition('&')[0].partition(';')[0]
330 query = req.querystring.partition('&')[0].partition(';')[0]
327
331
328 # translate user-visible url structure to internal structure
332 # translate user-visible url structure to internal structure
329
333
330 args = query.split('/', 2)
334 args = query.split('/', 2)
331 if 'cmd' not in req.qsparams and args and args[0]:
335 if 'cmd' not in req.qsparams and args and args[0]:
332 cmd = args.pop(0)
336 cmd = args.pop(0)
333 style = cmd.rfind('-')
337 style = cmd.rfind('-')
334 if style != -1:
338 if style != -1:
335 req.qsparams['style'] = cmd[:style]
339 req.qsparams['style'] = cmd[:style]
336 cmd = cmd[style + 1:]
340 cmd = cmd[style + 1:]
337
341
338 # avoid accepting e.g. style parameter as command
342 # avoid accepting e.g. style parameter as command
339 if util.safehasattr(webcommands, cmd):
343 if util.safehasattr(webcommands, cmd):
340 req.qsparams['cmd'] = cmd
344 req.qsparams['cmd'] = cmd
341
345
342 if cmd == 'static':
346 if cmd == 'static':
343 req.qsparams['file'] = '/'.join(args)
347 req.qsparams['file'] = '/'.join(args)
344 else:
348 else:
345 if args and args[0]:
349 if args and args[0]:
346 node = args.pop(0).replace('%2F', '/')
350 node = args.pop(0).replace('%2F', '/')
347 req.qsparams['node'] = node
351 req.qsparams['node'] = node
348 if args:
352 if args:
349 if 'file' in req.qsparams:
353 if 'file' in req.qsparams:
350 del req.qsparams['file']
354 del req.qsparams['file']
351 for a in args:
355 for a in args:
352 req.qsparams.add('file', a)
356 req.qsparams.add('file', a)
353
357
354 ua = req.headers.get('User-Agent', '')
358 ua = req.headers.get('User-Agent', '')
355 if cmd == 'rev' and 'mercurial' in ua:
359 if cmd == 'rev' and 'mercurial' in ua:
356 req.qsparams['style'] = 'raw'
360 req.qsparams['style'] = 'raw'
357
361
358 if cmd == 'archive':
362 if cmd == 'archive':
359 fn = req.qsparams['node']
363 fn = req.qsparams['node']
360 for type_, spec in rctx.archivespecs.iteritems():
364 for type_, spec in rctx.archivespecs.iteritems():
361 ext = spec[2]
365 ext = spec[2]
362 if fn.endswith(ext):
366 if fn.endswith(ext):
363 req.qsparams['node'] = fn[:-len(ext)]
367 req.qsparams['node'] = fn[:-len(ext)]
364 req.qsparams['type'] = type_
368 req.qsparams['type'] = type_
365 else:
369 else:
366 cmd = req.qsparams.get('cmd', '')
370 cmd = req.qsparams.get('cmd', '')
367
371
368 # process the web interface request
372 # process the web interface request
369
373
370 try:
374 try:
371 tmpl = rctx.templater(req)
375 rctx.tmpl = rctx.templater(req)
372 ctype = tmpl('mimetype', encoding=encoding.encoding)
376 ctype = rctx.tmpl('mimetype', encoding=encoding.encoding)
373 ctype = templater.stringify(ctype)
377 ctype = templater.stringify(ctype)
374
378
375 # check read permissions non-static content
379 # check read permissions non-static content
376 if cmd != 'static':
380 if cmd != 'static':
377 self.check_perm(rctx, req, None)
381 self.check_perm(rctx, req, None)
378
382
379 if cmd == '':
383 if cmd == '':
380 req.qsparams['cmd'] = tmpl.cache['default']
384 req.qsparams['cmd'] = rctx.tmpl.cache['default']
381 cmd = req.qsparams['cmd']
385 cmd = req.qsparams['cmd']
382
386
383 # Don't enable caching if using a CSP nonce because then it wouldn't
387 # Don't enable caching if using a CSP nonce because then it wouldn't
384 # be a nonce.
388 # be a nonce.
385 if rctx.configbool('web', 'cache') and not rctx.nonce:
389 if rctx.configbool('web', 'cache') and not rctx.nonce:
386 tag = 'W/"%d"' % self.mtime
390 tag = 'W/"%d"' % self.mtime
387 if req.headers.get('If-None-Match') == tag:
391 if req.headers.get('If-None-Match') == tag:
388 res.status = '304 Not Modified'
392 res.status = '304 Not Modified'
389 # Response body not allowed on 304.
393 # Response body not allowed on 304.
390 res.setbodybytes('')
394 res.setbodybytes('')
391 return res.sendresponse()
395 return res.sendresponse()
392
396
393 res.headers['ETag'] = tag
397 res.headers['ETag'] = tag
394
398
395 if cmd not in webcommands.__all__:
399 if cmd not in webcommands.__all__:
396 msg = 'no such method: %s' % cmd
400 msg = 'no such method: %s' % cmd
397 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
401 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
398 else:
402 else:
399 # Set some globals appropriate for web handlers. Commands can
403 # Set some globals appropriate for web handlers. Commands can
400 # override easily enough.
404 # override easily enough.
401 res.status = '200 Script output follows'
405 res.status = '200 Script output follows'
402 res.headers['Content-Type'] = ctype
406 res.headers['Content-Type'] = ctype
403 return getattr(webcommands, cmd)(rctx, wsgireq, tmpl)
407 return getattr(webcommands, cmd)(rctx, wsgireq, rctx.tmpl)
404
408
405 except (error.LookupError, error.RepoLookupError) as err:
409 except (error.LookupError, error.RepoLookupError) as err:
406 msg = pycompat.bytestr(err)
410 msg = pycompat.bytestr(err)
407 if (util.safehasattr(err, 'name') and
411 if (util.safehasattr(err, 'name') and
408 not isinstance(err, error.ManifestLookupError)):
412 not isinstance(err, error.ManifestLookupError)):
409 msg = 'revision not found: %s' % err.name
413 msg = 'revision not found: %s' % err.name
410
414
411 res.status = '404 Not Found'
415 res.status = '404 Not Found'
412 res.headers['Content-Type'] = ctype
416 res.headers['Content-Type'] = ctype
413 res.setbodygen(tmpl('error', error=msg))
417 return rctx.sendtemplate('error', error=msg)
414 return res.sendresponse()
415 except (error.RepoError, error.RevlogError) as e:
418 except (error.RepoError, error.RevlogError) as e:
416 res.status = '500 Internal Server Error'
419 res.status = '500 Internal Server Error'
417 res.headers['Content-Type'] = ctype
420 res.headers['Content-Type'] = ctype
418 res.setbodygen(tmpl('error', error=pycompat.bytestr(e)))
421 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
419 return res.sendresponse()
420 except ErrorResponse as e:
422 except ErrorResponse as e:
421 res.status = statusmessage(e.code, pycompat.bytestr(e))
423 res.status = statusmessage(e.code, pycompat.bytestr(e))
422 res.headers['Content-Type'] = ctype
424 res.headers['Content-Type'] = ctype
423 res.setbodygen(tmpl('error', error=pycompat.bytestr(e)))
425 return rctx.sendtemplate('error', error=pycompat.bytestr(e))
424 return res.sendresponse()
425
426
426 def check_perm(self, rctx, req, op):
427 def check_perm(self, rctx, req, op):
427 for permhook in permhooks:
428 for permhook in permhooks:
428 permhook(rctx, req, op)
429 permhook(rctx, req, op)
429
430
430 def getwebview(repo):
431 def getwebview(repo):
431 """The 'web.view' config controls changeset filter to hgweb. Possible
432 """The 'web.view' config controls changeset filter to hgweb. Possible
432 values are ``served``, ``visible`` and ``all``. Default is ``served``.
433 values are ``served``, ``visible`` and ``all``. Default is ``served``.
433 The ``served`` filter only shows changesets that can be pulled from the
434 The ``served`` filter only shows changesets that can be pulled from the
434 hgweb instance. The``visible`` filter includes secret changesets but
435 hgweb instance. The``visible`` filter includes secret changesets but
435 still excludes "hidden" one.
436 still excludes "hidden" one.
436
437
437 See the repoview module for details.
438 See the repoview module for details.
438
439
439 The option has been around undocumented since Mercurial 2.5, but no
440 The option has been around undocumented since Mercurial 2.5, but no
440 user ever asked about it. So we better keep it undocumented for now."""
441 user ever asked about it. So we better keep it undocumented for now."""
441 # experimental config: web.view
442 # experimental config: web.view
442 viewconfig = repo.ui.config('web', 'view', untrusted=True)
443 viewconfig = repo.ui.config('web', 'view', untrusted=True)
443 if viewconfig == 'all':
444 if viewconfig == 'all':
444 return repo.unfiltered()
445 return repo.unfiltered()
445 elif viewconfig in repoview.filtertable:
446 elif viewconfig in repoview.filtertable:
446 return repo.filtered(viewconfig)
447 return repo.filtered(viewconfig)
447 else:
448 else:
448 return repo.filtered('served')
449 return repo.filtered('served')
@@ -1,1509 +1,1481 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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import copy
10 import copy
11 import mimetypes
11 import mimetypes
12 import os
12 import os
13 import re
13 import re
14
14
15 from ..i18n import _
15 from ..i18n import _
16 from ..node import hex, nullid, short
16 from ..node import hex, nullid, short
17
17
18 from .common import (
18 from .common import (
19 ErrorResponse,
19 ErrorResponse,
20 HTTP_FORBIDDEN,
20 HTTP_FORBIDDEN,
21 HTTP_NOT_FOUND,
21 HTTP_NOT_FOUND,
22 get_contact,
22 get_contact,
23 paritygen,
23 paritygen,
24 staticfile,
24 staticfile,
25 )
25 )
26
26
27 from .. import (
27 from .. import (
28 archival,
28 archival,
29 dagop,
29 dagop,
30 encoding,
30 encoding,
31 error,
31 error,
32 graphmod,
32 graphmod,
33 pycompat,
33 pycompat,
34 revset,
34 revset,
35 revsetlang,
35 revsetlang,
36 scmutil,
36 scmutil,
37 smartset,
37 smartset,
38 templater,
38 templater,
39 util,
39 util,
40 )
40 )
41
41
42 from . import (
42 from . import (
43 webutil,
43 webutil,
44 )
44 )
45
45
46 __all__ = []
46 __all__ = []
47 commands = {}
47 commands = {}
48
48
49 class webcommand(object):
49 class webcommand(object):
50 """Decorator used to register a web command handler.
50 """Decorator used to register a web command handler.
51
51
52 The decorator takes as its positional arguments the name/path the
52 The decorator takes as its positional arguments the name/path the
53 command should be accessible under.
53 command should be accessible under.
54
54
55 When called, functions receive as arguments a ``requestcontext``,
55 When called, functions receive as arguments a ``requestcontext``,
56 ``wsgirequest``, and a templater instance for generatoring output.
56 ``wsgirequest``, and a templater instance for generatoring output.
57 The functions should populate the ``rctx.res`` object with details
57 The functions should populate the ``rctx.res`` object with details
58 about the HTTP response.
58 about the HTTP response.
59
59
60 The function returns a generator to be consumed by the WSGI application.
60 The function returns a generator to be consumed by the WSGI application.
61 For most commands, this should be the result from
61 For most commands, this should be the result from
62 ``web.res.sendresponse()``.
62 ``web.res.sendresponse()``. Many commands will call ``web.sendtemplate()``
63 to render a template.
63
64
64 Usage:
65 Usage:
65
66
66 @webcommand('mycommand')
67 @webcommand('mycommand')
67 def mycommand(web, req, tmpl):
68 def mycommand(web, req, tmpl):
68 pass
69 pass
69 """
70 """
70
71
71 def __init__(self, name):
72 def __init__(self, name):
72 self.name = name
73 self.name = name
73
74
74 def __call__(self, func):
75 def __call__(self, func):
75 __all__.append(self.name)
76 __all__.append(self.name)
76 commands[self.name] = func
77 commands[self.name] = func
77 return func
78 return func
78
79
79 @webcommand('log')
80 @webcommand('log')
80 def log(web, req, tmpl):
81 def log(web, req, tmpl):
81 """
82 """
82 /log[/{revision}[/{path}]]
83 /log[/{revision}[/{path}]]
83 --------------------------
84 --------------------------
84
85
85 Show repository or file history.
86 Show repository or file history.
86
87
87 For URLs of the form ``/log/{revision}``, a list of changesets starting at
88 For URLs of the form ``/log/{revision}``, a list of changesets starting at
88 the specified changeset identifier is shown. If ``{revision}`` is not
89 the specified changeset identifier is shown. If ``{revision}`` is not
89 defined, the default is ``tip``. This form is equivalent to the
90 defined, the default is ``tip``. This form is equivalent to the
90 ``changelog`` handler.
91 ``changelog`` handler.
91
92
92 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
93 For URLs of the form ``/log/{revision}/{file}``, the history for a specific
93 file will be shown. This form is equivalent to the ``filelog`` handler.
94 file will be shown. This form is equivalent to the ``filelog`` handler.
94 """
95 """
95
96
96 if web.req.qsparams.get('file'):
97 if web.req.qsparams.get('file'):
97 return filelog(web, req, tmpl)
98 return filelog(web, req, tmpl)
98 else:
99 else:
99 return changelog(web, req, tmpl)
100 return changelog(web, req, tmpl)
100
101
101 @webcommand('rawfile')
102 @webcommand('rawfile')
102 def rawfile(web, req, tmpl):
103 def rawfile(web, req, tmpl):
103 guessmime = web.configbool('web', 'guessmime')
104 guessmime = web.configbool('web', 'guessmime')
104
105
105 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
106 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
106 if not path:
107 if not path:
107 return manifest(web, req, tmpl)
108 return manifest(web, req, tmpl)
108
109
109 try:
110 try:
110 fctx = webutil.filectx(web.repo, req)
111 fctx = webutil.filectx(web.repo, req)
111 except error.LookupError as inst:
112 except error.LookupError as inst:
112 try:
113 try:
113 return manifest(web, req, tmpl)
114 return manifest(web, req, tmpl)
114 except ErrorResponse:
115 except ErrorResponse:
115 raise inst
116 raise inst
116
117
117 path = fctx.path()
118 path = fctx.path()
118 text = fctx.data()
119 text = fctx.data()
119 mt = 'application/binary'
120 mt = 'application/binary'
120 if guessmime:
121 if guessmime:
121 mt = mimetypes.guess_type(path)[0]
122 mt = mimetypes.guess_type(path)[0]
122 if mt is None:
123 if mt is None:
123 if util.binary(text):
124 if util.binary(text):
124 mt = 'application/binary'
125 mt = 'application/binary'
125 else:
126 else:
126 mt = 'text/plain'
127 mt = 'text/plain'
127 if mt.startswith('text/'):
128 if mt.startswith('text/'):
128 mt += '; charset="%s"' % encoding.encoding
129 mt += '; charset="%s"' % encoding.encoding
129
130
130 web.res.headers['Content-Type'] = mt
131 web.res.headers['Content-Type'] = mt
131 filename = (path.rpartition('/')[-1]
132 filename = (path.rpartition('/')[-1]
132 .replace('\\', '\\\\').replace('"', '\\"'))
133 .replace('\\', '\\\\').replace('"', '\\"'))
133 web.res.headers['Content-Disposition'] = 'inline; filename="%s"' % filename
134 web.res.headers['Content-Disposition'] = 'inline; filename="%s"' % filename
134 web.res.setbodybytes(text)
135 web.res.setbodybytes(text)
135 return web.res.sendresponse()
136 return web.res.sendresponse()
136
137
137 def _filerevision(web, req, tmpl, fctx):
138 def _filerevision(web, req, tmpl, fctx):
138 f = fctx.path()
139 f = fctx.path()
139 text = fctx.data()
140 text = fctx.data()
140 parity = paritygen(web.stripecount)
141 parity = paritygen(web.stripecount)
141 ishead = fctx.filerev() in fctx.filelog().headrevs()
142 ishead = fctx.filerev() in fctx.filelog().headrevs()
142
143
143 if util.binary(text):
144 if util.binary(text):
144 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
145 mt = mimetypes.guess_type(f)[0] or 'application/octet-stream'
145 text = '(binary:%s)' % mt
146 text = '(binary:%s)' % mt
146
147
147 def lines():
148 def lines():
148 for lineno, t in enumerate(text.splitlines(True)):
149 for lineno, t in enumerate(text.splitlines(True)):
149 yield {"line": t,
150 yield {"line": t,
150 "lineid": "l%d" % (lineno + 1),
151 "lineid": "l%d" % (lineno + 1),
151 "linenumber": "% 6d" % (lineno + 1),
152 "linenumber": "% 6d" % (lineno + 1),
152 "parity": next(parity)}
153 "parity": next(parity)}
153
154
154 web.res.setbodygen(tmpl(
155 return web.sendtemplate(
155 'filerevision',
156 'filerevision',
156 file=f,
157 file=f,
157 path=webutil.up(f),
158 path=webutil.up(f),
158 text=lines(),
159 text=lines(),
159 symrev=webutil.symrevorshortnode(req, fctx),
160 symrev=webutil.symrevorshortnode(req, fctx),
160 rename=webutil.renamelink(fctx),
161 rename=webutil.renamelink(fctx),
161 permissions=fctx.manifest().flags(f),
162 permissions=fctx.manifest().flags(f),
162 ishead=int(ishead),
163 ishead=int(ishead),
163 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))))
164 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
164
165 return web.res.sendresponse()
166
165
167 @webcommand('file')
166 @webcommand('file')
168 def file(web, req, tmpl):
167 def file(web, req, tmpl):
169 """
168 """
170 /file/{revision}[/{path}]
169 /file/{revision}[/{path}]
171 -------------------------
170 -------------------------
172
171
173 Show information about a directory or file in the repository.
172 Show information about a directory or file in the repository.
174
173
175 Info about the ``path`` given as a URL parameter will be rendered.
174 Info about the ``path`` given as a URL parameter will be rendered.
176
175
177 If ``path`` is a directory, information about the entries in that
176 If ``path`` is a directory, information about the entries in that
178 directory will be rendered. This form is equivalent to the ``manifest``
177 directory will be rendered. This form is equivalent to the ``manifest``
179 handler.
178 handler.
180
179
181 If ``path`` is a file, information about that file will be shown via
180 If ``path`` is a file, information about that file will be shown via
182 the ``filerevision`` template.
181 the ``filerevision`` template.
183
182
184 If ``path`` is not defined, information about the root directory will
183 If ``path`` is not defined, information about the root directory will
185 be rendered.
184 be rendered.
186 """
185 """
187 if web.req.qsparams.get('style') == 'raw':
186 if web.req.qsparams.get('style') == 'raw':
188 return rawfile(web, req, tmpl)
187 return rawfile(web, req, tmpl)
189
188
190 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
189 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
191 if not path:
190 if not path:
192 return manifest(web, req, tmpl)
191 return manifest(web, req, tmpl)
193 try:
192 try:
194 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
193 return _filerevision(web, req, tmpl, webutil.filectx(web.repo, req))
195 except error.LookupError as inst:
194 except error.LookupError as inst:
196 try:
195 try:
197 return manifest(web, req, tmpl)
196 return manifest(web, req, tmpl)
198 except ErrorResponse:
197 except ErrorResponse:
199 raise inst
198 raise inst
200
199
201 def _search(web, tmpl):
200 def _search(web, tmpl):
202 MODE_REVISION = 'rev'
201 MODE_REVISION = 'rev'
203 MODE_KEYWORD = 'keyword'
202 MODE_KEYWORD = 'keyword'
204 MODE_REVSET = 'revset'
203 MODE_REVSET = 'revset'
205
204
206 def revsearch(ctx):
205 def revsearch(ctx):
207 yield ctx
206 yield ctx
208
207
209 def keywordsearch(query):
208 def keywordsearch(query):
210 lower = encoding.lower
209 lower = encoding.lower
211 qw = lower(query).split()
210 qw = lower(query).split()
212
211
213 def revgen():
212 def revgen():
214 cl = web.repo.changelog
213 cl = web.repo.changelog
215 for i in xrange(len(web.repo) - 1, 0, -100):
214 for i in xrange(len(web.repo) - 1, 0, -100):
216 l = []
215 l = []
217 for j in cl.revs(max(0, i - 99), i):
216 for j in cl.revs(max(0, i - 99), i):
218 ctx = web.repo[j]
217 ctx = web.repo[j]
219 l.append(ctx)
218 l.append(ctx)
220 l.reverse()
219 l.reverse()
221 for e in l:
220 for e in l:
222 yield e
221 yield e
223
222
224 for ctx in revgen():
223 for ctx in revgen():
225 miss = 0
224 miss = 0
226 for q in qw:
225 for q in qw:
227 if not (q in lower(ctx.user()) or
226 if not (q in lower(ctx.user()) or
228 q in lower(ctx.description()) or
227 q in lower(ctx.description()) or
229 q in lower(" ".join(ctx.files()))):
228 q in lower(" ".join(ctx.files()))):
230 miss = 1
229 miss = 1
231 break
230 break
232 if miss:
231 if miss:
233 continue
232 continue
234
233
235 yield ctx
234 yield ctx
236
235
237 def revsetsearch(revs):
236 def revsetsearch(revs):
238 for r in revs:
237 for r in revs:
239 yield web.repo[r]
238 yield web.repo[r]
240
239
241 searchfuncs = {
240 searchfuncs = {
242 MODE_REVISION: (revsearch, 'exact revision search'),
241 MODE_REVISION: (revsearch, 'exact revision search'),
243 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
242 MODE_KEYWORD: (keywordsearch, 'literal keyword search'),
244 MODE_REVSET: (revsetsearch, 'revset expression search'),
243 MODE_REVSET: (revsetsearch, 'revset expression search'),
245 }
244 }
246
245
247 def getsearchmode(query):
246 def getsearchmode(query):
248 try:
247 try:
249 ctx = web.repo[query]
248 ctx = web.repo[query]
250 except (error.RepoError, error.LookupError):
249 except (error.RepoError, error.LookupError):
251 # query is not an exact revision pointer, need to
250 # query is not an exact revision pointer, need to
252 # decide if it's a revset expression or keywords
251 # decide if it's a revset expression or keywords
253 pass
252 pass
254 else:
253 else:
255 return MODE_REVISION, ctx
254 return MODE_REVISION, ctx
256
255
257 revdef = 'reverse(%s)' % query
256 revdef = 'reverse(%s)' % query
258 try:
257 try:
259 tree = revsetlang.parse(revdef)
258 tree = revsetlang.parse(revdef)
260 except error.ParseError:
259 except error.ParseError:
261 # can't parse to a revset tree
260 # can't parse to a revset tree
262 return MODE_KEYWORD, query
261 return MODE_KEYWORD, query
263
262
264 if revsetlang.depth(tree) <= 2:
263 if revsetlang.depth(tree) <= 2:
265 # no revset syntax used
264 # no revset syntax used
266 return MODE_KEYWORD, query
265 return MODE_KEYWORD, query
267
266
268 if any((token, (value or '')[:3]) == ('string', 're:')
267 if any((token, (value or '')[:3]) == ('string', 're:')
269 for token, value, pos in revsetlang.tokenize(revdef)):
268 for token, value, pos in revsetlang.tokenize(revdef)):
270 return MODE_KEYWORD, query
269 return MODE_KEYWORD, query
271
270
272 funcsused = revsetlang.funcsused(tree)
271 funcsused = revsetlang.funcsused(tree)
273 if not funcsused.issubset(revset.safesymbols):
272 if not funcsused.issubset(revset.safesymbols):
274 return MODE_KEYWORD, query
273 return MODE_KEYWORD, query
275
274
276 mfunc = revset.match(web.repo.ui, revdef, repo=web.repo)
275 mfunc = revset.match(web.repo.ui, revdef, repo=web.repo)
277 try:
276 try:
278 revs = mfunc(web.repo)
277 revs = mfunc(web.repo)
279 return MODE_REVSET, revs
278 return MODE_REVSET, revs
280 # ParseError: wrongly placed tokens, wrongs arguments, etc
279 # ParseError: wrongly placed tokens, wrongs arguments, etc
281 # RepoLookupError: no such revision, e.g. in 'revision:'
280 # RepoLookupError: no such revision, e.g. in 'revision:'
282 # Abort: bookmark/tag not exists
281 # Abort: bookmark/tag not exists
283 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
282 # LookupError: ambiguous identifier, e.g. in '(bc)' on a large repo
284 except (error.ParseError, error.RepoLookupError, error.Abort,
283 except (error.ParseError, error.RepoLookupError, error.Abort,
285 LookupError):
284 LookupError):
286 return MODE_KEYWORD, query
285 return MODE_KEYWORD, query
287
286
288 def changelist(**map):
287 def changelist(**map):
289 count = 0
288 count = 0
290
289
291 for ctx in searchfunc[0](funcarg):
290 for ctx in searchfunc[0](funcarg):
292 count += 1
291 count += 1
293 n = ctx.node()
292 n = ctx.node()
294 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
293 showtags = webutil.showtag(web.repo, tmpl, 'changelogtag', n)
295 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
294 files = webutil.listfilediffs(tmpl, ctx.files(), n, web.maxfiles)
296
295
297 yield tmpl('searchentry',
296 yield tmpl('searchentry',
298 parity=next(parity),
297 parity=next(parity),
299 changelogtag=showtags,
298 changelogtag=showtags,
300 files=files,
299 files=files,
301 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
300 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
302
301
303 if count >= revcount:
302 if count >= revcount:
304 break
303 break
305
304
306 query = web.req.qsparams['rev']
305 query = web.req.qsparams['rev']
307 revcount = web.maxchanges
306 revcount = web.maxchanges
308 if 'revcount' in web.req.qsparams:
307 if 'revcount' in web.req.qsparams:
309 try:
308 try:
310 revcount = int(web.req.qsparams.get('revcount', revcount))
309 revcount = int(web.req.qsparams.get('revcount', revcount))
311 revcount = max(revcount, 1)
310 revcount = max(revcount, 1)
312 tmpl.defaults['sessionvars']['revcount'] = revcount
311 tmpl.defaults['sessionvars']['revcount'] = revcount
313 except ValueError:
312 except ValueError:
314 pass
313 pass
315
314
316 lessvars = copy.copy(tmpl.defaults['sessionvars'])
315 lessvars = copy.copy(tmpl.defaults['sessionvars'])
317 lessvars['revcount'] = max(revcount // 2, 1)
316 lessvars['revcount'] = max(revcount // 2, 1)
318 lessvars['rev'] = query
317 lessvars['rev'] = query
319 morevars = copy.copy(tmpl.defaults['sessionvars'])
318 morevars = copy.copy(tmpl.defaults['sessionvars'])
320 morevars['revcount'] = revcount * 2
319 morevars['revcount'] = revcount * 2
321 morevars['rev'] = query
320 morevars['rev'] = query
322
321
323 mode, funcarg = getsearchmode(query)
322 mode, funcarg = getsearchmode(query)
324
323
325 if 'forcekw' in web.req.qsparams:
324 if 'forcekw' in web.req.qsparams:
326 showforcekw = ''
325 showforcekw = ''
327 showunforcekw = searchfuncs[mode][1]
326 showunforcekw = searchfuncs[mode][1]
328 mode = MODE_KEYWORD
327 mode = MODE_KEYWORD
329 funcarg = query
328 funcarg = query
330 else:
329 else:
331 if mode != MODE_KEYWORD:
330 if mode != MODE_KEYWORD:
332 showforcekw = searchfuncs[MODE_KEYWORD][1]
331 showforcekw = searchfuncs[MODE_KEYWORD][1]
333 else:
332 else:
334 showforcekw = ''
333 showforcekw = ''
335 showunforcekw = ''
334 showunforcekw = ''
336
335
337 searchfunc = searchfuncs[mode]
336 searchfunc = searchfuncs[mode]
338
337
339 tip = web.repo['tip']
338 tip = web.repo['tip']
340 parity = paritygen(web.stripecount)
339 parity = paritygen(web.stripecount)
341
340
342 web.res.setbodygen(tmpl(
341 return web.sendtemplate(
343 'search',
342 'search',
344 query=query,
343 query=query,
345 node=tip.hex(),
344 node=tip.hex(),
346 symrev='tip',
345 symrev='tip',
347 entries=changelist,
346 entries=changelist,
348 archives=web.archivelist('tip'),
347 archives=web.archivelist('tip'),
349 morevars=morevars,
348 morevars=morevars,
350 lessvars=lessvars,
349 lessvars=lessvars,
351 modedesc=searchfunc[1],
350 modedesc=searchfunc[1],
352 showforcekw=showforcekw,
351 showforcekw=showforcekw,
353 showunforcekw=showunforcekw))
352 showunforcekw=showunforcekw)
354
355 return web.res.sendresponse()
356
353
357 @webcommand('changelog')
354 @webcommand('changelog')
358 def changelog(web, req, tmpl, shortlog=False):
355 def changelog(web, req, tmpl, shortlog=False):
359 """
356 """
360 /changelog[/{revision}]
357 /changelog[/{revision}]
361 -----------------------
358 -----------------------
362
359
363 Show information about multiple changesets.
360 Show information about multiple changesets.
364
361
365 If the optional ``revision`` URL argument is absent, information about
362 If the optional ``revision`` URL argument is absent, information about
366 all changesets starting at ``tip`` will be rendered. If the ``revision``
363 all changesets starting at ``tip`` will be rendered. If the ``revision``
367 argument is present, changesets will be shown starting from the specified
364 argument is present, changesets will be shown starting from the specified
368 revision.
365 revision.
369
366
370 If ``revision`` is absent, the ``rev`` query string argument may be
367 If ``revision`` is absent, the ``rev`` query string argument may be
371 defined. This will perform a search for changesets.
368 defined. This will perform a search for changesets.
372
369
373 The argument for ``rev`` can be a single revision, a revision set,
370 The argument for ``rev`` can be a single revision, a revision set,
374 or a literal keyword to search for in changeset data (equivalent to
371 or a literal keyword to search for in changeset data (equivalent to
375 :hg:`log -k`).
372 :hg:`log -k`).
376
373
377 The ``revcount`` query string argument defines the maximum numbers of
374 The ``revcount`` query string argument defines the maximum numbers of
378 changesets to render.
375 changesets to render.
379
376
380 For non-searches, the ``changelog`` template will be rendered.
377 For non-searches, the ``changelog`` template will be rendered.
381 """
378 """
382
379
383 query = ''
380 query = ''
384 if 'node' in web.req.qsparams:
381 if 'node' in web.req.qsparams:
385 ctx = webutil.changectx(web.repo, req)
382 ctx = webutil.changectx(web.repo, req)
386 symrev = webutil.symrevorshortnode(req, ctx)
383 symrev = webutil.symrevorshortnode(req, ctx)
387 elif 'rev' in web.req.qsparams:
384 elif 'rev' in web.req.qsparams:
388 return _search(web, tmpl)
385 return _search(web, tmpl)
389 else:
386 else:
390 ctx = web.repo['tip']
387 ctx = web.repo['tip']
391 symrev = 'tip'
388 symrev = 'tip'
392
389
393 def changelist():
390 def changelist():
394 revs = []
391 revs = []
395 if pos != -1:
392 if pos != -1:
396 revs = web.repo.changelog.revs(pos, 0)
393 revs = web.repo.changelog.revs(pos, 0)
397 curcount = 0
394 curcount = 0
398 for rev in revs:
395 for rev in revs:
399 curcount += 1
396 curcount += 1
400 if curcount > revcount + 1:
397 if curcount > revcount + 1:
401 break
398 break
402
399
403 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
400 entry = webutil.changelistentry(web, web.repo[rev], tmpl)
404 entry['parity'] = next(parity)
401 entry['parity'] = next(parity)
405 yield entry
402 yield entry
406
403
407 if shortlog:
404 if shortlog:
408 revcount = web.maxshortchanges
405 revcount = web.maxshortchanges
409 else:
406 else:
410 revcount = web.maxchanges
407 revcount = web.maxchanges
411
408
412 if 'revcount' in web.req.qsparams:
409 if 'revcount' in web.req.qsparams:
413 try:
410 try:
414 revcount = int(web.req.qsparams.get('revcount', revcount))
411 revcount = int(web.req.qsparams.get('revcount', revcount))
415 revcount = max(revcount, 1)
412 revcount = max(revcount, 1)
416 tmpl.defaults['sessionvars']['revcount'] = revcount
413 tmpl.defaults['sessionvars']['revcount'] = revcount
417 except ValueError:
414 except ValueError:
418 pass
415 pass
419
416
420 lessvars = copy.copy(tmpl.defaults['sessionvars'])
417 lessvars = copy.copy(tmpl.defaults['sessionvars'])
421 lessvars['revcount'] = max(revcount // 2, 1)
418 lessvars['revcount'] = max(revcount // 2, 1)
422 morevars = copy.copy(tmpl.defaults['sessionvars'])
419 morevars = copy.copy(tmpl.defaults['sessionvars'])
423 morevars['revcount'] = revcount * 2
420 morevars['revcount'] = revcount * 2
424
421
425 count = len(web.repo)
422 count = len(web.repo)
426 pos = ctx.rev()
423 pos = ctx.rev()
427 parity = paritygen(web.stripecount)
424 parity = paritygen(web.stripecount)
428
425
429 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
426 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
430
427
431 entries = list(changelist())
428 entries = list(changelist())
432 latestentry = entries[:1]
429 latestentry = entries[:1]
433 if len(entries) > revcount:
430 if len(entries) > revcount:
434 nextentry = entries[-1:]
431 nextentry = entries[-1:]
435 entries = entries[:-1]
432 entries = entries[:-1]
436 else:
433 else:
437 nextentry = []
434 nextentry = []
438
435
439 web.res.setbodygen(tmpl(
436 return web.sendtemplate(
440 'shortlog' if shortlog else 'changelog',
437 'shortlog' if shortlog else 'changelog',
441 changenav=changenav,
438 changenav=changenav,
442 node=ctx.hex(),
439 node=ctx.hex(),
443 rev=pos,
440 rev=pos,
444 symrev=symrev,
441 symrev=symrev,
445 changesets=count,
442 changesets=count,
446 entries=entries,
443 entries=entries,
447 latestentry=latestentry,
444 latestentry=latestentry,
448 nextentry=nextentry,
445 nextentry=nextentry,
449 archives=web.archivelist('tip'),
446 archives=web.archivelist('tip'),
450 revcount=revcount,
447 revcount=revcount,
451 morevars=morevars,
448 morevars=morevars,
452 lessvars=lessvars,
449 lessvars=lessvars,
453 query=query))
450 query=query)
454
455 return web.res.sendresponse()
456
451
457 @webcommand('shortlog')
452 @webcommand('shortlog')
458 def shortlog(web, req, tmpl):
453 def shortlog(web, req, tmpl):
459 """
454 """
460 /shortlog
455 /shortlog
461 ---------
456 ---------
462
457
463 Show basic information about a set of changesets.
458 Show basic information about a set of changesets.
464
459
465 This accepts the same parameters as the ``changelog`` handler. The only
460 This accepts the same parameters as the ``changelog`` handler. The only
466 difference is the ``shortlog`` template will be rendered instead of the
461 difference is the ``shortlog`` template will be rendered instead of the
467 ``changelog`` template.
462 ``changelog`` template.
468 """
463 """
469 return changelog(web, req, tmpl, shortlog=True)
464 return changelog(web, req, tmpl, shortlog=True)
470
465
471 @webcommand('changeset')
466 @webcommand('changeset')
472 def changeset(web, req, tmpl):
467 def changeset(web, req, tmpl):
473 """
468 """
474 /changeset[/{revision}]
469 /changeset[/{revision}]
475 -----------------------
470 -----------------------
476
471
477 Show information about a single changeset.
472 Show information about a single changeset.
478
473
479 A URL path argument is the changeset identifier to show. See ``hg help
474 A URL path argument is the changeset identifier to show. See ``hg help
480 revisions`` for possible values. If not defined, the ``tip`` changeset
475 revisions`` for possible values. If not defined, the ``tip`` changeset
481 will be shown.
476 will be shown.
482
477
483 The ``changeset`` template is rendered. Contents of the ``changesettag``,
478 The ``changeset`` template is rendered. Contents of the ``changesettag``,
484 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
479 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
485 templates related to diffs may all be used to produce the output.
480 templates related to diffs may all be used to produce the output.
486 """
481 """
487 ctx = webutil.changectx(web.repo, req)
482 ctx = webutil.changectx(web.repo, req)
488 web.res.setbodygen(tmpl('changeset',
483
489 **webutil.changesetentry(web, req, tmpl, ctx)))
484 return web.sendtemplate(
490 return web.res.sendresponse()
485 'changeset',
486 **webutil.changesetentry(web, req, tmpl, ctx))
491
487
492 rev = webcommand('rev')(changeset)
488 rev = webcommand('rev')(changeset)
493
489
494 def decodepath(path):
490 def decodepath(path):
495 """Hook for mapping a path in the repository to a path in the
491 """Hook for mapping a path in the repository to a path in the
496 working copy.
492 working copy.
497
493
498 Extensions (e.g., largefiles) can override this to remap files in
494 Extensions (e.g., largefiles) can override this to remap files in
499 the virtual file system presented by the manifest command below."""
495 the virtual file system presented by the manifest command below."""
500 return path
496 return path
501
497
502 @webcommand('manifest')
498 @webcommand('manifest')
503 def manifest(web, req, tmpl):
499 def manifest(web, req, tmpl):
504 """
500 """
505 /manifest[/{revision}[/{path}]]
501 /manifest[/{revision}[/{path}]]
506 -------------------------------
502 -------------------------------
507
503
508 Show information about a directory.
504 Show information about a directory.
509
505
510 If the URL path arguments are omitted, information about the root
506 If the URL path arguments are omitted, information about the root
511 directory for the ``tip`` changeset will be shown.
507 directory for the ``tip`` changeset will be shown.
512
508
513 Because this handler can only show information for directories, it
509 Because this handler can only show information for directories, it
514 is recommended to use the ``file`` handler instead, as it can handle both
510 is recommended to use the ``file`` handler instead, as it can handle both
515 directories and files.
511 directories and files.
516
512
517 The ``manifest`` template will be rendered for this handler.
513 The ``manifest`` template will be rendered for this handler.
518 """
514 """
519 if 'node' in web.req.qsparams:
515 if 'node' in web.req.qsparams:
520 ctx = webutil.changectx(web.repo, req)
516 ctx = webutil.changectx(web.repo, req)
521 symrev = webutil.symrevorshortnode(req, ctx)
517 symrev = webutil.symrevorshortnode(req, ctx)
522 else:
518 else:
523 ctx = web.repo['tip']
519 ctx = web.repo['tip']
524 symrev = 'tip'
520 symrev = 'tip'
525 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
521 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
526 mf = ctx.manifest()
522 mf = ctx.manifest()
527 node = ctx.node()
523 node = ctx.node()
528
524
529 files = {}
525 files = {}
530 dirs = {}
526 dirs = {}
531 parity = paritygen(web.stripecount)
527 parity = paritygen(web.stripecount)
532
528
533 if path and path[-1:] != "/":
529 if path and path[-1:] != "/":
534 path += "/"
530 path += "/"
535 l = len(path)
531 l = len(path)
536 abspath = "/" + path
532 abspath = "/" + path
537
533
538 for full, n in mf.iteritems():
534 for full, n in mf.iteritems():
539 # the virtual path (working copy path) used for the full
535 # the virtual path (working copy path) used for the full
540 # (repository) path
536 # (repository) path
541 f = decodepath(full)
537 f = decodepath(full)
542
538
543 if f[:l] != path:
539 if f[:l] != path:
544 continue
540 continue
545 remain = f[l:]
541 remain = f[l:]
546 elements = remain.split('/')
542 elements = remain.split('/')
547 if len(elements) == 1:
543 if len(elements) == 1:
548 files[remain] = full
544 files[remain] = full
549 else:
545 else:
550 h = dirs # need to retain ref to dirs (root)
546 h = dirs # need to retain ref to dirs (root)
551 for elem in elements[0:-1]:
547 for elem in elements[0:-1]:
552 if elem not in h:
548 if elem not in h:
553 h[elem] = {}
549 h[elem] = {}
554 h = h[elem]
550 h = h[elem]
555 if len(h) > 1:
551 if len(h) > 1:
556 break
552 break
557 h[None] = None # denotes files present
553 h[None] = None # denotes files present
558
554
559 if mf and not files and not dirs:
555 if mf and not files and not dirs:
560 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
556 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
561
557
562 def filelist(**map):
558 def filelist(**map):
563 for f in sorted(files):
559 for f in sorted(files):
564 full = files[f]
560 full = files[f]
565
561
566 fctx = ctx.filectx(full)
562 fctx = ctx.filectx(full)
567 yield {"file": full,
563 yield {"file": full,
568 "parity": next(parity),
564 "parity": next(parity),
569 "basename": f,
565 "basename": f,
570 "date": fctx.date(),
566 "date": fctx.date(),
571 "size": fctx.size(),
567 "size": fctx.size(),
572 "permissions": mf.flags(full)}
568 "permissions": mf.flags(full)}
573
569
574 def dirlist(**map):
570 def dirlist(**map):
575 for d in sorted(dirs):
571 for d in sorted(dirs):
576
572
577 emptydirs = []
573 emptydirs = []
578 h = dirs[d]
574 h = dirs[d]
579 while isinstance(h, dict) and len(h) == 1:
575 while isinstance(h, dict) and len(h) == 1:
580 k, v = next(iter(h.items()))
576 k, v = next(iter(h.items()))
581 if v:
577 if v:
582 emptydirs.append(k)
578 emptydirs.append(k)
583 h = v
579 h = v
584
580
585 path = "%s%s" % (abspath, d)
581 path = "%s%s" % (abspath, d)
586 yield {"parity": next(parity),
582 yield {"parity": next(parity),
587 "path": path,
583 "path": path,
588 "emptydirs": "/".join(emptydirs),
584 "emptydirs": "/".join(emptydirs),
589 "basename": d}
585 "basename": d}
590
586
591 web.res.setbodygen(tmpl(
587 return web.sendtemplate(
592 'manifest',
588 'manifest',
593 symrev=symrev,
589 symrev=symrev,
594 path=abspath,
590 path=abspath,
595 up=webutil.up(abspath),
591 up=webutil.up(abspath),
596 upparity=next(parity),
592 upparity=next(parity),
597 fentries=filelist,
593 fentries=filelist,
598 dentries=dirlist,
594 dentries=dirlist,
599 archives=web.archivelist(hex(node)),
595 archives=web.archivelist(hex(node)),
600 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
596 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
601
602 return web.res.sendresponse()
603
597
604 @webcommand('tags')
598 @webcommand('tags')
605 def tags(web, req, tmpl):
599 def tags(web, req, tmpl):
606 """
600 """
607 /tags
601 /tags
608 -----
602 -----
609
603
610 Show information about tags.
604 Show information about tags.
611
605
612 No arguments are accepted.
606 No arguments are accepted.
613
607
614 The ``tags`` template is rendered.
608 The ``tags`` template is rendered.
615 """
609 """
616 i = list(reversed(web.repo.tagslist()))
610 i = list(reversed(web.repo.tagslist()))
617 parity = paritygen(web.stripecount)
611 parity = paritygen(web.stripecount)
618
612
619 def entries(notip, latestonly, **map):
613 def entries(notip, latestonly, **map):
620 t = i
614 t = i
621 if notip:
615 if notip:
622 t = [(k, n) for k, n in i if k != "tip"]
616 t = [(k, n) for k, n in i if k != "tip"]
623 if latestonly:
617 if latestonly:
624 t = t[:1]
618 t = t[:1]
625 for k, n in t:
619 for k, n in t:
626 yield {"parity": next(parity),
620 yield {"parity": next(parity),
627 "tag": k,
621 "tag": k,
628 "date": web.repo[n].date(),
622 "date": web.repo[n].date(),
629 "node": hex(n)}
623 "node": hex(n)}
630
624
631 web.res.setbodygen(tmpl(
625 return web.sendtemplate(
632 'tags',
626 'tags',
633 node=hex(web.repo.changelog.tip()),
627 node=hex(web.repo.changelog.tip()),
634 entries=lambda **x: entries(False, False, **x),
628 entries=lambda **x: entries(False, False, **x),
635 entriesnotip=lambda **x: entries(True, False, **x),
629 entriesnotip=lambda **x: entries(True, False, **x),
636 latestentry=lambda **x: entries(True, True, **x)))
630 latestentry=lambda **x: entries(True, True, **x))
637
638 return web.res.sendresponse()
639
631
640 @webcommand('bookmarks')
632 @webcommand('bookmarks')
641 def bookmarks(web, req, tmpl):
633 def bookmarks(web, req, tmpl):
642 """
634 """
643 /bookmarks
635 /bookmarks
644 ----------
636 ----------
645
637
646 Show information about bookmarks.
638 Show information about bookmarks.
647
639
648 No arguments are accepted.
640 No arguments are accepted.
649
641
650 The ``bookmarks`` template is rendered.
642 The ``bookmarks`` template is rendered.
651 """
643 """
652 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
644 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
653 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
645 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
654 i = sorted(i, key=sortkey, reverse=True)
646 i = sorted(i, key=sortkey, reverse=True)
655 parity = paritygen(web.stripecount)
647 parity = paritygen(web.stripecount)
656
648
657 def entries(latestonly, **map):
649 def entries(latestonly, **map):
658 t = i
650 t = i
659 if latestonly:
651 if latestonly:
660 t = i[:1]
652 t = i[:1]
661 for k, n in t:
653 for k, n in t:
662 yield {"parity": next(parity),
654 yield {"parity": next(parity),
663 "bookmark": k,
655 "bookmark": k,
664 "date": web.repo[n].date(),
656 "date": web.repo[n].date(),
665 "node": hex(n)}
657 "node": hex(n)}
666
658
667 if i:
659 if i:
668 latestrev = i[0][1]
660 latestrev = i[0][1]
669 else:
661 else:
670 latestrev = -1
662 latestrev = -1
671
663
672 web.res.setbodygen(tmpl(
664 return web.sendtemplate(
673 'bookmarks',
665 'bookmarks',
674 node=hex(web.repo.changelog.tip()),
666 node=hex(web.repo.changelog.tip()),
675 lastchange=[{'date': web.repo[latestrev].date()}],
667 lastchange=[{'date': web.repo[latestrev].date()}],
676 entries=lambda **x: entries(latestonly=False, **x),
668 entries=lambda **x: entries(latestonly=False, **x),
677 latestentry=lambda **x: entries(latestonly=True, **x)))
669 latestentry=lambda **x: entries(latestonly=True, **x))
678
679 return web.res.sendresponse()
680
670
681 @webcommand('branches')
671 @webcommand('branches')
682 def branches(web, req, tmpl):
672 def branches(web, req, tmpl):
683 """
673 """
684 /branches
674 /branches
685 ---------
675 ---------
686
676
687 Show information about branches.
677 Show information about branches.
688
678
689 All known branches are contained in the output, even closed branches.
679 All known branches are contained in the output, even closed branches.
690
680
691 No arguments are accepted.
681 No arguments are accepted.
692
682
693 The ``branches`` template is rendered.
683 The ``branches`` template is rendered.
694 """
684 """
695 entries = webutil.branchentries(web.repo, web.stripecount)
685 entries = webutil.branchentries(web.repo, web.stripecount)
696 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
686 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
697
687
698 web.res.setbodygen(tmpl(
688 return web.sendtemplate(
699 'branches',
689 'branches',
700 node=hex(web.repo.changelog.tip()),
690 node=hex(web.repo.changelog.tip()),
701 entries=entries,
691 entries=entries,
702 latestentry=latestentry))
692 latestentry=latestentry)
703
704 return web.res.sendresponse()
705
693
706 @webcommand('summary')
694 @webcommand('summary')
707 def summary(web, req, tmpl):
695 def summary(web, req, tmpl):
708 """
696 """
709 /summary
697 /summary
710 --------
698 --------
711
699
712 Show a summary of repository state.
700 Show a summary of repository state.
713
701
714 Information about the latest changesets, bookmarks, tags, and branches
702 Information about the latest changesets, bookmarks, tags, and branches
715 is captured by this handler.
703 is captured by this handler.
716
704
717 The ``summary`` template is rendered.
705 The ``summary`` template is rendered.
718 """
706 """
719 i = reversed(web.repo.tagslist())
707 i = reversed(web.repo.tagslist())
720
708
721 def tagentries(**map):
709 def tagentries(**map):
722 parity = paritygen(web.stripecount)
710 parity = paritygen(web.stripecount)
723 count = 0
711 count = 0
724 for k, n in i:
712 for k, n in i:
725 if k == "tip": # skip tip
713 if k == "tip": # skip tip
726 continue
714 continue
727
715
728 count += 1
716 count += 1
729 if count > 10: # limit to 10 tags
717 if count > 10: # limit to 10 tags
730 break
718 break
731
719
732 yield tmpl("tagentry",
720 yield tmpl("tagentry",
733 parity=next(parity),
721 parity=next(parity),
734 tag=k,
722 tag=k,
735 node=hex(n),
723 node=hex(n),
736 date=web.repo[n].date())
724 date=web.repo[n].date())
737
725
738 def bookmarks(**map):
726 def bookmarks(**map):
739 parity = paritygen(web.stripecount)
727 parity = paritygen(web.stripecount)
740 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
728 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
741 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
729 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
742 marks = sorted(marks, key=sortkey, reverse=True)
730 marks = sorted(marks, key=sortkey, reverse=True)
743 for k, n in marks[:10]: # limit to 10 bookmarks
731 for k, n in marks[:10]: # limit to 10 bookmarks
744 yield {'parity': next(parity),
732 yield {'parity': next(parity),
745 'bookmark': k,
733 'bookmark': k,
746 'date': web.repo[n].date(),
734 'date': web.repo[n].date(),
747 'node': hex(n)}
735 'node': hex(n)}
748
736
749 def changelist(**map):
737 def changelist(**map):
750 parity = paritygen(web.stripecount, offset=start - end)
738 parity = paritygen(web.stripecount, offset=start - end)
751 l = [] # build a list in forward order for efficiency
739 l = [] # build a list in forward order for efficiency
752 revs = []
740 revs = []
753 if start < end:
741 if start < end:
754 revs = web.repo.changelog.revs(start, end - 1)
742 revs = web.repo.changelog.revs(start, end - 1)
755 for i in revs:
743 for i in revs:
756 ctx = web.repo[i]
744 ctx = web.repo[i]
757
745
758 l.append(tmpl(
746 l.append(tmpl(
759 'shortlogentry',
747 'shortlogentry',
760 parity=next(parity),
748 parity=next(parity),
761 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
749 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
762
750
763 for entry in reversed(l):
751 for entry in reversed(l):
764 yield entry
752 yield entry
765
753
766 tip = web.repo['tip']
754 tip = web.repo['tip']
767 count = len(web.repo)
755 count = len(web.repo)
768 start = max(0, count - web.maxchanges)
756 start = max(0, count - web.maxchanges)
769 end = min(count, start + web.maxchanges)
757 end = min(count, start + web.maxchanges)
770
758
771 desc = web.config("web", "description")
759 desc = web.config("web", "description")
772 if not desc:
760 if not desc:
773 desc = 'unknown'
761 desc = 'unknown'
774
762
775 web.res.setbodygen(tmpl(
763 return web.sendtemplate(
776 'summary',
764 'summary',
777 desc=desc,
765 desc=desc,
778 owner=get_contact(web.config) or 'unknown',
766 owner=get_contact(web.config) or 'unknown',
779 lastchange=tip.date(),
767 lastchange=tip.date(),
780 tags=tagentries,
768 tags=tagentries,
781 bookmarks=bookmarks,
769 bookmarks=bookmarks,
782 branches=webutil.branchentries(web.repo, web.stripecount, 10),
770 branches=webutil.branchentries(web.repo, web.stripecount, 10),
783 shortlog=changelist,
771 shortlog=changelist,
784 node=tip.hex(),
772 node=tip.hex(),
785 symrev='tip',
773 symrev='tip',
786 archives=web.archivelist('tip'),
774 archives=web.archivelist('tip'),
787 labels=web.configlist('web', 'labels')))
775 labels=web.configlist('web', 'labels'))
788
789 return web.res.sendresponse()
790
776
791 @webcommand('filediff')
777 @webcommand('filediff')
792 def filediff(web, req, tmpl):
778 def filediff(web, req, tmpl):
793 """
779 """
794 /diff/{revision}/{path}
780 /diff/{revision}/{path}
795 -----------------------
781 -----------------------
796
782
797 Show how a file changed in a particular commit.
783 Show how a file changed in a particular commit.
798
784
799 The ``filediff`` template is rendered.
785 The ``filediff`` template is rendered.
800
786
801 This handler is registered under both the ``/diff`` and ``/filediff``
787 This handler is registered under both the ``/diff`` and ``/filediff``
802 paths. ``/diff`` is used in modern code.
788 paths. ``/diff`` is used in modern code.
803 """
789 """
804 fctx, ctx = None, None
790 fctx, ctx = None, None
805 try:
791 try:
806 fctx = webutil.filectx(web.repo, req)
792 fctx = webutil.filectx(web.repo, req)
807 except LookupError:
793 except LookupError:
808 ctx = webutil.changectx(web.repo, req)
794 ctx = webutil.changectx(web.repo, req)
809 path = webutil.cleanpath(web.repo, web.req.qsparams['file'])
795 path = webutil.cleanpath(web.repo, web.req.qsparams['file'])
810 if path not in ctx.files():
796 if path not in ctx.files():
811 raise
797 raise
812
798
813 if fctx is not None:
799 if fctx is not None:
814 path = fctx.path()
800 path = fctx.path()
815 ctx = fctx.changectx()
801 ctx = fctx.changectx()
816 basectx = ctx.p1()
802 basectx = ctx.p1()
817
803
818 style = web.config('web', 'style')
804 style = web.config('web', 'style')
819 if 'style' in web.req.qsparams:
805 if 'style' in web.req.qsparams:
820 style = web.req.qsparams['style']
806 style = web.req.qsparams['style']
821
807
822 diffs = webutil.diffs(web, tmpl, ctx, basectx, [path], style)
808 diffs = webutil.diffs(web, tmpl, ctx, basectx, [path], style)
823 if fctx is not None:
809 if fctx is not None:
824 rename = webutil.renamelink(fctx)
810 rename = webutil.renamelink(fctx)
825 ctx = fctx
811 ctx = fctx
826 else:
812 else:
827 rename = []
813 rename = []
828 ctx = ctx
814 ctx = ctx
829
815
830 web.res.setbodygen(tmpl(
816 return web.sendtemplate(
831 'filediff',
817 'filediff',
832 file=path,
818 file=path,
833 symrev=webutil.symrevorshortnode(req, ctx),
819 symrev=webutil.symrevorshortnode(req, ctx),
834 rename=rename,
820 rename=rename,
835 diff=diffs,
821 diff=diffs,
836 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
822 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
837
838 return web.res.sendresponse()
839
823
840 diff = webcommand('diff')(filediff)
824 diff = webcommand('diff')(filediff)
841
825
842 @webcommand('comparison')
826 @webcommand('comparison')
843 def comparison(web, req, tmpl):
827 def comparison(web, req, tmpl):
844 """
828 """
845 /comparison/{revision}/{path}
829 /comparison/{revision}/{path}
846 -----------------------------
830 -----------------------------
847
831
848 Show a comparison between the old and new versions of a file from changes
832 Show a comparison between the old and new versions of a file from changes
849 made on a particular revision.
833 made on a particular revision.
850
834
851 This is similar to the ``diff`` handler. However, this form features
835 This is similar to the ``diff`` handler. However, this form features
852 a split or side-by-side diff rather than a unified diff.
836 a split or side-by-side diff rather than a unified diff.
853
837
854 The ``context`` query string argument can be used to control the lines of
838 The ``context`` query string argument can be used to control the lines of
855 context in the diff.
839 context in the diff.
856
840
857 The ``filecomparison`` template is rendered.
841 The ``filecomparison`` template is rendered.
858 """
842 """
859 ctx = webutil.changectx(web.repo, req)
843 ctx = webutil.changectx(web.repo, req)
860 if 'file' not in web.req.qsparams:
844 if 'file' not in web.req.qsparams:
861 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
845 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
862 path = webutil.cleanpath(web.repo, web.req.qsparams['file'])
846 path = webutil.cleanpath(web.repo, web.req.qsparams['file'])
863
847
864 parsecontext = lambda v: v == 'full' and -1 or int(v)
848 parsecontext = lambda v: v == 'full' and -1 or int(v)
865 if 'context' in web.req.qsparams:
849 if 'context' in web.req.qsparams:
866 context = parsecontext(web.req.qsparams['context'])
850 context = parsecontext(web.req.qsparams['context'])
867 else:
851 else:
868 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
852 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
869
853
870 def filelines(f):
854 def filelines(f):
871 if f.isbinary():
855 if f.isbinary():
872 mt = mimetypes.guess_type(f.path())[0]
856 mt = mimetypes.guess_type(f.path())[0]
873 if not mt:
857 if not mt:
874 mt = 'application/octet-stream'
858 mt = 'application/octet-stream'
875 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
859 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
876 return f.data().splitlines()
860 return f.data().splitlines()
877
861
878 fctx = None
862 fctx = None
879 parent = ctx.p1()
863 parent = ctx.p1()
880 leftrev = parent.rev()
864 leftrev = parent.rev()
881 leftnode = parent.node()
865 leftnode = parent.node()
882 rightrev = ctx.rev()
866 rightrev = ctx.rev()
883 rightnode = ctx.node()
867 rightnode = ctx.node()
884 if path in ctx:
868 if path in ctx:
885 fctx = ctx[path]
869 fctx = ctx[path]
886 rightlines = filelines(fctx)
870 rightlines = filelines(fctx)
887 if path not in parent:
871 if path not in parent:
888 leftlines = ()
872 leftlines = ()
889 else:
873 else:
890 pfctx = parent[path]
874 pfctx = parent[path]
891 leftlines = filelines(pfctx)
875 leftlines = filelines(pfctx)
892 else:
876 else:
893 rightlines = ()
877 rightlines = ()
894 pfctx = ctx.parents()[0][path]
878 pfctx = ctx.parents()[0][path]
895 leftlines = filelines(pfctx)
879 leftlines = filelines(pfctx)
896
880
897 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
881 comparison = webutil.compare(tmpl, context, leftlines, rightlines)
898 if fctx is not None:
882 if fctx is not None:
899 rename = webutil.renamelink(fctx)
883 rename = webutil.renamelink(fctx)
900 ctx = fctx
884 ctx = fctx
901 else:
885 else:
902 rename = []
886 rename = []
903 ctx = ctx
887 ctx = ctx
904
888
905 web.res.setbodygen(tmpl(
889 return web.sendtemplate(
906 'filecomparison',
890 'filecomparison',
907 file=path,
891 file=path,
908 symrev=webutil.symrevorshortnode(req, ctx),
892 symrev=webutil.symrevorshortnode(req, ctx),
909 rename=rename,
893 rename=rename,
910 leftrev=leftrev,
894 leftrev=leftrev,
911 leftnode=hex(leftnode),
895 leftnode=hex(leftnode),
912 rightrev=rightrev,
896 rightrev=rightrev,
913 rightnode=hex(rightnode),
897 rightnode=hex(rightnode),
914 comparison=comparison,
898 comparison=comparison,
915 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx))))
899 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
916
917 return web.res.sendresponse()
918
900
919 @webcommand('annotate')
901 @webcommand('annotate')
920 def annotate(web, req, tmpl):
902 def annotate(web, req, tmpl):
921 """
903 """
922 /annotate/{revision}/{path}
904 /annotate/{revision}/{path}
923 ---------------------------
905 ---------------------------
924
906
925 Show changeset information for each line in a file.
907 Show changeset information for each line in a file.
926
908
927 The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
909 The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
928 ``ignoreblanklines`` query string arguments have the same meaning as
910 ``ignoreblanklines`` query string arguments have the same meaning as
929 their ``[annotate]`` config equivalents. It uses the hgrc boolean
911 their ``[annotate]`` config equivalents. It uses the hgrc boolean
930 parsing logic to interpret the value. e.g. ``0`` and ``false`` are
912 parsing logic to interpret the value. e.g. ``0`` and ``false`` are
931 false and ``1`` and ``true`` are true. If not defined, the server
913 false and ``1`` and ``true`` are true. If not defined, the server
932 default settings are used.
914 default settings are used.
933
915
934 The ``fileannotate`` template is rendered.
916 The ``fileannotate`` template is rendered.
935 """
917 """
936 fctx = webutil.filectx(web.repo, req)
918 fctx = webutil.filectx(web.repo, req)
937 f = fctx.path()
919 f = fctx.path()
938 parity = paritygen(web.stripecount)
920 parity = paritygen(web.stripecount)
939 ishead = fctx.filerev() in fctx.filelog().headrevs()
921 ishead = fctx.filerev() in fctx.filelog().headrevs()
940
922
941 # parents() is called once per line and several lines likely belong to
923 # parents() is called once per line and several lines likely belong to
942 # same revision. So it is worth caching.
924 # same revision. So it is worth caching.
943 # TODO there are still redundant operations within basefilectx.parents()
925 # TODO there are still redundant operations within basefilectx.parents()
944 # and from the fctx.annotate() call itself that could be cached.
926 # and from the fctx.annotate() call itself that could be cached.
945 parentscache = {}
927 parentscache = {}
946 def parents(f):
928 def parents(f):
947 rev = f.rev()
929 rev = f.rev()
948 if rev not in parentscache:
930 if rev not in parentscache:
949 parentscache[rev] = []
931 parentscache[rev] = []
950 for p in f.parents():
932 for p in f.parents():
951 entry = {
933 entry = {
952 'node': p.hex(),
934 'node': p.hex(),
953 'rev': p.rev(),
935 'rev': p.rev(),
954 }
936 }
955 parentscache[rev].append(entry)
937 parentscache[rev].append(entry)
956
938
957 for p in parentscache[rev]:
939 for p in parentscache[rev]:
958 yield p
940 yield p
959
941
960 def annotate(**map):
942 def annotate(**map):
961 if fctx.isbinary():
943 if fctx.isbinary():
962 mt = (mimetypes.guess_type(fctx.path())[0]
944 mt = (mimetypes.guess_type(fctx.path())[0]
963 or 'application/octet-stream')
945 or 'application/octet-stream')
964 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
946 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
965 else:
947 else:
966 lines = webutil.annotate(req, fctx, web.repo.ui)
948 lines = webutil.annotate(req, fctx, web.repo.ui)
967
949
968 previousrev = None
950 previousrev = None
969 blockparitygen = paritygen(1)
951 blockparitygen = paritygen(1)
970 for lineno, (aline, l) in enumerate(lines):
952 for lineno, (aline, l) in enumerate(lines):
971 f = aline.fctx
953 f = aline.fctx
972 rev = f.rev()
954 rev = f.rev()
973 if rev != previousrev:
955 if rev != previousrev:
974 blockhead = True
956 blockhead = True
975 blockparity = next(blockparitygen)
957 blockparity = next(blockparitygen)
976 else:
958 else:
977 blockhead = None
959 blockhead = None
978 previousrev = rev
960 previousrev = rev
979 yield {"parity": next(parity),
961 yield {"parity": next(parity),
980 "node": f.hex(),
962 "node": f.hex(),
981 "rev": rev,
963 "rev": rev,
982 "author": f.user(),
964 "author": f.user(),
983 "parents": parents(f),
965 "parents": parents(f),
984 "desc": f.description(),
966 "desc": f.description(),
985 "extra": f.extra(),
967 "extra": f.extra(),
986 "file": f.path(),
968 "file": f.path(),
987 "blockhead": blockhead,
969 "blockhead": blockhead,
988 "blockparity": blockparity,
970 "blockparity": blockparity,
989 "targetline": aline.lineno,
971 "targetline": aline.lineno,
990 "line": l,
972 "line": l,
991 "lineno": lineno + 1,
973 "lineno": lineno + 1,
992 "lineid": "l%d" % (lineno + 1),
974 "lineid": "l%d" % (lineno + 1),
993 "linenumber": "% 6d" % (lineno + 1),
975 "linenumber": "% 6d" % (lineno + 1),
994 "revdate": f.date()}
976 "revdate": f.date()}
995
977
996 diffopts = webutil.difffeatureopts(req, web.repo.ui, 'annotate')
978 diffopts = webutil.difffeatureopts(req, web.repo.ui, 'annotate')
997 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
979 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
998
980
999 web.res.setbodygen(tmpl(
981 return web.sendtemplate(
1000 'fileannotate',
982 'fileannotate',
1001 file=f,
983 file=f,
1002 annotate=annotate,
984 annotate=annotate,
1003 path=webutil.up(f),
985 path=webutil.up(f),
1004 symrev=webutil.symrevorshortnode(req, fctx),
986 symrev=webutil.symrevorshortnode(req, fctx),
1005 rename=webutil.renamelink(fctx),
987 rename=webutil.renamelink(fctx),
1006 permissions=fctx.manifest().flags(f),
988 permissions=fctx.manifest().flags(f),
1007 ishead=int(ishead),
989 ishead=int(ishead),
1008 diffopts=diffopts,
990 diffopts=diffopts,
1009 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))))
991 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
1010
1011 return web.res.sendresponse()
1012
992
1013 @webcommand('filelog')
993 @webcommand('filelog')
1014 def filelog(web, req, tmpl):
994 def filelog(web, req, tmpl):
1015 """
995 """
1016 /filelog/{revision}/{path}
996 /filelog/{revision}/{path}
1017 --------------------------
997 --------------------------
1018
998
1019 Show information about the history of a file in the repository.
999 Show information about the history of a file in the repository.
1020
1000
1021 The ``revcount`` query string argument can be defined to control the
1001 The ``revcount`` query string argument can be defined to control the
1022 maximum number of entries to show.
1002 maximum number of entries to show.
1023
1003
1024 The ``filelog`` template will be rendered.
1004 The ``filelog`` template will be rendered.
1025 """
1005 """
1026
1006
1027 try:
1007 try:
1028 fctx = webutil.filectx(web.repo, req)
1008 fctx = webutil.filectx(web.repo, req)
1029 f = fctx.path()
1009 f = fctx.path()
1030 fl = fctx.filelog()
1010 fl = fctx.filelog()
1031 except error.LookupError:
1011 except error.LookupError:
1032 f = webutil.cleanpath(web.repo, web.req.qsparams['file'])
1012 f = webutil.cleanpath(web.repo, web.req.qsparams['file'])
1033 fl = web.repo.file(f)
1013 fl = web.repo.file(f)
1034 numrevs = len(fl)
1014 numrevs = len(fl)
1035 if not numrevs: # file doesn't exist at all
1015 if not numrevs: # file doesn't exist at all
1036 raise
1016 raise
1037 rev = webutil.changectx(web.repo, req).rev()
1017 rev = webutil.changectx(web.repo, req).rev()
1038 first = fl.linkrev(0)
1018 first = fl.linkrev(0)
1039 if rev < first: # current rev is from before file existed
1019 if rev < first: # current rev is from before file existed
1040 raise
1020 raise
1041 frev = numrevs - 1
1021 frev = numrevs - 1
1042 while fl.linkrev(frev) > rev:
1022 while fl.linkrev(frev) > rev:
1043 frev -= 1
1023 frev -= 1
1044 fctx = web.repo.filectx(f, fl.linkrev(frev))
1024 fctx = web.repo.filectx(f, fl.linkrev(frev))
1045
1025
1046 revcount = web.maxshortchanges
1026 revcount = web.maxshortchanges
1047 if 'revcount' in web.req.qsparams:
1027 if 'revcount' in web.req.qsparams:
1048 try:
1028 try:
1049 revcount = int(web.req.qsparams.get('revcount', revcount))
1029 revcount = int(web.req.qsparams.get('revcount', revcount))
1050 revcount = max(revcount, 1)
1030 revcount = max(revcount, 1)
1051 tmpl.defaults['sessionvars']['revcount'] = revcount
1031 tmpl.defaults['sessionvars']['revcount'] = revcount
1052 except ValueError:
1032 except ValueError:
1053 pass
1033 pass
1054
1034
1055 lrange = webutil.linerange(req)
1035 lrange = webutil.linerange(req)
1056
1036
1057 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1037 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1058 lessvars['revcount'] = max(revcount // 2, 1)
1038 lessvars['revcount'] = max(revcount // 2, 1)
1059 morevars = copy.copy(tmpl.defaults['sessionvars'])
1039 morevars = copy.copy(tmpl.defaults['sessionvars'])
1060 morevars['revcount'] = revcount * 2
1040 morevars['revcount'] = revcount * 2
1061
1041
1062 patch = 'patch' in web.req.qsparams
1042 patch = 'patch' in web.req.qsparams
1063 if patch:
1043 if patch:
1064 lessvars['patch'] = morevars['patch'] = web.req.qsparams['patch']
1044 lessvars['patch'] = morevars['patch'] = web.req.qsparams['patch']
1065 descend = 'descend' in web.req.qsparams
1045 descend = 'descend' in web.req.qsparams
1066 if descend:
1046 if descend:
1067 lessvars['descend'] = morevars['descend'] = web.req.qsparams['descend']
1047 lessvars['descend'] = morevars['descend'] = web.req.qsparams['descend']
1068
1048
1069 count = fctx.filerev() + 1
1049 count = fctx.filerev() + 1
1070 start = max(0, count - revcount) # first rev on this page
1050 start = max(0, count - revcount) # first rev on this page
1071 end = min(count, start + revcount) # last rev on this page
1051 end = min(count, start + revcount) # last rev on this page
1072 parity = paritygen(web.stripecount, offset=start - end)
1052 parity = paritygen(web.stripecount, offset=start - end)
1073
1053
1074 repo = web.repo
1054 repo = web.repo
1075 revs = fctx.filelog().revs(start, end - 1)
1055 revs = fctx.filelog().revs(start, end - 1)
1076 entries = []
1056 entries = []
1077
1057
1078 diffstyle = web.config('web', 'style')
1058 diffstyle = web.config('web', 'style')
1079 if 'style' in web.req.qsparams:
1059 if 'style' in web.req.qsparams:
1080 diffstyle = web.req.qsparams['style']
1060 diffstyle = web.req.qsparams['style']
1081
1061
1082 def diff(fctx, linerange=None):
1062 def diff(fctx, linerange=None):
1083 ctx = fctx.changectx()
1063 ctx = fctx.changectx()
1084 basectx = ctx.p1()
1064 basectx = ctx.p1()
1085 path = fctx.path()
1065 path = fctx.path()
1086 return webutil.diffs(web, tmpl, ctx, basectx, [path], diffstyle,
1066 return webutil.diffs(web, tmpl, ctx, basectx, [path], diffstyle,
1087 linerange=linerange,
1067 linerange=linerange,
1088 lineidprefix='%s-' % ctx.hex()[:12])
1068 lineidprefix='%s-' % ctx.hex()[:12])
1089
1069
1090 linerange = None
1070 linerange = None
1091 if lrange is not None:
1071 if lrange is not None:
1092 linerange = webutil.formatlinerange(*lrange)
1072 linerange = webutil.formatlinerange(*lrange)
1093 # deactivate numeric nav links when linerange is specified as this
1073 # deactivate numeric nav links when linerange is specified as this
1094 # would required a dedicated "revnav" class
1074 # would required a dedicated "revnav" class
1095 nav = None
1075 nav = None
1096 if descend:
1076 if descend:
1097 it = dagop.blockdescendants(fctx, *lrange)
1077 it = dagop.blockdescendants(fctx, *lrange)
1098 else:
1078 else:
1099 it = dagop.blockancestors(fctx, *lrange)
1079 it = dagop.blockancestors(fctx, *lrange)
1100 for i, (c, lr) in enumerate(it, 1):
1080 for i, (c, lr) in enumerate(it, 1):
1101 diffs = None
1081 diffs = None
1102 if patch:
1082 if patch:
1103 diffs = diff(c, linerange=lr)
1083 diffs = diff(c, linerange=lr)
1104 # follow renames accross filtered (not in range) revisions
1084 # follow renames accross filtered (not in range) revisions
1105 path = c.path()
1085 path = c.path()
1106 entries.append(dict(
1086 entries.append(dict(
1107 parity=next(parity),
1087 parity=next(parity),
1108 filerev=c.rev(),
1088 filerev=c.rev(),
1109 file=path,
1089 file=path,
1110 diff=diffs,
1090 diff=diffs,
1111 linerange=webutil.formatlinerange(*lr),
1091 linerange=webutil.formatlinerange(*lr),
1112 **pycompat.strkwargs(webutil.commonentry(repo, c))))
1092 **pycompat.strkwargs(webutil.commonentry(repo, c))))
1113 if i == revcount:
1093 if i == revcount:
1114 break
1094 break
1115 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1095 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1116 morevars['linerange'] = lessvars['linerange']
1096 morevars['linerange'] = lessvars['linerange']
1117 else:
1097 else:
1118 for i in revs:
1098 for i in revs:
1119 iterfctx = fctx.filectx(i)
1099 iterfctx = fctx.filectx(i)
1120 diffs = None
1100 diffs = None
1121 if patch:
1101 if patch:
1122 diffs = diff(iterfctx)
1102 diffs = diff(iterfctx)
1123 entries.append(dict(
1103 entries.append(dict(
1124 parity=next(parity),
1104 parity=next(parity),
1125 filerev=i,
1105 filerev=i,
1126 file=f,
1106 file=f,
1127 diff=diffs,
1107 diff=diffs,
1128 rename=webutil.renamelink(iterfctx),
1108 rename=webutil.renamelink(iterfctx),
1129 **pycompat.strkwargs(webutil.commonentry(repo, iterfctx))))
1109 **pycompat.strkwargs(webutil.commonentry(repo, iterfctx))))
1130 entries.reverse()
1110 entries.reverse()
1131 revnav = webutil.filerevnav(web.repo, fctx.path())
1111 revnav = webutil.filerevnav(web.repo, fctx.path())
1132 nav = revnav.gen(end - 1, revcount, count)
1112 nav = revnav.gen(end - 1, revcount, count)
1133
1113
1134 latestentry = entries[:1]
1114 latestentry = entries[:1]
1135
1115
1136 web.res.setbodygen(tmpl(
1116 return web.sendtemplate(
1137 'filelog',
1117 'filelog',
1138 file=f,
1118 file=f,
1139 nav=nav,
1119 nav=nav,
1140 symrev=webutil.symrevorshortnode(req, fctx),
1120 symrev=webutil.symrevorshortnode(req, fctx),
1141 entries=entries,
1121 entries=entries,
1142 descend=descend,
1122 descend=descend,
1143 patch=patch,
1123 patch=patch,
1144 latestentry=latestentry,
1124 latestentry=latestentry,
1145 linerange=linerange,
1125 linerange=linerange,
1146 revcount=revcount,
1126 revcount=revcount,
1147 morevars=morevars,
1127 morevars=morevars,
1148 lessvars=lessvars,
1128 lessvars=lessvars,
1149 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx))))
1129 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
1150
1151 return web.res.sendresponse()
1152
1130
1153 @webcommand('archive')
1131 @webcommand('archive')
1154 def archive(web, req, tmpl):
1132 def archive(web, req, tmpl):
1155 """
1133 """
1156 /archive/{revision}.{format}[/{path}]
1134 /archive/{revision}.{format}[/{path}]
1157 -------------------------------------
1135 -------------------------------------
1158
1136
1159 Obtain an archive of repository content.
1137 Obtain an archive of repository content.
1160
1138
1161 The content and type of the archive is defined by a URL path parameter.
1139 The content and type of the archive is defined by a URL path parameter.
1162 ``format`` is the file extension of the archive type to be generated. e.g.
1140 ``format`` is the file extension of the archive type to be generated. e.g.
1163 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1141 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1164 server configuration.
1142 server configuration.
1165
1143
1166 The optional ``path`` URL parameter controls content to include in the
1144 The optional ``path`` URL parameter controls content to include in the
1167 archive. If omitted, every file in the specified revision is present in the
1145 archive. If omitted, every file in the specified revision is present in the
1168 archive. If included, only the specified file or contents of the specified
1146 archive. If included, only the specified file or contents of the specified
1169 directory will be included in the archive.
1147 directory will be included in the archive.
1170
1148
1171 No template is used for this handler. Raw, binary content is generated.
1149 No template is used for this handler. Raw, binary content is generated.
1172 """
1150 """
1173
1151
1174 type_ = web.req.qsparams.get('type')
1152 type_ = web.req.qsparams.get('type')
1175 allowed = web.configlist("web", "allow_archive")
1153 allowed = web.configlist("web", "allow_archive")
1176 key = web.req.qsparams['node']
1154 key = web.req.qsparams['node']
1177
1155
1178 if type_ not in web.archivespecs:
1156 if type_ not in web.archivespecs:
1179 msg = 'Unsupported archive type: %s' % type_
1157 msg = 'Unsupported archive type: %s' % type_
1180 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1158 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1181
1159
1182 if not ((type_ in allowed or
1160 if not ((type_ in allowed or
1183 web.configbool("web", "allow" + type_))):
1161 web.configbool("web", "allow" + type_))):
1184 msg = 'Archive type not allowed: %s' % type_
1162 msg = 'Archive type not allowed: %s' % type_
1185 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1163 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1186
1164
1187 reponame = re.sub(br"\W+", "-", os.path.basename(web.reponame))
1165 reponame = re.sub(br"\W+", "-", os.path.basename(web.reponame))
1188 cnode = web.repo.lookup(key)
1166 cnode = web.repo.lookup(key)
1189 arch_version = key
1167 arch_version = key
1190 if cnode == key or key == 'tip':
1168 if cnode == key or key == 'tip':
1191 arch_version = short(cnode)
1169 arch_version = short(cnode)
1192 name = "%s-%s" % (reponame, arch_version)
1170 name = "%s-%s" % (reponame, arch_version)
1193
1171
1194 ctx = webutil.changectx(web.repo, req)
1172 ctx = webutil.changectx(web.repo, req)
1195 pats = []
1173 pats = []
1196 match = scmutil.match(ctx, [])
1174 match = scmutil.match(ctx, [])
1197 file = web.req.qsparams.get('file')
1175 file = web.req.qsparams.get('file')
1198 if file:
1176 if file:
1199 pats = ['path:' + file]
1177 pats = ['path:' + file]
1200 match = scmutil.match(ctx, pats, default='path')
1178 match = scmutil.match(ctx, pats, default='path')
1201 if pats:
1179 if pats:
1202 files = [f for f in ctx.manifest().keys() if match(f)]
1180 files = [f for f in ctx.manifest().keys() if match(f)]
1203 if not files:
1181 if not files:
1204 raise ErrorResponse(HTTP_NOT_FOUND,
1182 raise ErrorResponse(HTTP_NOT_FOUND,
1205 'file(s) not found: %s' % file)
1183 'file(s) not found: %s' % file)
1206
1184
1207 mimetype, artype, extension, encoding = web.archivespecs[type_]
1185 mimetype, artype, extension, encoding = web.archivespecs[type_]
1208
1186
1209 web.res.headers['Content-Type'] = mimetype
1187 web.res.headers['Content-Type'] = mimetype
1210 web.res.headers['Content-Disposition'] = 'attachment; filename=%s%s' % (
1188 web.res.headers['Content-Disposition'] = 'attachment; filename=%s%s' % (
1211 name, extension)
1189 name, extension)
1212
1190
1213 if encoding:
1191 if encoding:
1214 web.res.headers['Content-Encoding'] = encoding
1192 web.res.headers['Content-Encoding'] = encoding
1215
1193
1216 web.res.setbodywillwrite()
1194 web.res.setbodywillwrite()
1217 assert list(web.res.sendresponse()) == []
1195 assert list(web.res.sendresponse()) == []
1218
1196
1219 bodyfh = web.res.getbodyfile()
1197 bodyfh = web.res.getbodyfile()
1220
1198
1221 archival.archive(web.repo, bodyfh, cnode, artype, prefix=name,
1199 archival.archive(web.repo, bodyfh, cnode, artype, prefix=name,
1222 matchfn=match,
1200 matchfn=match,
1223 subrepos=web.configbool("web", "archivesubrepos"))
1201 subrepos=web.configbool("web", "archivesubrepos"))
1224
1202
1225 return []
1203 return []
1226
1204
1227 @webcommand('static')
1205 @webcommand('static')
1228 def static(web, req, tmpl):
1206 def static(web, req, tmpl):
1229 fname = web.req.qsparams['file']
1207 fname = web.req.qsparams['file']
1230 # a repo owner may set web.static in .hg/hgrc to get any file
1208 # a repo owner may set web.static in .hg/hgrc to get any file
1231 # readable by the user running the CGI script
1209 # readable by the user running the CGI script
1232 static = web.config("web", "static", None, untrusted=False)
1210 static = web.config("web", "static", None, untrusted=False)
1233 if not static:
1211 if not static:
1234 tp = web.templatepath or templater.templatepaths()
1212 tp = web.templatepath or templater.templatepaths()
1235 if isinstance(tp, str):
1213 if isinstance(tp, str):
1236 tp = [tp]
1214 tp = [tp]
1237 static = [os.path.join(p, 'static') for p in tp]
1215 static = [os.path.join(p, 'static') for p in tp]
1238
1216
1239 staticfile(static, fname, web.res)
1217 staticfile(static, fname, web.res)
1240 return web.res.sendresponse()
1218 return web.res.sendresponse()
1241
1219
1242 @webcommand('graph')
1220 @webcommand('graph')
1243 def graph(web, req, tmpl):
1221 def graph(web, req, tmpl):
1244 """
1222 """
1245 /graph[/{revision}]
1223 /graph[/{revision}]
1246 -------------------
1224 -------------------
1247
1225
1248 Show information about the graphical topology of the repository.
1226 Show information about the graphical topology of the repository.
1249
1227
1250 Information rendered by this handler can be used to create visual
1228 Information rendered by this handler can be used to create visual
1251 representations of repository topology.
1229 representations of repository topology.
1252
1230
1253 The ``revision`` URL parameter controls the starting changeset. If it's
1231 The ``revision`` URL parameter controls the starting changeset. If it's
1254 absent, the default is ``tip``.
1232 absent, the default is ``tip``.
1255
1233
1256 The ``revcount`` query string argument can define the number of changesets
1234 The ``revcount`` query string argument can define the number of changesets
1257 to show information for.
1235 to show information for.
1258
1236
1259 The ``graphtop`` query string argument can specify the starting changeset
1237 The ``graphtop`` query string argument can specify the starting changeset
1260 for producing ``jsdata`` variable that is used for rendering graph in
1238 for producing ``jsdata`` variable that is used for rendering graph in
1261 JavaScript. By default it has the same value as ``revision``.
1239 JavaScript. By default it has the same value as ``revision``.
1262
1240
1263 This handler will render the ``graph`` template.
1241 This handler will render the ``graph`` template.
1264 """
1242 """
1265
1243
1266 if 'node' in web.req.qsparams:
1244 if 'node' in web.req.qsparams:
1267 ctx = webutil.changectx(web.repo, req)
1245 ctx = webutil.changectx(web.repo, req)
1268 symrev = webutil.symrevorshortnode(req, ctx)
1246 symrev = webutil.symrevorshortnode(req, ctx)
1269 else:
1247 else:
1270 ctx = web.repo['tip']
1248 ctx = web.repo['tip']
1271 symrev = 'tip'
1249 symrev = 'tip'
1272 rev = ctx.rev()
1250 rev = ctx.rev()
1273
1251
1274 bg_height = 39
1252 bg_height = 39
1275 revcount = web.maxshortchanges
1253 revcount = web.maxshortchanges
1276 if 'revcount' in web.req.qsparams:
1254 if 'revcount' in web.req.qsparams:
1277 try:
1255 try:
1278 revcount = int(web.req.qsparams.get('revcount', revcount))
1256 revcount = int(web.req.qsparams.get('revcount', revcount))
1279 revcount = max(revcount, 1)
1257 revcount = max(revcount, 1)
1280 tmpl.defaults['sessionvars']['revcount'] = revcount
1258 tmpl.defaults['sessionvars']['revcount'] = revcount
1281 except ValueError:
1259 except ValueError:
1282 pass
1260 pass
1283
1261
1284 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1262 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1285 lessvars['revcount'] = max(revcount // 2, 1)
1263 lessvars['revcount'] = max(revcount // 2, 1)
1286 morevars = copy.copy(tmpl.defaults['sessionvars'])
1264 morevars = copy.copy(tmpl.defaults['sessionvars'])
1287 morevars['revcount'] = revcount * 2
1265 morevars['revcount'] = revcount * 2
1288
1266
1289 graphtop = web.req.qsparams.get('graphtop', ctx.hex())
1267 graphtop = web.req.qsparams.get('graphtop', ctx.hex())
1290 graphvars = copy.copy(tmpl.defaults['sessionvars'])
1268 graphvars = copy.copy(tmpl.defaults['sessionvars'])
1291 graphvars['graphtop'] = graphtop
1269 graphvars['graphtop'] = graphtop
1292
1270
1293 count = len(web.repo)
1271 count = len(web.repo)
1294 pos = rev
1272 pos = rev
1295
1273
1296 uprev = min(max(0, count - 1), rev + revcount)
1274 uprev = min(max(0, count - 1), rev + revcount)
1297 downrev = max(0, rev - revcount)
1275 downrev = max(0, rev - revcount)
1298 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1276 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1299
1277
1300 tree = []
1278 tree = []
1301 nextentry = []
1279 nextentry = []
1302 lastrev = 0
1280 lastrev = 0
1303 if pos != -1:
1281 if pos != -1:
1304 allrevs = web.repo.changelog.revs(pos, 0)
1282 allrevs = web.repo.changelog.revs(pos, 0)
1305 revs = []
1283 revs = []
1306 for i in allrevs:
1284 for i in allrevs:
1307 revs.append(i)
1285 revs.append(i)
1308 if len(revs) >= revcount + 1:
1286 if len(revs) >= revcount + 1:
1309 break
1287 break
1310
1288
1311 if len(revs) > revcount:
1289 if len(revs) > revcount:
1312 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1290 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1313 revs = revs[:-1]
1291 revs = revs[:-1]
1314
1292
1315 lastrev = revs[-1]
1293 lastrev = revs[-1]
1316
1294
1317 # We have to feed a baseset to dagwalker as it is expecting smartset
1295 # We have to feed a baseset to dagwalker as it is expecting smartset
1318 # object. This does not have a big impact on hgweb performance itself
1296 # object. This does not have a big impact on hgweb performance itself
1319 # since hgweb graphing code is not itself lazy yet.
1297 # since hgweb graphing code is not itself lazy yet.
1320 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1298 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1321 # As we said one line above... not lazy.
1299 # As we said one line above... not lazy.
1322 tree = list(item for item in graphmod.colored(dag, web.repo)
1300 tree = list(item for item in graphmod.colored(dag, web.repo)
1323 if item[1] == graphmod.CHANGESET)
1301 if item[1] == graphmod.CHANGESET)
1324
1302
1325 def nodecurrent(ctx):
1303 def nodecurrent(ctx):
1326 wpnodes = web.repo.dirstate.parents()
1304 wpnodes = web.repo.dirstate.parents()
1327 if wpnodes[1] == nullid:
1305 if wpnodes[1] == nullid:
1328 wpnodes = wpnodes[:1]
1306 wpnodes = wpnodes[:1]
1329 if ctx.node() in wpnodes:
1307 if ctx.node() in wpnodes:
1330 return '@'
1308 return '@'
1331 return ''
1309 return ''
1332
1310
1333 def nodesymbol(ctx):
1311 def nodesymbol(ctx):
1334 if ctx.obsolete():
1312 if ctx.obsolete():
1335 return 'x'
1313 return 'x'
1336 elif ctx.isunstable():
1314 elif ctx.isunstable():
1337 return '*'
1315 return '*'
1338 elif ctx.closesbranch():
1316 elif ctx.closesbranch():
1339 return '_'
1317 return '_'
1340 else:
1318 else:
1341 return 'o'
1319 return 'o'
1342
1320
1343 def fulltree():
1321 def fulltree():
1344 pos = web.repo[graphtop].rev()
1322 pos = web.repo[graphtop].rev()
1345 tree = []
1323 tree = []
1346 if pos != -1:
1324 if pos != -1:
1347 revs = web.repo.changelog.revs(pos, lastrev)
1325 revs = web.repo.changelog.revs(pos, lastrev)
1348 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1326 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1349 tree = list(item for item in graphmod.colored(dag, web.repo)
1327 tree = list(item for item in graphmod.colored(dag, web.repo)
1350 if item[1] == graphmod.CHANGESET)
1328 if item[1] == graphmod.CHANGESET)
1351 return tree
1329 return tree
1352
1330
1353 def jsdata():
1331 def jsdata():
1354 return [{'node': pycompat.bytestr(ctx),
1332 return [{'node': pycompat.bytestr(ctx),
1355 'graphnode': nodecurrent(ctx) + nodesymbol(ctx),
1333 'graphnode': nodecurrent(ctx) + nodesymbol(ctx),
1356 'vertex': vtx,
1334 'vertex': vtx,
1357 'edges': edges}
1335 'edges': edges}
1358 for (id, type, ctx, vtx, edges) in fulltree()]
1336 for (id, type, ctx, vtx, edges) in fulltree()]
1359
1337
1360 def nodes():
1338 def nodes():
1361 parity = paritygen(web.stripecount)
1339 parity = paritygen(web.stripecount)
1362 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1340 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1363 entry = webutil.commonentry(web.repo, ctx)
1341 entry = webutil.commonentry(web.repo, ctx)
1364 edgedata = [{'col': edge[0],
1342 edgedata = [{'col': edge[0],
1365 'nextcol': edge[1],
1343 'nextcol': edge[1],
1366 'color': (edge[2] - 1) % 6 + 1,
1344 'color': (edge[2] - 1) % 6 + 1,
1367 'width': edge[3],
1345 'width': edge[3],
1368 'bcolor': edge[4]}
1346 'bcolor': edge[4]}
1369 for edge in edges]
1347 for edge in edges]
1370
1348
1371 entry.update({'col': vtx[0],
1349 entry.update({'col': vtx[0],
1372 'color': (vtx[1] - 1) % 6 + 1,
1350 'color': (vtx[1] - 1) % 6 + 1,
1373 'parity': next(parity),
1351 'parity': next(parity),
1374 'edges': edgedata,
1352 'edges': edgedata,
1375 'row': row,
1353 'row': row,
1376 'nextrow': row + 1})
1354 'nextrow': row + 1})
1377
1355
1378 yield entry
1356 yield entry
1379
1357
1380 rows = len(tree)
1358 rows = len(tree)
1381
1359
1382 web.res.setbodygen(tmpl(
1360 return web.sendtemplate(
1383 'graph',
1361 'graph',
1384 rev=rev,
1362 rev=rev,
1385 symrev=symrev,
1363 symrev=symrev,
1386 revcount=revcount,
1364 revcount=revcount,
1387 uprev=uprev,
1365 uprev=uprev,
1388 lessvars=lessvars,
1366 lessvars=lessvars,
1389 morevars=morevars,
1367 morevars=morevars,
1390 downrev=downrev,
1368 downrev=downrev,
1391 graphvars=graphvars,
1369 graphvars=graphvars,
1392 rows=rows,
1370 rows=rows,
1393 bg_height=bg_height,
1371 bg_height=bg_height,
1394 changesets=count,
1372 changesets=count,
1395 nextentry=nextentry,
1373 nextentry=nextentry,
1396 jsdata=lambda **x: jsdata(),
1374 jsdata=lambda **x: jsdata(),
1397 nodes=lambda **x: nodes(),
1375 nodes=lambda **x: nodes(),
1398 node=ctx.hex(),
1376 node=ctx.hex(),
1399 changenav=changenav))
1377 changenav=changenav)
1400
1401 return web.res.sendresponse()
1402
1378
1403 def _getdoc(e):
1379 def _getdoc(e):
1404 doc = e[0].__doc__
1380 doc = e[0].__doc__
1405 if doc:
1381 if doc:
1406 doc = _(doc).partition('\n')[0]
1382 doc = _(doc).partition('\n')[0]
1407 else:
1383 else:
1408 doc = _('(no help text available)')
1384 doc = _('(no help text available)')
1409 return doc
1385 return doc
1410
1386
1411 @webcommand('help')
1387 @webcommand('help')
1412 def help(web, req, tmpl):
1388 def help(web, req, tmpl):
1413 """
1389 """
1414 /help[/{topic}]
1390 /help[/{topic}]
1415 ---------------
1391 ---------------
1416
1392
1417 Render help documentation.
1393 Render help documentation.
1418
1394
1419 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1395 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1420 is defined, that help topic will be rendered. If not, an index of
1396 is defined, that help topic will be rendered. If not, an index of
1421 available help topics will be rendered.
1397 available help topics will be rendered.
1422
1398
1423 The ``help`` template will be rendered when requesting help for a topic.
1399 The ``help`` template will be rendered when requesting help for a topic.
1424 ``helptopics`` will be rendered for the index of help topics.
1400 ``helptopics`` will be rendered for the index of help topics.
1425 """
1401 """
1426 from .. import commands, help as helpmod # avoid cycle
1402 from .. import commands, help as helpmod # avoid cycle
1427
1403
1428 topicname = web.req.qsparams.get('node')
1404 topicname = web.req.qsparams.get('node')
1429 if not topicname:
1405 if not topicname:
1430 def topics(**map):
1406 def topics(**map):
1431 for entries, summary, _doc in helpmod.helptable:
1407 for entries, summary, _doc in helpmod.helptable:
1432 yield {'topic': entries[0], 'summary': summary}
1408 yield {'topic': entries[0], 'summary': summary}
1433
1409
1434 early, other = [], []
1410 early, other = [], []
1435 primary = lambda s: s.partition('|')[0]
1411 primary = lambda s: s.partition('|')[0]
1436 for c, e in commands.table.iteritems():
1412 for c, e in commands.table.iteritems():
1437 doc = _getdoc(e)
1413 doc = _getdoc(e)
1438 if 'DEPRECATED' in doc or c.startswith('debug'):
1414 if 'DEPRECATED' in doc or c.startswith('debug'):
1439 continue
1415 continue
1440 cmd = primary(c)
1416 cmd = primary(c)
1441 if cmd.startswith('^'):
1417 if cmd.startswith('^'):
1442 early.append((cmd[1:], doc))
1418 early.append((cmd[1:], doc))
1443 else:
1419 else:
1444 other.append((cmd, doc))
1420 other.append((cmd, doc))
1445
1421
1446 early.sort()
1422 early.sort()
1447 other.sort()
1423 other.sort()
1448
1424
1449 def earlycommands(**map):
1425 def earlycommands(**map):
1450 for c, doc in early:
1426 for c, doc in early:
1451 yield {'topic': c, 'summary': doc}
1427 yield {'topic': c, 'summary': doc}
1452
1428
1453 def othercommands(**map):
1429 def othercommands(**map):
1454 for c, doc in other:
1430 for c, doc in other:
1455 yield {'topic': c, 'summary': doc}
1431 yield {'topic': c, 'summary': doc}
1456
1432
1457 web.res.setbodygen(tmpl(
1433 return web.sendtemplate(
1458 'helptopics',
1434 'helptopics',
1459 topics=topics,
1435 topics=topics,
1460 earlycommands=earlycommands,
1436 earlycommands=earlycommands,
1461 othercommands=othercommands,
1437 othercommands=othercommands,
1462 title='Index'))
1438 title='Index')
1463 return web.res.sendresponse()
1464
1439
1465 # Render an index of sub-topics.
1440 # Render an index of sub-topics.
1466 if topicname in helpmod.subtopics:
1441 if topicname in helpmod.subtopics:
1467 topics = []
1442 topics = []
1468 for entries, summary, _doc in helpmod.subtopics[topicname]:
1443 for entries, summary, _doc in helpmod.subtopics[topicname]:
1469 topics.append({
1444 topics.append({
1470 'topic': '%s.%s' % (topicname, entries[0]),
1445 'topic': '%s.%s' % (topicname, entries[0]),
1471 'basename': entries[0],
1446 'basename': entries[0],
1472 'summary': summary,
1447 'summary': summary,
1473 })
1448 })
1474
1449
1475 web.res.setbodygen(tmpl(
1450 return web.sendtemplate(
1476 'helptopics',
1451 'helptopics',
1477 topics=topics,
1452 topics=topics,
1478 title=topicname,
1453 title=topicname,
1479 subindex=True))
1454 subindex=True)
1480 return web.res.sendresponse()
1481
1455
1482 u = webutil.wsgiui.load()
1456 u = webutil.wsgiui.load()
1483 u.verbose = True
1457 u.verbose = True
1484
1458
1485 # Render a page from a sub-topic.
1459 # Render a page from a sub-topic.
1486 if '.' in topicname:
1460 if '.' in topicname:
1487 # TODO implement support for rendering sections, like
1461 # TODO implement support for rendering sections, like
1488 # `hg help` works.
1462 # `hg help` works.
1489 topic, subtopic = topicname.split('.', 1)
1463 topic, subtopic = topicname.split('.', 1)
1490 if topic not in helpmod.subtopics:
1464 if topic not in helpmod.subtopics:
1491 raise ErrorResponse(HTTP_NOT_FOUND)
1465 raise ErrorResponse(HTTP_NOT_FOUND)
1492 else:
1466 else:
1493 topic = topicname
1467 topic = topicname
1494 subtopic = None
1468 subtopic = None
1495
1469
1496 try:
1470 try:
1497 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1471 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1498 except error.Abort:
1472 except error.Abort:
1499 raise ErrorResponse(HTTP_NOT_FOUND)
1473 raise ErrorResponse(HTTP_NOT_FOUND)
1500
1474
1501 web.res.setbodygen(tmpl(
1475 return web.sendtemplate(
1502 'help',
1476 'help',
1503 topic=topicname,
1477 topic=topicname,
1504 doc=doc))
1478 doc=doc)
1505
1506 return web.res.sendresponse()
1507
1479
1508 # tell hggettext to extract docstrings from these functions:
1480 # tell hggettext to extract docstrings from these functions:
1509 i18nfunctions = commands.values()
1481 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now