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