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