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