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