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