##// END OF EJS Templates
hgweb: remove unused argument 'tmpl' from listfilediffs()
Yuya Nishihara -
r37973:9482498b default
parent child Browse files
Show More
@@ -1,1477 +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 files = webutil.listfilediffs(web.tmpl, ctx.files(), n,
300 web.maxfiles)
299 files = webutil.listfilediffs(ctx.files(), n, web.maxfiles)
301 300
302 301 lm = webutil.commonentry(web.repo, ctx)
303 302 lm.update({
304 303 'parity': next(parity),
305 304 'changelogtag': showtags,
306 305 'files': files,
307 306 })
308 307 yield lm
309 308
310 309 if count >= revcount:
311 310 break
312 311
313 312 query = web.req.qsparams['rev']
314 313 revcount = web.maxchanges
315 314 if 'revcount' in web.req.qsparams:
316 315 try:
317 316 revcount = int(web.req.qsparams.get('revcount', revcount))
318 317 revcount = max(revcount, 1)
319 318 web.tmpl.defaults['sessionvars']['revcount'] = revcount
320 319 except ValueError:
321 320 pass
322 321
323 322 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
324 323 lessvars['revcount'] = max(revcount // 2, 1)
325 324 lessvars['rev'] = query
326 325 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
327 326 morevars['revcount'] = revcount * 2
328 327 morevars['rev'] = query
329 328
330 329 mode, funcarg = getsearchmode(query)
331 330
332 331 if 'forcekw' in web.req.qsparams:
333 332 showforcekw = ''
334 333 showunforcekw = searchfuncs[mode][1]
335 334 mode = MODE_KEYWORD
336 335 funcarg = query
337 336 else:
338 337 if mode != MODE_KEYWORD:
339 338 showforcekw = searchfuncs[MODE_KEYWORD][1]
340 339 else:
341 340 showforcekw = ''
342 341 showunforcekw = ''
343 342
344 343 searchfunc = searchfuncs[mode]
345 344
346 345 tip = web.repo['tip']
347 346 parity = paritygen(web.stripecount)
348 347
349 348 return web.sendtemplate(
350 349 'search',
351 350 query=query,
352 351 node=tip.hex(),
353 352 symrev='tip',
354 353 entries=templateutil.mappinggenerator(changelist, name='searchentry'),
355 354 archives=web.archivelist('tip'),
356 355 morevars=morevars,
357 356 lessvars=lessvars,
358 357 modedesc=searchfunc[1],
359 358 showforcekw=showforcekw,
360 359 showunforcekw=showunforcekw)
361 360
362 361 @webcommand('changelog')
363 362 def changelog(web, shortlog=False):
364 363 """
365 364 /changelog[/{revision}]
366 365 -----------------------
367 366
368 367 Show information about multiple changesets.
369 368
370 369 If the optional ``revision`` URL argument is absent, information about
371 370 all changesets starting at ``tip`` will be rendered. If the ``revision``
372 371 argument is present, changesets will be shown starting from the specified
373 372 revision.
374 373
375 374 If ``revision`` is absent, the ``rev`` query string argument may be
376 375 defined. This will perform a search for changesets.
377 376
378 377 The argument for ``rev`` can be a single revision, a revision set,
379 378 or a literal keyword to search for in changeset data (equivalent to
380 379 :hg:`log -k`).
381 380
382 381 The ``revcount`` query string argument defines the maximum numbers of
383 382 changesets to render.
384 383
385 384 For non-searches, the ``changelog`` template will be rendered.
386 385 """
387 386
388 387 query = ''
389 388 if 'node' in web.req.qsparams:
390 389 ctx = webutil.changectx(web.repo, web.req)
391 390 symrev = webutil.symrevorshortnode(web.req, ctx)
392 391 elif 'rev' in web.req.qsparams:
393 392 return _search(web)
394 393 else:
395 394 ctx = web.repo['tip']
396 395 symrev = 'tip'
397 396
398 397 def changelist():
399 398 revs = []
400 399 if pos != -1:
401 400 revs = web.repo.changelog.revs(pos, 0)
402 401 curcount = 0
403 402 for rev in revs:
404 403 curcount += 1
405 404 if curcount > revcount + 1:
406 405 break
407 406
408 407 entry = webutil.changelistentry(web, web.repo[rev])
409 408 entry['parity'] = next(parity)
410 409 yield entry
411 410
412 411 if shortlog:
413 412 revcount = web.maxshortchanges
414 413 else:
415 414 revcount = web.maxchanges
416 415
417 416 if 'revcount' in web.req.qsparams:
418 417 try:
419 418 revcount = int(web.req.qsparams.get('revcount', revcount))
420 419 revcount = max(revcount, 1)
421 420 web.tmpl.defaults['sessionvars']['revcount'] = revcount
422 421 except ValueError:
423 422 pass
424 423
425 424 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
426 425 lessvars['revcount'] = max(revcount // 2, 1)
427 426 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
428 427 morevars['revcount'] = revcount * 2
429 428
430 429 count = len(web.repo)
431 430 pos = ctx.rev()
432 431 parity = paritygen(web.stripecount)
433 432
434 433 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
435 434
436 435 entries = list(changelist())
437 436 latestentry = entries[:1]
438 437 if len(entries) > revcount:
439 438 nextentry = entries[-1:]
440 439 entries = entries[:-1]
441 440 else:
442 441 nextentry = []
443 442
444 443 return web.sendtemplate(
445 444 'shortlog' if shortlog else 'changelog',
446 445 changenav=changenav,
447 446 node=ctx.hex(),
448 447 rev=pos,
449 448 symrev=symrev,
450 449 changesets=count,
451 450 entries=entries,
452 451 latestentry=latestentry,
453 452 nextentry=nextentry,
454 453 archives=web.archivelist('tip'),
455 454 revcount=revcount,
456 455 morevars=morevars,
457 456 lessvars=lessvars,
458 457 query=query)
459 458
460 459 @webcommand('shortlog')
461 460 def shortlog(web):
462 461 """
463 462 /shortlog
464 463 ---------
465 464
466 465 Show basic information about a set of changesets.
467 466
468 467 This accepts the same parameters as the ``changelog`` handler. The only
469 468 difference is the ``shortlog`` template will be rendered instead of the
470 469 ``changelog`` template.
471 470 """
472 471 return changelog(web, shortlog=True)
473 472
474 473 @webcommand('changeset')
475 474 def changeset(web):
476 475 """
477 476 /changeset[/{revision}]
478 477 -----------------------
479 478
480 479 Show information about a single changeset.
481 480
482 481 A URL path argument is the changeset identifier to show. See ``hg help
483 482 revisions`` for possible values. If not defined, the ``tip`` changeset
484 483 will be shown.
485 484
486 485 The ``changeset`` template is rendered. Contents of the ``changesettag``,
487 486 ``changesetbookmark``, ``filenodelink``, ``filenolink``, and the many
488 487 templates related to diffs may all be used to produce the output.
489 488 """
490 489 ctx = webutil.changectx(web.repo, web.req)
491 490
492 491 return web.sendtemplate(
493 492 'changeset',
494 493 **webutil.changesetentry(web, ctx))
495 494
496 495 rev = webcommand('rev')(changeset)
497 496
498 497 def decodepath(path):
499 498 """Hook for mapping a path in the repository to a path in the
500 499 working copy.
501 500
502 501 Extensions (e.g., largefiles) can override this to remap files in
503 502 the virtual file system presented by the manifest command below."""
504 503 return path
505 504
506 505 @webcommand('manifest')
507 506 def manifest(web):
508 507 """
509 508 /manifest[/{revision}[/{path}]]
510 509 -------------------------------
511 510
512 511 Show information about a directory.
513 512
514 513 If the URL path arguments are omitted, information about the root
515 514 directory for the ``tip`` changeset will be shown.
516 515
517 516 Because this handler can only show information for directories, it
518 517 is recommended to use the ``file`` handler instead, as it can handle both
519 518 directories and files.
520 519
521 520 The ``manifest`` template will be rendered for this handler.
522 521 """
523 522 if 'node' in web.req.qsparams:
524 523 ctx = webutil.changectx(web.repo, web.req)
525 524 symrev = webutil.symrevorshortnode(web.req, ctx)
526 525 else:
527 526 ctx = web.repo['tip']
528 527 symrev = 'tip'
529 528 path = webutil.cleanpath(web.repo, web.req.qsparams.get('file', ''))
530 529 mf = ctx.manifest()
531 530 node = ctx.node()
532 531
533 532 files = {}
534 533 dirs = {}
535 534 parity = paritygen(web.stripecount)
536 535
537 536 if path and path[-1:] != "/":
538 537 path += "/"
539 538 l = len(path)
540 539 abspath = "/" + path
541 540
542 541 for full, n in mf.iteritems():
543 542 # the virtual path (working copy path) used for the full
544 543 # (repository) path
545 544 f = decodepath(full)
546 545
547 546 if f[:l] != path:
548 547 continue
549 548 remain = f[l:]
550 549 elements = remain.split('/')
551 550 if len(elements) == 1:
552 551 files[remain] = full
553 552 else:
554 553 h = dirs # need to retain ref to dirs (root)
555 554 for elem in elements[0:-1]:
556 555 if elem not in h:
557 556 h[elem] = {}
558 557 h = h[elem]
559 558 if len(h) > 1:
560 559 break
561 560 h[None] = None # denotes files present
562 561
563 562 if mf and not files and not dirs:
564 563 raise ErrorResponse(HTTP_NOT_FOUND, 'path not found: ' + path)
565 564
566 565 def filelist(**map):
567 566 for f in sorted(files):
568 567 full = files[f]
569 568
570 569 fctx = ctx.filectx(full)
571 570 yield {"file": full,
572 571 "parity": next(parity),
573 572 "basename": f,
574 573 "date": fctx.date(),
575 574 "size": fctx.size(),
576 575 "permissions": mf.flags(full)}
577 576
578 577 def dirlist(**map):
579 578 for d in sorted(dirs):
580 579
581 580 emptydirs = []
582 581 h = dirs[d]
583 582 while isinstance(h, dict) and len(h) == 1:
584 583 k, v = next(iter(h.items()))
585 584 if v:
586 585 emptydirs.append(k)
587 586 h = v
588 587
589 588 path = "%s%s" % (abspath, d)
590 589 yield {"parity": next(parity),
591 590 "path": path,
592 591 "emptydirs": "/".join(emptydirs),
593 592 "basename": d}
594 593
595 594 return web.sendtemplate(
596 595 'manifest',
597 596 symrev=symrev,
598 597 path=abspath,
599 598 up=webutil.up(abspath),
600 599 upparity=next(parity),
601 600 fentries=filelist,
602 601 dentries=dirlist,
603 602 archives=web.archivelist(hex(node)),
604 603 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
605 604
606 605 @webcommand('tags')
607 606 def tags(web):
608 607 """
609 608 /tags
610 609 -----
611 610
612 611 Show information about tags.
613 612
614 613 No arguments are accepted.
615 614
616 615 The ``tags`` template is rendered.
617 616 """
618 617 i = list(reversed(web.repo.tagslist()))
619 618 parity = paritygen(web.stripecount)
620 619
621 620 def entries(notip, latestonly, **map):
622 621 t = i
623 622 if notip:
624 623 t = [(k, n) for k, n in i if k != "tip"]
625 624 if latestonly:
626 625 t = t[:1]
627 626 for k, n in t:
628 627 yield {"parity": next(parity),
629 628 "tag": k,
630 629 "date": web.repo[n].date(),
631 630 "node": hex(n)}
632 631
633 632 return web.sendtemplate(
634 633 'tags',
635 634 node=hex(web.repo.changelog.tip()),
636 635 entries=lambda **x: entries(False, False, **x),
637 636 entriesnotip=lambda **x: entries(True, False, **x),
638 637 latestentry=lambda **x: entries(True, True, **x))
639 638
640 639 @webcommand('bookmarks')
641 640 def bookmarks(web):
642 641 """
643 642 /bookmarks
644 643 ----------
645 644
646 645 Show information about bookmarks.
647 646
648 647 No arguments are accepted.
649 648
650 649 The ``bookmarks`` template is rendered.
651 650 """
652 651 i = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
653 652 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
654 653 i = sorted(i, key=sortkey, reverse=True)
655 654 parity = paritygen(web.stripecount)
656 655
657 656 def entries(latestonly, **map):
658 657 t = i
659 658 if latestonly:
660 659 t = i[:1]
661 660 for k, n in t:
662 661 yield {"parity": next(parity),
663 662 "bookmark": k,
664 663 "date": web.repo[n].date(),
665 664 "node": hex(n)}
666 665
667 666 if i:
668 667 latestrev = i[0][1]
669 668 else:
670 669 latestrev = -1
671 670
672 671 return web.sendtemplate(
673 672 'bookmarks',
674 673 node=hex(web.repo.changelog.tip()),
675 674 lastchange=[{'date': web.repo[latestrev].date()}],
676 675 entries=lambda **x: entries(latestonly=False, **x),
677 676 latestentry=lambda **x: entries(latestonly=True, **x))
678 677
679 678 @webcommand('branches')
680 679 def branches(web):
681 680 """
682 681 /branches
683 682 ---------
684 683
685 684 Show information about branches.
686 685
687 686 All known branches are contained in the output, even closed branches.
688 687
689 688 No arguments are accepted.
690 689
691 690 The ``branches`` template is rendered.
692 691 """
693 692 entries = webutil.branchentries(web.repo, web.stripecount)
694 693 latestentry = webutil.branchentries(web.repo, web.stripecount, 1)
695 694
696 695 return web.sendtemplate(
697 696 'branches',
698 697 node=hex(web.repo.changelog.tip()),
699 698 entries=entries,
700 699 latestentry=latestentry)
701 700
702 701 @webcommand('summary')
703 702 def summary(web):
704 703 """
705 704 /summary
706 705 --------
707 706
708 707 Show a summary of repository state.
709 708
710 709 Information about the latest changesets, bookmarks, tags, and branches
711 710 is captured by this handler.
712 711
713 712 The ``summary`` template is rendered.
714 713 """
715 714 i = reversed(web.repo.tagslist())
716 715
717 716 def tagentries(context):
718 717 parity = paritygen(web.stripecount)
719 718 count = 0
720 719 for k, n in i:
721 720 if k == "tip": # skip tip
722 721 continue
723 722
724 723 count += 1
725 724 if count > 10: # limit to 10 tags
726 725 break
727 726
728 727 yield {
729 728 'parity': next(parity),
730 729 'tag': k,
731 730 'node': hex(n),
732 731 'date': web.repo[n].date(),
733 732 }
734 733
735 734 def bookmarks(**map):
736 735 parity = paritygen(web.stripecount)
737 736 marks = [b for b in web.repo._bookmarks.items() if b[1] in web.repo]
738 737 sortkey = lambda b: (web.repo[b[1]].rev(), b[0])
739 738 marks = sorted(marks, key=sortkey, reverse=True)
740 739 for k, n in marks[:10]: # limit to 10 bookmarks
741 740 yield {'parity': next(parity),
742 741 'bookmark': k,
743 742 'date': web.repo[n].date(),
744 743 'node': hex(n)}
745 744
746 745 def changelist(context):
747 746 parity = paritygen(web.stripecount, offset=start - end)
748 747 l = [] # build a list in forward order for efficiency
749 748 revs = []
750 749 if start < end:
751 750 revs = web.repo.changelog.revs(start, end - 1)
752 751 for i in revs:
753 752 ctx = web.repo[i]
754 753 lm = webutil.commonentry(web.repo, ctx)
755 754 lm['parity'] = next(parity)
756 755 l.append(lm)
757 756
758 757 for entry in reversed(l):
759 758 yield entry
760 759
761 760 tip = web.repo['tip']
762 761 count = len(web.repo)
763 762 start = max(0, count - web.maxchanges)
764 763 end = min(count, start + web.maxchanges)
765 764
766 765 desc = web.config("web", "description")
767 766 if not desc:
768 767 desc = 'unknown'
769 768 labels = web.configlist('web', 'labels')
770 769
771 770 return web.sendtemplate(
772 771 'summary',
773 772 desc=desc,
774 773 owner=get_contact(web.config) or 'unknown',
775 774 lastchange=tip.date(),
776 775 tags=templateutil.mappinggenerator(tagentries, name='tagentry'),
777 776 bookmarks=bookmarks,
778 777 branches=webutil.branchentries(web.repo, web.stripecount, 10),
779 778 shortlog=templateutil.mappinggenerator(changelist,
780 779 name='shortlogentry'),
781 780 node=tip.hex(),
782 781 symrev='tip',
783 782 archives=web.archivelist('tip'),
784 783 labels=templateutil.hybridlist(labels, name='label'))
785 784
786 785 @webcommand('filediff')
787 786 def filediff(web):
788 787 """
789 788 /diff/{revision}/{path}
790 789 -----------------------
791 790
792 791 Show how a file changed in a particular commit.
793 792
794 793 The ``filediff`` template is rendered.
795 794
796 795 This handler is registered under both the ``/diff`` and ``/filediff``
797 796 paths. ``/diff`` is used in modern code.
798 797 """
799 798 fctx, ctx = None, None
800 799 try:
801 800 fctx = webutil.filectx(web.repo, web.req)
802 801 except LookupError:
803 802 ctx = webutil.changectx(web.repo, web.req)
804 803 path = webutil.cleanpath(web.repo, web.req.qsparams['file'])
805 804 if path not in ctx.files():
806 805 raise
807 806
808 807 if fctx is not None:
809 808 path = fctx.path()
810 809 ctx = fctx.changectx()
811 810 basectx = ctx.p1()
812 811
813 812 style = web.config('web', 'style')
814 813 if 'style' in web.req.qsparams:
815 814 style = web.req.qsparams['style']
816 815
817 816 diffs = webutil.diffs(web, ctx, basectx, [path], style)
818 817 if fctx is not None:
819 818 rename = webutil.renamelink(fctx)
820 819 ctx = fctx
821 820 else:
822 821 rename = templateutil.mappinglist([])
823 822 ctx = ctx
824 823
825 824 return web.sendtemplate(
826 825 'filediff',
827 826 file=path,
828 827 symrev=webutil.symrevorshortnode(web.req, ctx),
829 828 rename=rename,
830 829 diff=diffs,
831 830 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
832 831
833 832 diff = webcommand('diff')(filediff)
834 833
835 834 @webcommand('comparison')
836 835 def comparison(web):
837 836 """
838 837 /comparison/{revision}/{path}
839 838 -----------------------------
840 839
841 840 Show a comparison between the old and new versions of a file from changes
842 841 made on a particular revision.
843 842
844 843 This is similar to the ``diff`` handler. However, this form features
845 844 a split or side-by-side diff rather than a unified diff.
846 845
847 846 The ``context`` query string argument can be used to control the lines of
848 847 context in the diff.
849 848
850 849 The ``filecomparison`` template is rendered.
851 850 """
852 851 ctx = webutil.changectx(web.repo, web.req)
853 852 if 'file' not in web.req.qsparams:
854 853 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
855 854 path = webutil.cleanpath(web.repo, web.req.qsparams['file'])
856 855
857 856 parsecontext = lambda v: v == 'full' and -1 or int(v)
858 857 if 'context' in web.req.qsparams:
859 858 context = parsecontext(web.req.qsparams['context'])
860 859 else:
861 860 context = parsecontext(web.config('web', 'comparisoncontext', '5'))
862 861
863 862 def filelines(f):
864 863 if f.isbinary():
865 864 mt = mimetypes.guess_type(f.path())[0]
866 865 if not mt:
867 866 mt = 'application/octet-stream'
868 867 return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
869 868 return f.data().splitlines()
870 869
871 870 fctx = None
872 871 parent = ctx.p1()
873 872 leftrev = parent.rev()
874 873 leftnode = parent.node()
875 874 rightrev = ctx.rev()
876 875 rightnode = ctx.node()
877 876 if path in ctx:
878 877 fctx = ctx[path]
879 878 rightlines = filelines(fctx)
880 879 if path not in parent:
881 880 leftlines = ()
882 881 else:
883 882 pfctx = parent[path]
884 883 leftlines = filelines(pfctx)
885 884 else:
886 885 rightlines = ()
887 886 pfctx = ctx.parents()[0][path]
888 887 leftlines = filelines(pfctx)
889 888
890 889 comparison = webutil.compare(web.tmpl, context, leftlines, rightlines)
891 890 if fctx is not None:
892 891 rename = webutil.renamelink(fctx)
893 892 ctx = fctx
894 893 else:
895 894 rename = templateutil.mappinglist([])
896 895 ctx = ctx
897 896
898 897 return web.sendtemplate(
899 898 'filecomparison',
900 899 file=path,
901 900 symrev=webutil.symrevorshortnode(web.req, ctx),
902 901 rename=rename,
903 902 leftrev=leftrev,
904 903 leftnode=hex(leftnode),
905 904 rightrev=rightrev,
906 905 rightnode=hex(rightnode),
907 906 comparison=comparison,
908 907 **pycompat.strkwargs(webutil.commonentry(web.repo, ctx)))
909 908
910 909 @webcommand('annotate')
911 910 def annotate(web):
912 911 """
913 912 /annotate/{revision}/{path}
914 913 ---------------------------
915 914
916 915 Show changeset information for each line in a file.
917 916
918 917 The ``ignorews``, ``ignorewsamount``, ``ignorewseol``, and
919 918 ``ignoreblanklines`` query string arguments have the same meaning as
920 919 their ``[annotate]`` config equivalents. It uses the hgrc boolean
921 920 parsing logic to interpret the value. e.g. ``0`` and ``false`` are
922 921 false and ``1`` and ``true`` are true. If not defined, the server
923 922 default settings are used.
924 923
925 924 The ``fileannotate`` template is rendered.
926 925 """
927 926 fctx = webutil.filectx(web.repo, web.req)
928 927 f = fctx.path()
929 928 parity = paritygen(web.stripecount)
930 929 ishead = fctx.filerev() in fctx.filelog().headrevs()
931 930
932 931 # parents() is called once per line and several lines likely belong to
933 932 # same revision. So it is worth caching.
934 933 # TODO there are still redundant operations within basefilectx.parents()
935 934 # and from the fctx.annotate() call itself that could be cached.
936 935 parentscache = {}
937 936 def parents(f):
938 937 rev = f.rev()
939 938 if rev not in parentscache:
940 939 parentscache[rev] = []
941 940 for p in f.parents():
942 941 entry = {
943 942 'node': p.hex(),
944 943 'rev': p.rev(),
945 944 }
946 945 parentscache[rev].append(entry)
947 946
948 947 for p in parentscache[rev]:
949 948 yield p
950 949
951 950 def annotate(**map):
952 951 if fctx.isbinary():
953 952 mt = (mimetypes.guess_type(fctx.path())[0]
954 953 or 'application/octet-stream')
955 954 lines = [dagop.annotateline(fctx=fctx.filectx(fctx.filerev()),
956 955 lineno=1, text='(binary:%s)' % mt)]
957 956 else:
958 957 lines = webutil.annotate(web.req, fctx, web.repo.ui)
959 958
960 959 previousrev = None
961 960 blockparitygen = paritygen(1)
962 961 for lineno, aline in enumerate(lines):
963 962 f = aline.fctx
964 963 rev = f.rev()
965 964 if rev != previousrev:
966 965 blockhead = True
967 966 blockparity = next(blockparitygen)
968 967 else:
969 968 blockhead = None
970 969 previousrev = rev
971 970 yield {"parity": next(parity),
972 971 "node": f.hex(),
973 972 "rev": rev,
974 973 "author": f.user(),
975 974 "parents": parents(f),
976 975 "desc": f.description(),
977 976 "extra": f.extra(),
978 977 "file": f.path(),
979 978 "blockhead": blockhead,
980 979 "blockparity": blockparity,
981 980 "targetline": aline.lineno,
982 981 "line": aline.text,
983 982 "lineno": lineno + 1,
984 983 "lineid": "l%d" % (lineno + 1),
985 984 "linenumber": "% 6d" % (lineno + 1),
986 985 "revdate": f.date()}
987 986
988 987 diffopts = webutil.difffeatureopts(web.req, web.repo.ui, 'annotate')
989 988 diffopts = {k: getattr(diffopts, k) for k in diffopts.defaults}
990 989
991 990 return web.sendtemplate(
992 991 'fileannotate',
993 992 file=f,
994 993 annotate=annotate,
995 994 path=webutil.up(f),
996 995 symrev=webutil.symrevorshortnode(web.req, fctx),
997 996 rename=webutil.renamelink(fctx),
998 997 permissions=fctx.manifest().flags(f),
999 998 ishead=int(ishead),
1000 999 diffopts=diffopts,
1001 1000 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
1002 1001
1003 1002 @webcommand('filelog')
1004 1003 def filelog(web):
1005 1004 """
1006 1005 /filelog/{revision}/{path}
1007 1006 --------------------------
1008 1007
1009 1008 Show information about the history of a file in the repository.
1010 1009
1011 1010 The ``revcount`` query string argument can be defined to control the
1012 1011 maximum number of entries to show.
1013 1012
1014 1013 The ``filelog`` template will be rendered.
1015 1014 """
1016 1015
1017 1016 try:
1018 1017 fctx = webutil.filectx(web.repo, web.req)
1019 1018 f = fctx.path()
1020 1019 fl = fctx.filelog()
1021 1020 except error.LookupError:
1022 1021 f = webutil.cleanpath(web.repo, web.req.qsparams['file'])
1023 1022 fl = web.repo.file(f)
1024 1023 numrevs = len(fl)
1025 1024 if not numrevs: # file doesn't exist at all
1026 1025 raise
1027 1026 rev = webutil.changectx(web.repo, web.req).rev()
1028 1027 first = fl.linkrev(0)
1029 1028 if rev < first: # current rev is from before file existed
1030 1029 raise
1031 1030 frev = numrevs - 1
1032 1031 while fl.linkrev(frev) > rev:
1033 1032 frev -= 1
1034 1033 fctx = web.repo.filectx(f, fl.linkrev(frev))
1035 1034
1036 1035 revcount = web.maxshortchanges
1037 1036 if 'revcount' in web.req.qsparams:
1038 1037 try:
1039 1038 revcount = int(web.req.qsparams.get('revcount', revcount))
1040 1039 revcount = max(revcount, 1)
1041 1040 web.tmpl.defaults['sessionvars']['revcount'] = revcount
1042 1041 except ValueError:
1043 1042 pass
1044 1043
1045 1044 lrange = webutil.linerange(web.req)
1046 1045
1047 1046 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
1048 1047 lessvars['revcount'] = max(revcount // 2, 1)
1049 1048 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
1050 1049 morevars['revcount'] = revcount * 2
1051 1050
1052 1051 patch = 'patch' in web.req.qsparams
1053 1052 if patch:
1054 1053 lessvars['patch'] = morevars['patch'] = web.req.qsparams['patch']
1055 1054 descend = 'descend' in web.req.qsparams
1056 1055 if descend:
1057 1056 lessvars['descend'] = morevars['descend'] = web.req.qsparams['descend']
1058 1057
1059 1058 count = fctx.filerev() + 1
1060 1059 start = max(0, count - revcount) # first rev on this page
1061 1060 end = min(count, start + revcount) # last rev on this page
1062 1061 parity = paritygen(web.stripecount, offset=start - end)
1063 1062
1064 1063 repo = web.repo
1065 1064 filelog = fctx.filelog()
1066 1065 revs = [filerev for filerev in filelog.revs(start, end - 1)
1067 1066 if filelog.linkrev(filerev) in repo]
1068 1067 entries = []
1069 1068
1070 1069 diffstyle = web.config('web', 'style')
1071 1070 if 'style' in web.req.qsparams:
1072 1071 diffstyle = web.req.qsparams['style']
1073 1072
1074 1073 def diff(fctx, linerange=None):
1075 1074 ctx = fctx.changectx()
1076 1075 basectx = ctx.p1()
1077 1076 path = fctx.path()
1078 1077 return webutil.diffs(web, ctx, basectx, [path], diffstyle,
1079 1078 linerange=linerange,
1080 1079 lineidprefix='%s-' % ctx.hex()[:12])
1081 1080
1082 1081 linerange = None
1083 1082 if lrange is not None:
1084 1083 linerange = webutil.formatlinerange(*lrange)
1085 1084 # deactivate numeric nav links when linerange is specified as this
1086 1085 # would required a dedicated "revnav" class
1087 1086 nav = templateutil.mappinglist([])
1088 1087 if descend:
1089 1088 it = dagop.blockdescendants(fctx, *lrange)
1090 1089 else:
1091 1090 it = dagop.blockancestors(fctx, *lrange)
1092 1091 for i, (c, lr) in enumerate(it, 1):
1093 1092 diffs = None
1094 1093 if patch:
1095 1094 diffs = diff(c, linerange=lr)
1096 1095 # follow renames accross filtered (not in range) revisions
1097 1096 path = c.path()
1098 1097 entries.append(dict(
1099 1098 parity=next(parity),
1100 1099 filerev=c.rev(),
1101 1100 file=path,
1102 1101 diff=diffs,
1103 1102 linerange=webutil.formatlinerange(*lr),
1104 1103 **pycompat.strkwargs(webutil.commonentry(repo, c))))
1105 1104 if i == revcount:
1106 1105 break
1107 1106 lessvars['linerange'] = webutil.formatlinerange(*lrange)
1108 1107 morevars['linerange'] = lessvars['linerange']
1109 1108 else:
1110 1109 for i in revs:
1111 1110 iterfctx = fctx.filectx(i)
1112 1111 diffs = None
1113 1112 if patch:
1114 1113 diffs = diff(iterfctx)
1115 1114 entries.append(dict(
1116 1115 parity=next(parity),
1117 1116 filerev=i,
1118 1117 file=f,
1119 1118 diff=diffs,
1120 1119 rename=webutil.renamelink(iterfctx),
1121 1120 **pycompat.strkwargs(webutil.commonentry(repo, iterfctx))))
1122 1121 entries.reverse()
1123 1122 revnav = webutil.filerevnav(web.repo, fctx.path())
1124 1123 nav = revnav.gen(end - 1, revcount, count)
1125 1124
1126 1125 latestentry = entries[:1]
1127 1126
1128 1127 return web.sendtemplate(
1129 1128 'filelog',
1130 1129 file=f,
1131 1130 nav=nav,
1132 1131 symrev=webutil.symrevorshortnode(web.req, fctx),
1133 1132 entries=entries,
1134 1133 descend=descend,
1135 1134 patch=patch,
1136 1135 latestentry=latestentry,
1137 1136 linerange=linerange,
1138 1137 revcount=revcount,
1139 1138 morevars=morevars,
1140 1139 lessvars=lessvars,
1141 1140 **pycompat.strkwargs(webutil.commonentry(web.repo, fctx)))
1142 1141
1143 1142 @webcommand('archive')
1144 1143 def archive(web):
1145 1144 """
1146 1145 /archive/{revision}.{format}[/{path}]
1147 1146 -------------------------------------
1148 1147
1149 1148 Obtain an archive of repository content.
1150 1149
1151 1150 The content and type of the archive is defined by a URL path parameter.
1152 1151 ``format`` is the file extension of the archive type to be generated. e.g.
1153 1152 ``zip`` or ``tar.bz2``. Not all archive types may be allowed by your
1154 1153 server configuration.
1155 1154
1156 1155 The optional ``path`` URL parameter controls content to include in the
1157 1156 archive. If omitted, every file in the specified revision is present in the
1158 1157 archive. If included, only the specified file or contents of the specified
1159 1158 directory will be included in the archive.
1160 1159
1161 1160 No template is used for this handler. Raw, binary content is generated.
1162 1161 """
1163 1162
1164 1163 type_ = web.req.qsparams.get('type')
1165 1164 allowed = web.configlist("web", "allow_archive")
1166 1165 key = web.req.qsparams['node']
1167 1166
1168 1167 if type_ not in webutil.archivespecs:
1169 1168 msg = 'Unsupported archive type: %s' % type_
1170 1169 raise ErrorResponse(HTTP_NOT_FOUND, msg)
1171 1170
1172 1171 if not ((type_ in allowed or
1173 1172 web.configbool("web", "allow" + type_))):
1174 1173 msg = 'Archive type not allowed: %s' % type_
1175 1174 raise ErrorResponse(HTTP_FORBIDDEN, msg)
1176 1175
1177 1176 reponame = re.sub(br"\W+", "-", os.path.basename(web.reponame))
1178 1177 cnode = web.repo.lookup(key)
1179 1178 arch_version = key
1180 1179 if cnode == key or key == 'tip':
1181 1180 arch_version = short(cnode)
1182 1181 name = "%s-%s" % (reponame, arch_version)
1183 1182
1184 1183 ctx = webutil.changectx(web.repo, web.req)
1185 1184 pats = []
1186 1185 match = scmutil.match(ctx, [])
1187 1186 file = web.req.qsparams.get('file')
1188 1187 if file:
1189 1188 pats = ['path:' + file]
1190 1189 match = scmutil.match(ctx, pats, default='path')
1191 1190 if pats:
1192 1191 files = [f for f in ctx.manifest().keys() if match(f)]
1193 1192 if not files:
1194 1193 raise ErrorResponse(HTTP_NOT_FOUND,
1195 1194 'file(s) not found: %s' % file)
1196 1195
1197 1196 mimetype, artype, extension, encoding = webutil.archivespecs[type_]
1198 1197
1199 1198 web.res.headers['Content-Type'] = mimetype
1200 1199 web.res.headers['Content-Disposition'] = 'attachment; filename=%s%s' % (
1201 1200 name, extension)
1202 1201
1203 1202 if encoding:
1204 1203 web.res.headers['Content-Encoding'] = encoding
1205 1204
1206 1205 web.res.setbodywillwrite()
1207 1206 if list(web.res.sendresponse()):
1208 1207 raise error.ProgrammingError('sendresponse() should not emit data '
1209 1208 'if writing later')
1210 1209
1211 1210 bodyfh = web.res.getbodyfile()
1212 1211
1213 1212 archival.archive(web.repo, bodyfh, cnode, artype, prefix=name,
1214 1213 matchfn=match,
1215 1214 subrepos=web.configbool("web", "archivesubrepos"))
1216 1215
1217 1216 return []
1218 1217
1219 1218 @webcommand('static')
1220 1219 def static(web):
1221 1220 fname = web.req.qsparams['file']
1222 1221 # a repo owner may set web.static in .hg/hgrc to get any file
1223 1222 # readable by the user running the CGI script
1224 1223 static = web.config("web", "static", None, untrusted=False)
1225 1224 if not static:
1226 1225 tp = web.templatepath or templater.templatepaths()
1227 1226 if isinstance(tp, str):
1228 1227 tp = [tp]
1229 1228 static = [os.path.join(p, 'static') for p in tp]
1230 1229
1231 1230 staticfile(static, fname, web.res)
1232 1231 return web.res.sendresponse()
1233 1232
1234 1233 @webcommand('graph')
1235 1234 def graph(web):
1236 1235 """
1237 1236 /graph[/{revision}]
1238 1237 -------------------
1239 1238
1240 1239 Show information about the graphical topology of the repository.
1241 1240
1242 1241 Information rendered by this handler can be used to create visual
1243 1242 representations of repository topology.
1244 1243
1245 1244 The ``revision`` URL parameter controls the starting changeset. If it's
1246 1245 absent, the default is ``tip``.
1247 1246
1248 1247 The ``revcount`` query string argument can define the number of changesets
1249 1248 to show information for.
1250 1249
1251 1250 The ``graphtop`` query string argument can specify the starting changeset
1252 1251 for producing ``jsdata`` variable that is used for rendering graph in
1253 1252 JavaScript. By default it has the same value as ``revision``.
1254 1253
1255 1254 This handler will render the ``graph`` template.
1256 1255 """
1257 1256
1258 1257 if 'node' in web.req.qsparams:
1259 1258 ctx = webutil.changectx(web.repo, web.req)
1260 1259 symrev = webutil.symrevorshortnode(web.req, ctx)
1261 1260 else:
1262 1261 ctx = web.repo['tip']
1263 1262 symrev = 'tip'
1264 1263 rev = ctx.rev()
1265 1264
1266 1265 bg_height = 39
1267 1266 revcount = web.maxshortchanges
1268 1267 if 'revcount' in web.req.qsparams:
1269 1268 try:
1270 1269 revcount = int(web.req.qsparams.get('revcount', revcount))
1271 1270 revcount = max(revcount, 1)
1272 1271 web.tmpl.defaults['sessionvars']['revcount'] = revcount
1273 1272 except ValueError:
1274 1273 pass
1275 1274
1276 1275 lessvars = copy.copy(web.tmpl.defaults['sessionvars'])
1277 1276 lessvars['revcount'] = max(revcount // 2, 1)
1278 1277 morevars = copy.copy(web.tmpl.defaults['sessionvars'])
1279 1278 morevars['revcount'] = revcount * 2
1280 1279
1281 1280 graphtop = web.req.qsparams.get('graphtop', ctx.hex())
1282 1281 graphvars = copy.copy(web.tmpl.defaults['sessionvars'])
1283 1282 graphvars['graphtop'] = graphtop
1284 1283
1285 1284 count = len(web.repo)
1286 1285 pos = rev
1287 1286
1288 1287 uprev = min(max(0, count - 1), rev + revcount)
1289 1288 downrev = max(0, rev - revcount)
1290 1289 changenav = webutil.revnav(web.repo).gen(pos, revcount, count)
1291 1290
1292 1291 tree = []
1293 1292 nextentry = []
1294 1293 lastrev = 0
1295 1294 if pos != -1:
1296 1295 allrevs = web.repo.changelog.revs(pos, 0)
1297 1296 revs = []
1298 1297 for i in allrevs:
1299 1298 revs.append(i)
1300 1299 if len(revs) >= revcount + 1:
1301 1300 break
1302 1301
1303 1302 if len(revs) > revcount:
1304 1303 nextentry = [webutil.commonentry(web.repo, web.repo[revs[-1]])]
1305 1304 revs = revs[:-1]
1306 1305
1307 1306 lastrev = revs[-1]
1308 1307
1309 1308 # We have to feed a baseset to dagwalker as it is expecting smartset
1310 1309 # object. This does not have a big impact on hgweb performance itself
1311 1310 # since hgweb graphing code is not itself lazy yet.
1312 1311 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1313 1312 # As we said one line above... not lazy.
1314 1313 tree = list(item for item in graphmod.colored(dag, web.repo)
1315 1314 if item[1] == graphmod.CHANGESET)
1316 1315
1317 1316 def fulltree():
1318 1317 pos = web.repo[graphtop].rev()
1319 1318 tree = []
1320 1319 if pos != -1:
1321 1320 revs = web.repo.changelog.revs(pos, lastrev)
1322 1321 dag = graphmod.dagwalker(web.repo, smartset.baseset(revs))
1323 1322 tree = list(item for item in graphmod.colored(dag, web.repo)
1324 1323 if item[1] == graphmod.CHANGESET)
1325 1324 return tree
1326 1325
1327 1326 def jsdata():
1328 1327 return [{'node': pycompat.bytestr(ctx),
1329 1328 'graphnode': webutil.getgraphnode(web.repo, ctx),
1330 1329 'vertex': vtx,
1331 1330 'edges': edges}
1332 1331 for (id, type, ctx, vtx, edges) in fulltree()]
1333 1332
1334 1333 def nodes():
1335 1334 parity = paritygen(web.stripecount)
1336 1335 for row, (id, type, ctx, vtx, edges) in enumerate(tree):
1337 1336 entry = webutil.commonentry(web.repo, ctx)
1338 1337 edgedata = [{'col': edge[0],
1339 1338 'nextcol': edge[1],
1340 1339 'color': (edge[2] - 1) % 6 + 1,
1341 1340 'width': edge[3],
1342 1341 'bcolor': edge[4]}
1343 1342 for edge in edges]
1344 1343
1345 1344 entry.update({'col': vtx[0],
1346 1345 'color': (vtx[1] - 1) % 6 + 1,
1347 1346 'parity': next(parity),
1348 1347 'edges': edgedata,
1349 1348 'row': row,
1350 1349 'nextrow': row + 1})
1351 1350
1352 1351 yield entry
1353 1352
1354 1353 rows = len(tree)
1355 1354
1356 1355 return web.sendtemplate(
1357 1356 'graph',
1358 1357 rev=rev,
1359 1358 symrev=symrev,
1360 1359 revcount=revcount,
1361 1360 uprev=uprev,
1362 1361 lessvars=lessvars,
1363 1362 morevars=morevars,
1364 1363 downrev=downrev,
1365 1364 graphvars=graphvars,
1366 1365 rows=rows,
1367 1366 bg_height=bg_height,
1368 1367 changesets=count,
1369 1368 nextentry=nextentry,
1370 1369 jsdata=lambda **x: jsdata(),
1371 1370 nodes=lambda **x: nodes(),
1372 1371 node=ctx.hex(),
1373 1372 changenav=changenav)
1374 1373
1375 1374 def _getdoc(e):
1376 1375 doc = e[0].__doc__
1377 1376 if doc:
1378 1377 doc = _(doc).partition('\n')[0]
1379 1378 else:
1380 1379 doc = _('(no help text available)')
1381 1380 return doc
1382 1381
1383 1382 @webcommand('help')
1384 1383 def help(web):
1385 1384 """
1386 1385 /help[/{topic}]
1387 1386 ---------------
1388 1387
1389 1388 Render help documentation.
1390 1389
1391 1390 This web command is roughly equivalent to :hg:`help`. If a ``topic``
1392 1391 is defined, that help topic will be rendered. If not, an index of
1393 1392 available help topics will be rendered.
1394 1393
1395 1394 The ``help`` template will be rendered when requesting help for a topic.
1396 1395 ``helptopics`` will be rendered for the index of help topics.
1397 1396 """
1398 1397 from .. import commands, help as helpmod # avoid cycle
1399 1398
1400 1399 topicname = web.req.qsparams.get('node')
1401 1400 if not topicname:
1402 1401 def topics(**map):
1403 1402 for entries, summary, _doc in helpmod.helptable:
1404 1403 yield {'topic': entries[0], 'summary': summary}
1405 1404
1406 1405 early, other = [], []
1407 1406 primary = lambda s: s.partition('|')[0]
1408 1407 for c, e in commands.table.iteritems():
1409 1408 doc = _getdoc(e)
1410 1409 if 'DEPRECATED' in doc or c.startswith('debug'):
1411 1410 continue
1412 1411 cmd = primary(c)
1413 1412 if cmd.startswith('^'):
1414 1413 early.append((cmd[1:], doc))
1415 1414 else:
1416 1415 other.append((cmd, doc))
1417 1416
1418 1417 early.sort()
1419 1418 other.sort()
1420 1419
1421 1420 def earlycommands(**map):
1422 1421 for c, doc in early:
1423 1422 yield {'topic': c, 'summary': doc}
1424 1423
1425 1424 def othercommands(**map):
1426 1425 for c, doc in other:
1427 1426 yield {'topic': c, 'summary': doc}
1428 1427
1429 1428 return web.sendtemplate(
1430 1429 'helptopics',
1431 1430 topics=topics,
1432 1431 earlycommands=earlycommands,
1433 1432 othercommands=othercommands,
1434 1433 title='Index')
1435 1434
1436 1435 # Render an index of sub-topics.
1437 1436 if topicname in helpmod.subtopics:
1438 1437 topics = []
1439 1438 for entries, summary, _doc in helpmod.subtopics[topicname]:
1440 1439 topics.append({
1441 1440 'topic': '%s.%s' % (topicname, entries[0]),
1442 1441 'basename': entries[0],
1443 1442 'summary': summary,
1444 1443 })
1445 1444
1446 1445 return web.sendtemplate(
1447 1446 'helptopics',
1448 1447 topics=topics,
1449 1448 title=topicname,
1450 1449 subindex=True)
1451 1450
1452 1451 u = webutil.wsgiui.load()
1453 1452 u.verbose = True
1454 1453
1455 1454 # Render a page from a sub-topic.
1456 1455 if '.' in topicname:
1457 1456 # TODO implement support for rendering sections, like
1458 1457 # `hg help` works.
1459 1458 topic, subtopic = topicname.split('.', 1)
1460 1459 if topic not in helpmod.subtopics:
1461 1460 raise ErrorResponse(HTTP_NOT_FOUND)
1462 1461 else:
1463 1462 topic = topicname
1464 1463 subtopic = None
1465 1464
1466 1465 try:
1467 1466 doc = helpmod.help_(u, commands, topic, subtopic=subtopic)
1468 1467 except error.Abort:
1469 1468 raise ErrorResponse(HTTP_NOT_FOUND)
1470 1469
1471 1470 return web.sendtemplate(
1472 1471 'help',
1473 1472 topic=topicname,
1474 1473 doc=doc)
1475 1474
1476 1475 # tell hggettext to extract docstrings from these functions:
1477 1476 i18nfunctions = commands.values()
@@ -1,748 +1,748 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 files = listfilediffs(web.tmpl, ctx.files(), n, web.maxfiles)
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 def listfilediffs(tmpl, files, node, max):
518 def listfilediffs(files, node, max):
519 519 return templateutil.mappedgenerator(_listfilediffsgen,
520 520 args=(files, node, max))
521 521
522 522 def diffs(web, ctx, basectx, files, style, linerange=None,
523 523 lineidprefix=''):
524 524
525 525 def prettyprintlines(lines, blockno):
526 526 for lineno, l in enumerate(lines, 1):
527 527 difflineno = "%d.%d" % (blockno, lineno)
528 528 if l.startswith('+'):
529 529 ltype = "difflineplus"
530 530 elif l.startswith('-'):
531 531 ltype = "difflineminus"
532 532 elif l.startswith('@'):
533 533 ltype = "difflineat"
534 534 else:
535 535 ltype = "diffline"
536 536 yield web.tmpl.generate(ltype, {
537 537 'line': l,
538 538 'lineno': lineno,
539 539 'lineid': lineidprefix + "l%s" % difflineno,
540 540 'linenumber': "% 8s" % difflineno,
541 541 })
542 542
543 543 repo = web.repo
544 544 if files:
545 545 m = match.exact(repo.root, repo.getcwd(), files)
546 546 else:
547 547 m = match.always(repo.root, repo.getcwd())
548 548
549 549 diffopts = patch.diffopts(repo.ui, untrusted=True)
550 550 node1 = basectx.node()
551 551 node2 = ctx.node()
552 552 parity = paritygen(web.stripecount)
553 553
554 554 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
555 555 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
556 556 if style != 'raw':
557 557 header = header[1:]
558 558 lines = [h + '\n' for h in header]
559 559 for hunkrange, hunklines in hunks:
560 560 if linerange is not None and hunkrange is not None:
561 561 s1, l1, s2, l2 = hunkrange
562 562 if not mdiff.hunkinrange((s2, l2), linerange):
563 563 continue
564 564 lines.extend(hunklines)
565 565 if lines:
566 566 yield web.tmpl.generate('diffblock', {
567 567 'parity': next(parity),
568 568 'blockno': blockno,
569 569 'lines': prettyprintlines(lines, blockno),
570 570 })
571 571
572 572 def compare(tmpl, context, leftlines, rightlines):
573 573 '''Generator function that provides side-by-side comparison data.'''
574 574
575 575 def compline(type, leftlineno, leftline, rightlineno, rightline):
576 576 lineid = leftlineno and ("l%d" % leftlineno) or ''
577 577 lineid += rightlineno and ("r%d" % rightlineno) or ''
578 578 llno = '%d' % leftlineno if leftlineno else ''
579 579 rlno = '%d' % rightlineno if rightlineno else ''
580 580 return tmpl.generate('comparisonline', {
581 581 'type': type,
582 582 'lineid': lineid,
583 583 'leftlineno': leftlineno,
584 584 'leftlinenumber': "% 6s" % llno,
585 585 'leftline': leftline or '',
586 586 'rightlineno': rightlineno,
587 587 'rightlinenumber': "% 6s" % rlno,
588 588 'rightline': rightline or '',
589 589 })
590 590
591 591 def getblock(opcodes):
592 592 for type, llo, lhi, rlo, rhi in opcodes:
593 593 len1 = lhi - llo
594 594 len2 = rhi - rlo
595 595 count = min(len1, len2)
596 596 for i in xrange(count):
597 597 yield compline(type=type,
598 598 leftlineno=llo + i + 1,
599 599 leftline=leftlines[llo + i],
600 600 rightlineno=rlo + i + 1,
601 601 rightline=rightlines[rlo + i])
602 602 if len1 > len2:
603 603 for i in xrange(llo + count, lhi):
604 604 yield compline(type=type,
605 605 leftlineno=i + 1,
606 606 leftline=leftlines[i],
607 607 rightlineno=None,
608 608 rightline=None)
609 609 elif len2 > len1:
610 610 for i in xrange(rlo + count, rhi):
611 611 yield compline(type=type,
612 612 leftlineno=None,
613 613 leftline=None,
614 614 rightlineno=i + 1,
615 615 rightline=rightlines[i])
616 616
617 617 s = difflib.SequenceMatcher(None, leftlines, rightlines)
618 618 if context < 0:
619 619 yield tmpl.generate('comparisonblock',
620 620 {'lines': getblock(s.get_opcodes())})
621 621 else:
622 622 for oc in s.get_grouped_opcodes(n=context):
623 623 yield tmpl.generate('comparisonblock', {'lines': getblock(oc)})
624 624
625 625 def diffstatgen(ctx, basectx):
626 626 '''Generator function that provides the diffstat data.'''
627 627
628 628 stats = patch.diffstatdata(
629 629 util.iterlines(ctx.diff(basectx, noprefix=False)))
630 630 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
631 631 while True:
632 632 yield stats, maxname, maxtotal, addtotal, removetotal, binary
633 633
634 634 def diffsummary(statgen):
635 635 '''Return a short summary of the diff.'''
636 636
637 637 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
638 638 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
639 639 len(stats), addtotal, removetotal)
640 640
641 641 def diffstat(tmpl, ctx, statgen, parity):
642 642 '''Return a diffstat template for each file in the diff.'''
643 643
644 644 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
645 645 files = ctx.files()
646 646
647 647 def pct(i):
648 648 if maxtotal == 0:
649 649 return 0
650 650 return (float(i) / maxtotal) * 100
651 651
652 652 fileno = 0
653 653 for filename, adds, removes, isbinary in stats:
654 654 template = 'diffstatlink' if filename in files else 'diffstatnolink'
655 655 total = adds + removes
656 656 fileno += 1
657 657 yield tmpl.generate(template, {
658 658 'node': ctx.hex(),
659 659 'file': filename,
660 660 'fileno': fileno,
661 661 'total': total,
662 662 'addpct': pct(adds),
663 663 'removepct': pct(removes),
664 664 'parity': next(parity),
665 665 })
666 666
667 667 class sessionvars(templateutil.wrapped):
668 668 def __init__(self, vars, start='?'):
669 669 self._start = start
670 670 self._vars = vars
671 671
672 672 def __getitem__(self, key):
673 673 return self._vars[key]
674 674
675 675 def __setitem__(self, key, value):
676 676 self._vars[key] = value
677 677
678 678 def __copy__(self):
679 679 return sessionvars(copy.copy(self._vars), self._start)
680 680
681 681 def itermaps(self, context):
682 682 separator = self._start
683 683 for key, value in sorted(self._vars.iteritems()):
684 684 yield {'name': key,
685 685 'value': pycompat.bytestr(value),
686 686 'separator': separator,
687 687 }
688 688 separator = '&'
689 689
690 690 def join(self, context, mapping, sep):
691 691 # could be '{separator}{name}={value|urlescape}'
692 692 raise error.ParseError(_('not displayable without template'))
693 693
694 694 def show(self, context, mapping):
695 695 return self.join(context, '')
696 696
697 697 def tovalue(self, context, mapping):
698 698 return self._vars
699 699
700 700 class wsgiui(uimod.ui):
701 701 # default termwidth breaks under mod_wsgi
702 702 def termwidth(self):
703 703 return 80
704 704
705 705 def getwebsubs(repo):
706 706 websubtable = []
707 707 websubdefs = repo.ui.configitems('websub')
708 708 # we must maintain interhg backwards compatibility
709 709 websubdefs += repo.ui.configitems('interhg')
710 710 for key, pattern in websubdefs:
711 711 # grab the delimiter from the character after the "s"
712 712 unesc = pattern[1:2]
713 713 delim = re.escape(unesc)
714 714
715 715 # identify portions of the pattern, taking care to avoid escaped
716 716 # delimiters. the replace format and flags are optional, but
717 717 # delimiters are required.
718 718 match = re.match(
719 719 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
720 720 % (delim, delim, delim), pattern)
721 721 if not match:
722 722 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
723 723 % (key, pattern))
724 724 continue
725 725
726 726 # we need to unescape the delimiter for regexp and format
727 727 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
728 728 regexp = delim_re.sub(unesc, match.group(1))
729 729 format = delim_re.sub(unesc, match.group(2))
730 730
731 731 # the pattern allows for 6 regexp flags, so set them if necessary
732 732 flagin = match.group(3)
733 733 flags = 0
734 734 if flagin:
735 735 for flag in flagin.upper():
736 736 flags |= re.__dict__[flag]
737 737
738 738 try:
739 739 regexp = re.compile(regexp, flags)
740 740 websubtable.append((regexp, format))
741 741 except re.error:
742 742 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
743 743 % (key, regexp))
744 744 return websubtable
745 745
746 746 def getgraphnode(repo, ctx):
747 747 return (templatekw.getgraphnodecurrent(repo, ctx) +
748 748 templatekw.getgraphnodesymbol(ctx))
General Comments 0
You need to be logged in to leave comments. Login now