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