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