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