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