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