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