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