##// END OF EJS Templates
hgweb: use archivespecs (dict) instead of archives (tuple) for "in" check
av6 -
r30734:b9e49f7b default
parent child Browse files
Show More
@@ -1,1326 +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 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 977 parity = paritygen(web.stripecount, offset=start - end)
978 978
979 979 def entries():
980 980 l = []
981 981
982 982 repo = web.repo
983 983 revs = fctx.filelog().revs(start, end - 1)
984 984 for i in revs:
985 985 iterfctx = fctx.filectx(i)
986 986
987 987 l.append(dict(
988 988 parity=next(parity),
989 989 filerev=i,
990 990 file=f,
991 991 rename=webutil.renamelink(iterfctx),
992 992 **webutil.commonentry(repo, iterfctx)))
993 993 for e in reversed(l):
994 994 yield e
995 995
996 996 entries = list(entries())
997 997 latestentry = entries[:1]
998 998
999 999 revnav = webutil.filerevnav(web.repo, fctx.path())
1000 1000 nav = revnav.gen(end - 1, revcount, count)
1001 1001 return tmpl("filelog",
1002 1002 file=f,
1003 1003 nav=nav,
1004 1004 symrev=webutil.symrevorshortnode(req, fctx),
1005 1005 entries=entries,
1006 1006 latestentry=latestentry,
1007 1007 revcount=revcount,
1008 1008 morevars=morevars,
1009 1009 lessvars=lessvars,
1010 1010 **webutil.commonentry(web.repo, fctx))
1011 1011
1012 1012 @webcommand('archive')
1013 1013 def archive(web, req, tmpl):
1014 1014 """
1015 1015 /archive/{revision}.{format}[/{path}]
1016 1016 -------------------------------------
1017 1017
1018 1018 Obtain an archive of repository content.
1019 1019
1020 1020 The content and type of the archive is defined by a URL path parameter.
1021 1021 ``format`` is the file extension of the archive type to be generated. e.g.
1022 1022 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1023 1023 server configuration.
1024 1024
1025 1025 The optional ``path`` URL parameter controls content to include in the
1026 1026 archive. If omitted, every file in the specified revision is present in the
1027 1027 archive. If included, only the specified file or contents of the specified
1028 1028 directory will be included in the archive.
1029 1029
1030 1030 No template is used for this handler. Raw, binary content is generated.
1031 1031 """
1032 1032
1033 1033 type_ = req.form.get('type', [None])[0]
1034 1034 allowed = web.configlist("web", "allow_archive")
1035 1035 key = req.form['node'][0]
1036 1036
1037 if type_ not in web.archives:
1037 if type_ not in web.archivespecs:
1038 1038 msg = 'Unsupported archive type: %s' % type_
1039 1039 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1040 1040
1041 1041 if not ((type_ in allowed or
1042 1042 web.configbool("web", "allow" + type_, False))):
1043 1043 msg = 'Archive type not allowed: %s' % type_
1044 1044 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1045 1045
1046 1046 reponame = re.sub(r"\W+", "-", os.path.basename(web.reponame))
1047 1047 cnode = web.repo.lookup(key)
1048 1048 arch_version = key
1049 1049 if cnode == key or key == 'tip':
1050 1050 arch_version = short(cnode)
1051 1051 name = "%s-%s" % (reponame, arch_version)
1052 1052
1053 1053 ctx = webutil.changectx(web.repo, req)
1054 1054 pats = []
1055 1055 matchfn = scmutil.match(ctx, [])
1056 1056 file = req.form.get('file', None)
1057 1057 if file:
1058 1058 pats = ['path:' + file[0]]
1059 1059 matchfn = scmutil.match(ctx, pats, default='path')
1060 1060 if pats:
1061 1061 files = [f for f in ctx.manifest().keys() if matchfn(f)]
1062 1062 if not files:
1063 1063 raise ErrorResponse(HTTP_NOT_FOUND,
1064 1064 'file(s) not found: %s' % file[0])
1065 1065
1066 1066 mimetype, artype, extension, encoding = web.archivespecs[type_]
1067 1067 headers = [
1068 1068 ('Content-Disposition', 'attachment; filename=%s%s' % (name, extension))
1069 1069 ]
1070 1070 if encoding:
1071 1071 headers.append(('Content-Encoding', encoding))
1072 1072 req.headers.extend(headers)
1073 1073 req.respond(HTTP_OK, mimetype)
1074 1074
1075 1075 archival.archive(web.repo, req, cnode, artype, prefix=name,
1076 1076 matchfn=matchfn,
1077 1077 subrepos=web.configbool("web", "archivesubrepos"))
1078 1078 return []
1079 1079
1080 1080
1081 1081 @webcommand('static')
1082 1082 def static(web, req, tmpl):
1083 1083 fname = req.form['file'][0]
1084 1084 # a repo owner may set web.static in .hg/hgrc to get any file
1085 1085 # readable by the user running the CGI script
1086 1086 static = web.config("web", "static", None, untrusted=False)
1087 1087 if not static:
1088 1088 tp = web.templatepath or templater.templatepaths()
1089 1089 if isinstance(tp, str):
1090 1090 tp = [tp]
1091 1091 static = [os.path.join(p, 'static') for p in tp]
1092 1092 staticfile(static, fname, req)
1093 1093 return []
1094 1094
1095 1095 @webcommand('graph')
1096 1096 def graph(web, req, tmpl):
1097 1097 """
1098 1098 /graph[/{revision}]
1099 1099 -------------------
1100 1100
1101 1101 Show information about the graphical topology of the repository.
1102 1102
1103 1103 Information rendered by this handler can be used to create visual
1104 1104 representations of repository topology.
1105 1105
1106 1106 The ``revision`` URL parameter controls the starting changeset.
1107 1107
1108 1108 The ``revcount`` query string argument can define the number of changesets
1109 1109 to show information for.
1110 1110
1111 1111 This handler will render the ``graph`` template.
1112 1112 """
1113 1113
1114 1114 if 'node' in req.form:
1115 1115 ctx = webutil.changectx(web.repo, req)
1116 1116 symrev = webutil.symrevorshortnode(req, ctx)
1117 1117 else:
1118 1118 ctx = web.repo['tip']
1119 1119 symrev = 'tip'
1120 1120 rev = ctx.rev()
1121 1121
1122 1122 bg_height = 39
1123 1123 revcount = web.maxshortchanges
1124 1124 if 'revcount' in req.form:
1125 1125 try:
1126 1126 revcount = int(req.form.get('revcount', [revcount])[0])
1127 1127 revcount = max(revcount, 1)
1128 1128 tmpl.defaults['sessionvars']['revcount'] = revcount
1129 1129 except ValueError:
1130 1130 pass
1131 1131
1132 1132 lessvars = copy.copy(tmpl.defaults['sessionvars'])
1133 1133 lessvars['revcount'] = max(revcount / 2, 1)
1134 1134 morevars = copy.copy(tmpl.defaults['sessionvars'])
1135 1135 morevars['revcount'] = revcount * 2
1136 1136
1137 1137 count = len(web.repo)
1138 1138 pos = rev
1139 1139
1140 1140 uprev = min(max(0, count - 1), rev + revcount)
1141 1141 downrev = max(0, rev - revcount)
1142 1142 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1143 1143
1144 1144 tree = []
1145 1145 if pos != -1:
1146 1146 allrevs = web.repo.changelog.revs(pos, 0)
1147 1147 revs = []
1148 1148 for i in allrevs:
1149 1149 revs.append(i)
1150 1150 if len(revs) >= revcount:
1151 1151 break
1152 1152
1153 1153 # We have to feed a baseset to dagwalker as it is expecting smartset
1154 1154 # object. This does not have a big impact on hgweb performance itself
1155 1155 # since hgweb graphing code is not itself lazy yet.
1156 1156 dag = graphmod.dagwalker(web.repo, revset.baseset(revs))
1157 1157 # As we said one line above... not lazy.
1158 1158 tree = list(graphmod.colored(dag, web.repo))
1159 1159
1160 1160 def getcolumns(tree):
1161 1161 cols = 0
1162 1162 for (id, type, ctx, vtx, edges) in tree:
1163 1163 if type != graphmod.CHANGESET:
1164 1164 continue
1165 1165 cols = max(cols, max([edge[0] for edge in edges] or [0]),
1166 1166 max([edge[1] for edge in edges] or [0]))
1167 1167 return cols
1168 1168
1169 1169 def graphdata(usetuples, encodestr):
1170 1170 data = []
1171 1171
1172 1172 row = 0
1173 1173 for (id, type, ctx, vtx, edges) in tree:
1174 1174 if type != graphmod.CHANGESET:
1175 1175 continue
1176 1176 node = str(ctx)
1177 1177 age = encodestr(templatefilters.age(ctx.date()))
1178 1178 desc = templatefilters.firstline(encodestr(ctx.description()))
1179 1179 desc = cgi.escape(templatefilters.nonempty(desc))
1180 1180 user = cgi.escape(templatefilters.person(encodestr(ctx.user())))
1181 1181 branch = cgi.escape(encodestr(ctx.branch()))
1182 1182 try:
1183 1183 branchnode = web.repo.branchtip(branch)
1184 1184 except error.RepoLookupError:
1185 1185 branchnode = None
1186 1186 branch = branch, branchnode == ctx.node()
1187 1187
1188 1188 if usetuples:
1189 1189 data.append((node, vtx, edges, desc, user, age, branch,
1190 1190 [cgi.escape(encodestr(x)) for x in ctx.tags()],
1191 1191 [cgi.escape(encodestr(x))
1192 1192 for x in ctx.bookmarks()]))
1193 1193 else:
1194 1194 edgedata = [{'col': edge[0], 'nextcol': edge[1],
1195 1195 'color': (edge[2] - 1) % 6 + 1,
1196 1196 'width': edge[3], 'bcolor': edge[4]}
1197 1197 for edge in edges]
1198 1198
1199 1199 data.append(
1200 1200 {'node': node,
1201 1201 'col': vtx[0],
1202 1202 'color': (vtx[1] - 1) % 6 + 1,
1203 1203 'edges': edgedata,
1204 1204 'row': row,
1205 1205 'nextrow': row + 1,
1206 1206 'desc': desc,
1207 1207 'user': user,
1208 1208 'age': age,
1209 1209 'bookmarks': webutil.nodebookmarksdict(
1210 1210 web.repo, ctx.node()),
1211 1211 'branches': webutil.nodebranchdict(web.repo, ctx),
1212 1212 'inbranch': webutil.nodeinbranch(web.repo, ctx),
1213 1213 'tags': webutil.nodetagsdict(web.repo, ctx.node())})
1214 1214
1215 1215 row += 1
1216 1216
1217 1217 return data
1218 1218
1219 1219 cols = getcolumns(tree)
1220 1220 rows = len(tree)
1221 1221 canvasheight = (rows + 1) * bg_height - 27
1222 1222
1223 1223 return tmpl('graph', rev=rev, symrev=symrev, revcount=revcount,
1224 1224 uprev=uprev,
1225 1225 lessvars=lessvars, morevars=morevars, downrev=downrev,
1226 1226 cols=cols, rows=rows,
1227 1227 canvaswidth=(cols + 1) * bg_height,
1228 1228 truecanvasheight=rows * bg_height,
1229 1229 canvasheight=canvasheight, bg_height=bg_height,
1230 1230 # {jsdata} will be passed to |json, so it must be in utf-8
1231 1231 jsdata=lambda **x: graphdata(True, encoding.fromlocal),
1232 1232 nodes=lambda **x: graphdata(False, str),
1233 1233 node=ctx.hex(), changenav=changenav)
1234 1234
1235 1235 def _getdoc(e):
1236 1236 doc = e[0].__doc__
1237 1237 if doc:
1238 1238 doc = _(doc).partition('\n')[0]
1239 1239 else:
1240 1240 doc = _('(no help text available)')
1241 1241 return doc
1242 1242
1243 1243 @webcommand('help')
1244 1244 def help(web, req, tmpl):
1245 1245 """
1246 1246 /help[/{topic}]
1247 1247 ---------------
1248 1248
1249 1249 Render help documentation.
1250 1250
1251 1251 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1252 1252 is defined, that help topic will be rendered. If not, an index of
1253 1253 available help topics will be rendered.
1254 1254
1255 1255 The ``help`` template will be rendered when requesting help for a topic.
1256 1256 ``helptopics`` will be rendered for the index of help topics.
1257 1257 """
1258 1258 from .. import commands, help as helpmod # avoid cycle
1259 1259
1260 1260 topicname = req.form.get('node', [None])[0]
1261 1261 if not topicname:
1262 1262 def topics(**map):
1263 1263 for entries, summary, _doc in helpmod.helptable:
1264 1264 yield {'topic': entries[0], 'summary': summary}
1265 1265
1266 1266 early, other = [], []
1267 1267 primary = lambda s: s.partition('|')[0]
1268 1268 for c, e in commands.table.iteritems():
1269 1269 doc = _getdoc(e)
1270 1270 if 'DEPRECATED' in doc or c.startswith('debug'):
1271 1271 continue
1272 1272 cmd = primary(c)
1273 1273 if cmd.startswith('^'):
1274 1274 early.append((cmd[1:], doc))
1275 1275 else:
1276 1276 other.append((cmd, doc))
1277 1277
1278 1278 early.sort()
1279 1279 other.sort()
1280 1280
1281 1281 def earlycommands(**map):
1282 1282 for c, doc in early:
1283 1283 yield {'topic': c, 'summary': doc}
1284 1284
1285 1285 def othercommands(**map):
1286 1286 for c, doc in other:
1287 1287 yield {'topic': c, 'summary': doc}
1288 1288
1289 1289 return tmpl('helptopics', topics=topics, earlycommands=earlycommands,
1290 1290 othercommands=othercommands, title='Index')
1291 1291
1292 1292 # Render an index of sub-topics.
1293 1293 if topicname in helpmod.subtopics:
1294 1294 topics = []
1295 1295 for entries, summary, _doc in helpmod.subtopics[topicname]:
1296 1296 topics.append({
1297 1297 'topic': '%s.%s' % (topicname, entries[0]),
1298 1298 'basename': entries[0],
1299 1299 'summary': summary,
1300 1300 })
1301 1301
1302 1302 return tmpl('helptopics', topics=topics, title=topicname,
1303 1303 subindex=True)
1304 1304
1305 1305 u = webutil.wsgiui.load()
1306 1306 u.verbose = True
1307 1307
1308 1308 # Render a page from a sub-topic.
1309 1309 if '.' in topicname:
1310 1310 # TODO implement support for rendering sections, like
1311 1311 # `hg help` works.
1312 1312 topic, subtopic = topicname.split('.', 1)
1313 1313 if topic not in helpmod.subtopics:
1314 1314 raise ErrorResponse(HTTP_NOT_FOUND)
1315 1315 else:
1316 1316 topic = topicname
1317 1317 subtopic = None
1318 1318
1319 1319 try:
1320 1320 doc = helpmod.help_(u, topic, subtopic=subtopic)
1321 1321 except error.UnknownCommand:
1322 1322 raise ErrorResponse(HTTP_NOT_FOUND)
1323 1323 return tmpl('help', topic=topicname, doc=doc)
1324 1324
1325 1325 # tell hggettext to extract docstrings from these functions:
1326 1326 i18nfunctions = commands.values()
General Comments 0
You need to be logged in to leave comments. Login now