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