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