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