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