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