##// END OF EJS Templates
hgweb: restore ascending iteration on revs in filelog web command...
Denis Laxalde -
r30825:4deb7c1a default
parent child Browse files
Show More
@@ -1,1320 +1,1321 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 864 # parents() is called once per line and several lines likely belong to
865 865 # same revision. So it is worth caching.
866 866 # TODO there are still redundant operations within basefilectx.parents()
867 867 # and from the fctx.annotate() call itself that could be cached.
868 868 parentscache = {}
869 869 def parents(f):
870 870 rev = f.rev()
871 871 if rev not in parentscache:
872 872 parentscache[rev] = []
873 873 for p in f.parents():
874 874 entry = {
875 875 'node': p.hex(),
876 876 'rev': p.rev(),
877 877 }
878 878 parentscache[rev].append(entry)
879 879
880 880 for p in parentscache[rev]:
881 881 yield p
882 882
883 883 def annotate(**map):
884 884 if util.binary(fctx.data()):
885 885 mt = (mimetypes.guess_type(fctx.path())[0]
886 886 or 'application/octet-stream')
887 887 lines = [((fctx.filectx(fctx.filerev()), 1), '(binary:%s)' % mt)]
888 888 else:
889 889 lines = webutil.annotate(fctx, web.repo.ui)
890 890
891 891 previousrev = None
892 892 blockparitygen = paritygen(1)
893 893 for lineno, ((f, targetline), l) in enumerate(lines):
894 894 rev = f.rev()
895 895 if rev != previousrev:
896 896 blockhead = True
897 897 blockparity = next(blockparitygen)
898 898 else:
899 899 blockhead = None
900 900 previousrev = rev
901 901 yield {"parity": next(parity),
902 902 "node": f.hex(),
903 903 "rev": rev,
904 904 "author": f.user(),
905 905 "parents": parents(f),
906 906 "desc": f.description(),
907 907 "extra": f.extra(),
908 908 "file": f.path(),
909 909 "blockhead": blockhead,
910 910 "blockparity": blockparity,
911 911 "targetline": targetline,
912 912 "line": l,
913 913 "lineno": lineno + 1,
914 914 "lineid": "l%d" % (lineno + 1),
915 915 "linenumber": "% 6d" % (lineno + 1),
916 916 "revdate": f.date()}
917 917
918 918 return tmpl("fileannotate",
919 919 file=f,
920 920 annotate=annotate,
921 921 path=webutil.up(f),
922 922 symrev=webutil.symrevorshortnode(req, fctx),
923 923 rename=webutil.renamelink(fctx),
924 924 permissions=fctx.manifest().flags(f),
925 925 **webutil.commonentry(web.repo, fctx))
926 926
927 927 @webcommand('filelog')
928 928 def filelog(web, req, tmpl):
929 929 """
930 930 /filelog/{revision}/{path}
931 931 --------------------------
932 932
933 933 Show information about the history of a file in the repository.
934 934
935 935 The ``revcount`` query string argument can be defined to control the
936 936 maximum number of entries to show.
937 937
938 938 The ``filelog`` template will be rendered.
939 939 """
940 940
941 941 try:
942 942 fctx = webutil.filectx(web.repo, req)
943 943 f = fctx.path()
944 944 fl = fctx.filelog()
945 945 except error.LookupError:
946 946 f = webutil.cleanpath(web.repo, req.form['file'][0])
947 947 fl = web.repo.file(f)
948 948 numrevs = len(fl)
949 949 if not numrevs: # file doesn't exist at all
950 950 raise
951 951 rev = webutil.changectx(web.repo, req).rev()
952 952 first = fl.linkrev(0)
953 953 if rev < first: # current rev is from before file existed
954 954 raise
955 955 frev = numrevs - 1
956 956 while fl.linkrev(frev) > rev:
957 957 frev -= 1
958 958 fctx = web.repo.filectx(f, fl.linkrev(frev))
959 959
960 960 revcount = web.maxshortchanges
961 961 if 'revcount' in req.form:
962 962 try:
963 963 revcount = int(req.form.get('revcount', [revcount])[0])
964 964 revcount = max(revcount, 1)
965 965 tmpl.defaults['sessionvars']['revcount'] = revcount
966 966 except ValueError:
967 967 pass
968 968
969 969 lessvars = copy.copy(tmpl.defaults['sessionvars'])
970 970 lessvars['revcount'] = max(revcount / 2, 1)
971 971 morevars = copy.copy(tmpl.defaults['sessionvars'])
972 972 morevars['revcount'] = revcount * 2
973 973
974 974 count = fctx.filerev() + 1
975 975 start = max(0, fctx.filerev() - revcount + 1) # first rev on this page
976 976 end = min(count, start + revcount) # last rev on this page
977 parity = paritygen(web.stripecount, offset=start - end + 1)
977 parity = paritygen(web.stripecount, offset=start - end)
978 978
979 979 repo = web.repo
980 980 revs = fctx.filelog().revs(start, end - 1)
981 981 entries = []
982 for i in reversed(revs):
982 for i in revs:
983 983 iterfctx = fctx.filectx(i)
984 984 entries.append(dict(
985 985 parity=next(parity),
986 986 filerev=i,
987 987 file=f,
988 988 rename=webutil.renamelink(iterfctx),
989 989 **webutil.commonentry(repo, iterfctx)))
990 entries.reverse()
990 991
991 992 latestentry = entries[:1]
992 993
993 994 revnav = webutil.filerevnav(web.repo, fctx.path())
994 995 nav = revnav.gen(end - 1, revcount, count)
995 996 return tmpl("filelog",
996 997 file=f,
997 998 nav=nav,
998 999 symrev=webutil.symrevorshortnode(req, fctx),
999 1000 entries=entries,
1000 1001 latestentry=latestentry,
1001 1002 revcount=revcount,
1002 1003 morevars=morevars,
1003 1004 lessvars=lessvars,
1004 1005 **webutil.commonentry(web.repo, fctx))
1005 1006
1006 1007 @webcommand('archive')
1007 1008 def archive(web, req, tmpl):
1008 1009 """
1009 1010 /archive/{revision}.{format}[/{path}]
1010 1011 -------------------------------------
1011 1012
1012 1013 Obtain an archive of repository content.
1013 1014
1014 1015 The content and type of the archive is defined by a URL path parameter.
1015 1016 ``format`` is the file extension of the archive type to be generated. e.g.
1016 1017 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1017 1018 server configuration.
1018 1019
1019 1020 The optional ``path`` URL parameter controls content to include in the
1020 1021 archive. If omitted, every file in the specified revision is present in the
1021 1022 archive. If included, only the specified file or contents of the specified
1022 1023 directory will be included in the archive.
1023 1024
1024 1025 No template is used for this handler. Raw, binary content is generated.
1025 1026 """
1026 1027
1027 1028 type_ = req.form.get('type', [None])[0]
1028 1029 allowed = web.configlist("web", "allow_archive")
1029 1030 key = req.form['node'][0]
1030 1031
1031 1032 if type_ not in web.archivespecs:
1032 1033 msg = 'Unsupported archive type: %s' % type_
1033 1034 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1034 1035
1035 1036 if not ((type_ in allowed or
1036 1037 web.configbool("web", "allow" + type_, False))):
1037 1038 msg = 'Archive type not allowed: %s' % type_
1038 1039 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1039 1040
1040 1041 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1041 1042 cnode = web.repo.lookup(key)
1042 1043 arch_version = key
1043 1044 if cnode == key or key == 'tip':
1044 1045 arch_version = short(cnode)
1045 1046 name = "%s-%s" % (reponame, arch_version)
1046 1047
1047 1048 ctx = webutil.changectx(web.repo, req)
1048 1049 pats = []
1049 1050 matchfn = scmutil.match(ctx, [])
1050 1051 file = req.form.get('file', None)
1051 1052 if file:
1052 1053 pats = ['path:' + file[0]]
1053 1054 matchfn = scmutil.match(ctx, pats, default='path')
1054 1055 if pats:
1055 1056 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1056 1057 if not files:
1057 1058 raise ErrorResponse(HTTP_NOT_FOUND,
1058 1059 'file(s) not found: %s' % file[0])
1059 1060
1060 1061 mimetype, artype, extension, encoding = web.archivespecs[type_]
1061 1062 headers = [
1062 1063 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1063 1064 ]
1064 1065 if encoding:
1065 1066 headers.append(('Content-Encoding', encoding))
1066 1067 req.headers.extend(headers)
1067 1068 req.respond(HTTP_OK, mimetype)
1068 1069
1069 1070 archival.archive(web.repo, req, cnode, artype, prefix=name,
1070 1071 matchfn=matchfn,
1071 1072 subrepos=web.configbool("web", "archivesubrepos"))
1072 1073 return []
1073 1074
1074 1075
1075 1076 @webcommand('static')
1076 1077 def static(web, req, tmpl):
1077 1078 fname = req.form['file'][0]
1078 1079 # a repo owner may set web.static in .hg/hgrc to get any file
1079 1080 # readable by the user running the CGI script
1080 1081 static = web.config("web", "static", None, untrusted=False)
1081 1082 if not static:
1082 1083 tp = web.templatepath or templater.templatepaths()
1083 1084 if isinstance(tp, str):
1084 1085 tp = [tp]
1085 1086 static = [os.path.join(p, 'static') for p in tp]
1086 1087 staticfile(static, fname, req)
1087 1088 return []
1088 1089
1089 1090 @webcommand('graph')
1090 1091 def graph(web, req, tmpl):
1091 1092 """
1092 1093 /graph[/{revision}]
1093 1094 -------------------
1094 1095
1095 1096 Show information about the graphical topology of the repository.
1096 1097
1097 1098 Information rendered by this handler can be used to create visual
1098 1099 representations of repository topology.
1099 1100
1100 1101 The ``revision`` URL parameter controls the starting changeset.
1101 1102
1102 1103 The ``revcount`` query string argument can define the number of changesets
1103 1104 to show information for.
1104 1105
1105 1106 This handler will render the ``graph`` template.
1106 1107 """
1107 1108
1108 1109 if 'node' in req.form:
1109 1110 ctx = webutil.changectx(web.repo, req)
1110 1111 symrev = webutil.symrevorshortnode(req, ctx)
1111 1112 else:
1112 1113 ctx = web.repo['tip']
1113 1114 symrev = 'tip'
1114 1115 rev = ctx.rev()
1115 1116
1116 1117 bg_height = 39
1117 1118 revcount = web.maxshortchanges
1118 1119 if 'revcount' in req.form:
1119 1120 try:
1120 1121 revcount = int(req.form.get('revcount', [revcount])[0])
1121 1122 revcount = max(revcount, 1)
1122 1123 tmpl.defaults['sessionvars']['revcount'] = revcount
1123 1124 except ValueError:
1124 1125 pass
1125 1126
1126 1127 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1127 1128 lessvars['revcount'] = max(revcount / 2, 1)
1128 1129 morevars = copy.copy(tmpl.defaults['sessionvars'])
1129 1130 morevars['revcount'] = revcount * 2
1130 1131
1131 1132 count = len(web.repo)
1132 1133 pos = rev
1133 1134
1134 1135 uprev = min(max(0, count - 1), rev + revcount)
1135 1136 downrev = max(0, rev - revcount)
1136 1137 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1137 1138
1138 1139 tree = []
1139 1140 if pos != -1:
1140 1141 allrevs = web.repo.changelog.revs(pos, 0)
1141 1142 revs = []
1142 1143 for i in allrevs:
1143 1144 revs.append(i)
1144 1145 if len(revs) >= revcount:
1145 1146 break
1146 1147
1147 1148 # We have to feed a baseset to dagwalker as it is expecting smartset
1148 1149 # object. This does not have a big impact on hgweb performance itself
1149 1150 # since hgweb graphing code is not itself lazy yet.
1150 1151 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1151 1152 # As we said one line above... not lazy.
1152 1153 tree = list(graphmod.colored(dag, web.repo))
1153 1154
1154 1155 def getcolumns(tree):
1155 1156 cols = 0
1156 1157 for (id, type, ctx, vtx, edges) in tree:
1157 1158 if type != graphmod.CHANGESET:
1158 1159 continue
1159 1160 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1160 1161 max([edge[1] for edge in edges] or [0]))
1161 1162 return cols
1162 1163
1163 1164 def graphdata(usetuples, encodestr):
1164 1165 data = []
1165 1166
1166 1167 row = 0
1167 1168 for (id, type, ctx, vtx, edges) in tree:
1168 1169 if type != graphmod.CHANGESET:
1169 1170 continue
1170 1171 node = str(ctx)
1171 1172 age = encodestr(templatefilters.age(ctx.date()))
1172 1173 desc = templatefilters.firstline(encodestr(ctx.description()))
1173 1174 desc = cgi.escape(templatefilters.nonempty(desc))
1174 1175 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1175 1176 branch = cgi.escape(encodestr(ctx.branch()))
1176 1177 try:
1177 1178 branchnode = web.repo.branchtip(branch)
1178 1179 except error.RepoLookupError:
1179 1180 branchnode = None
1180 1181 branch = branch, branchnode == ctx.node()
1181 1182
1182 1183 if usetuples:
1183 1184 data.append((node, vtx, edges, desc, user, age, branch,
1184 1185 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1185 1186 [cgi.escape(encodestr(x))
1186 1187 for x in ctx.bookmarks()]))
1187 1188 else:
1188 1189 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1189 1190 'color': (edge[2] - 1) % 6 + 1,
1190 1191 'width': edge[3], 'bcolor': edge[4]}
1191 1192 for edge in edges]
1192 1193
1193 1194 data.append(
1194 1195 {'node': node,
1195 1196 'col': vtx[0],
1196 1197 'color': (vtx[1] - 1) % 6 + 1,
1197 1198 'edges': edgedata,
1198 1199 'row': row,
1199 1200 'nextrow': row + 1,
1200 1201 'desc': desc,
1201 1202 'user': user,
1202 1203 'age': age,
1203 1204 'bookmarks': webutil.nodebookmarksdict(
1204 1205 web.repo, ctx.node()),
1205 1206 'branches': webutil.nodebranchdict(web.repo, ctx),
1206 1207 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1207 1208 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1208 1209
1209 1210 row += 1
1210 1211
1211 1212 return data
1212 1213
1213 1214 cols = getcolumns(tree)
1214 1215 rows = len(tree)
1215 1216 canvasheight = (rows + 1) * bg_height - 27
1216 1217
1217 1218 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1218 1219 uprev=uprev,
1219 1220 lessvars=lessvars, morevars=morevars, downrev=downrev,
1220 1221 cols=cols, rows=rows,
1221 1222 canvaswidth=(cols + 1) * bg_height,
1222 1223 truecanvasheight=rows * bg_height,
1223 1224 canvasheight=canvasheight, bg_height=bg_height,
1224 1225 # {jsdata} will be passed to |json, so it must be in utf-8
1225 1226 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1226 1227 nodes=lambda **x: graphdata(False, str),
1227 1228 node=ctx.hex(), changenav=changenav)
1228 1229
1229 1230 def _getdoc(e):
1230 1231 doc = e[0].__doc__
1231 1232 if doc:
1232 1233 doc = _(doc).partition('\n')[0]
1233 1234 else:
1234 1235 doc = _('(no help text available)')
1235 1236 return doc
1236 1237
1237 1238 @webcommand('help')
1238 1239 def help(web, req, tmpl):
1239 1240 """
1240 1241 /help[/{topic}]
1241 1242 ---------------
1242 1243
1243 1244 Render help documentation.
1244 1245
1245 1246 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1246 1247 is defined, that help topic will be rendered. If not, an index of
1247 1248 available help topics will be rendered.
1248 1249
1249 1250 The ``help`` template will be rendered when requesting help for a topic.
1250 1251 ``helptopics`` will be rendered for the index of help topics.
1251 1252 """
1252 1253 from .. import commands, help as helpmod # avoid cycle
1253 1254
1254 1255 topicname = req.form.get('node', [None])[0]
1255 1256 if not topicname:
1256 1257 def topics(**map):
1257 1258 for entries, summary, _doc in helpmod.helptable:
1258 1259 yield {'topic': entries[0], 'summary': summary}
1259 1260
1260 1261 early, other = [], []
1261 1262 primary = lambda s: s.partition('|')[0]
1262 1263 for c, e in commands.table.iteritems():
1263 1264 doc = _getdoc(e)
1264 1265 if 'DEPRECATED' in doc or c.startswith('debug'):
1265 1266 continue
1266 1267 cmd = primary(c)
1267 1268 if cmd.startswith('^'):
1268 1269 early.append((cmd[1:], doc))
1269 1270 else:
1270 1271 other.append((cmd, doc))
1271 1272
1272 1273 early.sort()
1273 1274 other.sort()
1274 1275
1275 1276 def earlycommands(**map):
1276 1277 for c, doc in early:
1277 1278 yield {'topic': c, 'summary': doc}
1278 1279
1279 1280 def othercommands(**map):
1280 1281 for c, doc in other:
1281 1282 yield {'topic': c, 'summary': doc}
1282 1283
1283 1284 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1284 1285 othercommands=othercommands, title='Index')
1285 1286
1286 1287 # Render an index of sub-topics.
1287 1288 if topicname in helpmod.subtopics:
1288 1289 topics = []
1289 1290 for entries, summary, _doc in helpmod.subtopics[topicname]:
1290 1291 topics.append({
1291 1292 'topic': '%s.%s' % (topicname, entries[0]),
1292 1293 'basename': entries[0],
1293 1294 'summary': summary,
1294 1295 })
1295 1296
1296 1297 return tmpl('helptopics', topics=topics, title=topicname,
1297 1298 subindex=True)
1298 1299
1299 1300 u = webutil.wsgiui.load()
1300 1301 u.verbose = True
1301 1302
1302 1303 # Render a page from a sub-topic.
1303 1304 if '.' in topicname:
1304 1305 # TODO implement support for rendering sections, like
1305 1306 # `hg help` works.
1306 1307 topic, subtopic = topicname.split('.', 1)
1307 1308 if topic not in helpmod.subtopics:
1308 1309 raise ErrorResponse(HTTP_NOT_FOUND)
1309 1310 else:
1310 1311 topic = topicname
1311 1312 subtopic = None
1312 1313
1313 1314 try:
1314 1315 doc = helpmod.help_(u, topic, subtopic=subtopic)
1315 1316 except error.UnknownCommand:
1316 1317 raise ErrorResponse(HTTP_NOT_FOUND)
1317 1318 return tmpl('help', topic=topicname, doc=doc)
1318 1319
1319 1320 # tell hggettext to extract docstrings from these functions:
1320 1321 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now