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