##// END OF EJS Templates
hgweb: make revnav.gen() simply build a list of mappings by one pass...
Yuya Nishihara -
r37715:6fb50e91 default
parent child Browse files
Show More
@@ -1,731 +1,729 b''
1 1 # hgweb/webutil.py - utility library for the web interface.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import copy
12 12 import difflib
13 13 import os
14 14 import re
15 15
16 16 from ..i18n import _
17 17 from ..node import hex, nullid, short
18 18
19 19 from .common import (
20 20 ErrorResponse,
21 21 HTTP_BAD_REQUEST,
22 22 HTTP_NOT_FOUND,
23 23 paritygen,
24 24 )
25 25
26 26 from .. import (
27 27 context,
28 28 error,
29 29 match,
30 30 mdiff,
31 31 obsutil,
32 32 patch,
33 33 pathutil,
34 34 pycompat,
35 35 scmutil,
36 36 templatefilters,
37 37 templatekw,
38 38 templateutil,
39 39 ui as uimod,
40 40 util,
41 41 )
42 42
43 43 from ..utils import (
44 44 stringutil,
45 45 )
46 46
47 47 archivespecs = util.sortdict((
48 48 ('zip', ('application/zip', 'zip', '.zip', None)),
49 49 ('gz', ('application/x-gzip', 'tgz', '.tar.gz', None)),
50 50 ('bz2', ('application/x-bzip2', 'tbz2', '.tar.bz2', None)),
51 51 ))
52 52
53 53 def archivelist(ui, nodeid, url=None):
54 54 allowed = ui.configlist('web', 'allow_archive', untrusted=True)
55 55 archives = []
56 56
57 57 for typ, spec in archivespecs.iteritems():
58 58 if typ in allowed or ui.configbool('web', 'allow' + typ,
59 59 untrusted=True):
60 60 archives.append({
61 61 'type': typ,
62 62 'extension': spec[2],
63 63 'node': nodeid,
64 64 'url': url,
65 65 })
66 66
67 67 return templateutil.mappinglist(archives)
68 68
69 69 def up(p):
70 70 if p[0:1] != "/":
71 71 p = "/" + p
72 72 if p[-1:] == "/":
73 73 p = p[:-1]
74 74 up = os.path.dirname(p)
75 75 if up == "/":
76 76 return "/"
77 77 return up + "/"
78 78
79 79 def _navseq(step, firststep=None):
80 80 if firststep:
81 81 yield firststep
82 82 if firststep >= 20 and firststep <= 40:
83 83 firststep = 50
84 84 yield firststep
85 85 assert step > 0
86 86 assert firststep > 0
87 87 while step <= firststep:
88 88 step *= 10
89 89 while True:
90 90 yield 1 * step
91 91 yield 3 * step
92 92 step *= 10
93 93
94 94 class revnav(object):
95 95
96 96 def __init__(self, repo):
97 97 """Navigation generation object
98 98
99 99 :repo: repo object we generate nav for
100 100 """
101 101 # used for hex generation
102 102 self._revlog = repo.changelog
103 103
104 104 def __nonzero__(self):
105 105 """return True if any revision to navigate over"""
106 106 return self._first() is not None
107 107
108 108 __bool__ = __nonzero__
109 109
110 110 def _first(self):
111 111 """return the minimum non-filtered changeset or None"""
112 112 try:
113 113 return next(iter(self._revlog))
114 114 except StopIteration:
115 115 return None
116 116
117 117 def hex(self, rev):
118 118 return hex(self._revlog.node(rev))
119 119
120 120 def gen(self, pos, pagelen, limit):
121 121 """computes label and revision id for navigation link
122 122
123 123 :pos: is the revision relative to which we generate navigation.
124 124 :pagelen: the size of each navigation page
125 125 :limit: how far shall we link
126 126
127 127 The return is:
128 128 - a single element tuple
129 129 - containing a dictionary with a `before` and `after` key
130 - values are generator functions taking arbitrary number of kwargs
131 - yield items are dictionaries with `label` and `node` keys
130 - values are dictionaries with `label` and `node` keys
132 131 """
133 132 if not self:
134 133 # empty repo
135 134 return ({'before': (), 'after': ()},)
136 135
137 136 targets = []
138 137 for f in _navseq(1, pagelen):
139 138 if f > limit:
140 139 break
141 140 targets.append(pos + f)
142 141 targets.append(pos - f)
143 142 targets.sort()
144 143
145 144 first = self._first()
146 navbefore = [("(%i)" % first, self.hex(first))]
145 navbefore = [{'label': '(%i)' % first, 'node': self.hex(first)}]
147 146 navafter = []
148 147 for rev in targets:
149 148 if rev not in self._revlog:
150 149 continue
151 150 if pos < rev < limit:
152 navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
151 navafter.append({'label': '+%d' % abs(rev - pos),
152 'node': self.hex(rev)})
153 153 if 0 < rev < pos:
154 navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
155
154 navbefore.append({'label': '-%d' % abs(rev - pos),
155 'node': self.hex(rev)})
156 156
157 navafter.append(("tip", "tip"))
157 navafter.append({'label': 'tip', 'node': 'tip'})
158 158
159 data = lambda i: {"label": i[0], "node": i[1]}
160 return ({'before': lambda **map: (data(i) for i in navbefore),
161 'after': lambda **map: (data(i) for i in navafter)},)
159 return ({'before': navbefore, 'after': navafter},)
162 160
163 161 class filerevnav(revnav):
164 162
165 163 def __init__(self, repo, path):
166 164 """Navigation generation object
167 165
168 166 :repo: repo object we generate nav for
169 167 :path: path of the file we generate nav for
170 168 """
171 169 # used for iteration
172 170 self._changelog = repo.unfiltered().changelog
173 171 # used for hex generation
174 172 self._revlog = repo.file(path)
175 173
176 174 def hex(self, rev):
177 175 return hex(self._changelog.node(self._revlog.linkrev(rev)))
178 176
179 177 class _siblings(object):
180 178 def __init__(self, siblings=None, hiderev=None):
181 179 if siblings is None:
182 180 siblings = []
183 181 self.siblings = [s for s in siblings if s.node() != nullid]
184 182 if len(self.siblings) == 1 and self.siblings[0].rev() == hiderev:
185 183 self.siblings = []
186 184
187 185 def __iter__(self):
188 186 for s in self.siblings:
189 187 d = {
190 188 'node': s.hex(),
191 189 'rev': s.rev(),
192 190 'user': s.user(),
193 191 'date': s.date(),
194 192 'description': s.description(),
195 193 'branch': s.branch(),
196 194 }
197 195 if util.safehasattr(s, 'path'):
198 196 d['file'] = s.path()
199 197 yield d
200 198
201 199 def __len__(self):
202 200 return len(self.siblings)
203 201
204 202 def difffeatureopts(req, ui, section):
205 203 diffopts = patch.difffeatureopts(ui, untrusted=True,
206 204 section=section, whitespace=True)
207 205
208 206 for k in ('ignorews', 'ignorewsamount', 'ignorewseol', 'ignoreblanklines'):
209 207 v = req.qsparams.get(k)
210 208 if v is not None:
211 209 v = stringutil.parsebool(v)
212 210 setattr(diffopts, k, v if v is not None else True)
213 211
214 212 return diffopts
215 213
216 214 def annotate(req, fctx, ui):
217 215 diffopts = difffeatureopts(req, ui, 'annotate')
218 216 return fctx.annotate(follow=True, diffopts=diffopts)
219 217
220 218 def parents(ctx, hide=None):
221 219 if isinstance(ctx, context.basefilectx):
222 220 introrev = ctx.introrev()
223 221 if ctx.changectx().rev() != introrev:
224 222 return _siblings([ctx.repo()[introrev]], hide)
225 223 return _siblings(ctx.parents(), hide)
226 224
227 225 def children(ctx, hide=None):
228 226 return _siblings(ctx.children(), hide)
229 227
230 228 def renamelink(fctx):
231 229 r = fctx.renamed()
232 230 if r:
233 231 return [{'file': r[0], 'node': hex(r[1])}]
234 232 return []
235 233
236 234 def nodetagsdict(repo, node):
237 235 return [{"name": i} for i in repo.nodetags(node)]
238 236
239 237 def nodebookmarksdict(repo, node):
240 238 return [{"name": i} for i in repo.nodebookmarks(node)]
241 239
242 240 def nodebranchdict(repo, ctx):
243 241 branches = []
244 242 branch = ctx.branch()
245 243 # If this is an empty repo, ctx.node() == nullid,
246 244 # ctx.branch() == 'default'.
247 245 try:
248 246 branchnode = repo.branchtip(branch)
249 247 except error.RepoLookupError:
250 248 branchnode = None
251 249 if branchnode == ctx.node():
252 250 branches.append({"name": branch})
253 251 return branches
254 252
255 253 def nodeinbranch(repo, ctx):
256 254 branches = []
257 255 branch = ctx.branch()
258 256 try:
259 257 branchnode = repo.branchtip(branch)
260 258 except error.RepoLookupError:
261 259 branchnode = None
262 260 if branch != 'default' and branchnode != ctx.node():
263 261 branches.append({"name": branch})
264 262 return branches
265 263
266 264 def nodebranchnodefault(ctx):
267 265 branches = []
268 266 branch = ctx.branch()
269 267 if branch != 'default':
270 268 branches.append({"name": branch})
271 269 return branches
272 270
273 271 def showtag(repo, tmpl, t1, node=nullid, **args):
274 272 args = pycompat.byteskwargs(args)
275 273 for t in repo.nodetags(node):
276 274 lm = args.copy()
277 275 lm['tag'] = t
278 276 yield tmpl.generate(t1, lm)
279 277
280 278 def showbookmark(repo, tmpl, t1, node=nullid, **args):
281 279 args = pycompat.byteskwargs(args)
282 280 for t in repo.nodebookmarks(node):
283 281 lm = args.copy()
284 282 lm['bookmark'] = t
285 283 yield tmpl.generate(t1, lm)
286 284
287 285 def branchentries(repo, stripecount, limit=0):
288 286 tips = []
289 287 heads = repo.heads()
290 288 parity = paritygen(stripecount)
291 289 sortkey = lambda item: (not item[1], item[0].rev())
292 290
293 291 def entries(**map):
294 292 count = 0
295 293 if not tips:
296 294 for tag, hs, tip, closed in repo.branchmap().iterbranches():
297 295 tips.append((repo[tip], closed))
298 296 for ctx, closed in sorted(tips, key=sortkey, reverse=True):
299 297 if limit > 0 and count >= limit:
300 298 return
301 299 count += 1
302 300 if closed:
303 301 status = 'closed'
304 302 elif ctx.node() not in heads:
305 303 status = 'inactive'
306 304 else:
307 305 status = 'open'
308 306 yield {
309 307 'parity': next(parity),
310 308 'branch': ctx.branch(),
311 309 'status': status,
312 310 'node': ctx.hex(),
313 311 'date': ctx.date()
314 312 }
315 313
316 314 return entries
317 315
318 316 def cleanpath(repo, path):
319 317 path = path.lstrip('/')
320 318 return pathutil.canonpath(repo.root, '', path)
321 319
322 320 def changectx(repo, req):
323 321 changeid = "tip"
324 322 if 'node' in req.qsparams:
325 323 changeid = req.qsparams['node']
326 324 ipos = changeid.find(':')
327 325 if ipos != -1:
328 326 changeid = changeid[(ipos + 1):]
329 327
330 328 return scmutil.revsymbol(repo, changeid)
331 329
332 330 def basechangectx(repo, req):
333 331 if 'node' in req.qsparams:
334 332 changeid = req.qsparams['node']
335 333 ipos = changeid.find(':')
336 334 if ipos != -1:
337 335 changeid = changeid[:ipos]
338 336 return scmutil.revsymbol(repo, changeid)
339 337
340 338 return None
341 339
342 340 def filectx(repo, req):
343 341 if 'file' not in req.qsparams:
344 342 raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
345 343 path = cleanpath(repo, req.qsparams['file'])
346 344 if 'node' in req.qsparams:
347 345 changeid = req.qsparams['node']
348 346 elif 'filenode' in req.qsparams:
349 347 changeid = req.qsparams['filenode']
350 348 else:
351 349 raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
352 350 try:
353 351 fctx = scmutil.revsymbol(repo, changeid)[path]
354 352 except error.RepoError:
355 353 fctx = repo.filectx(path, fileid=changeid)
356 354
357 355 return fctx
358 356
359 357 def linerange(req):
360 358 linerange = req.qsparams.getall('linerange')
361 359 if not linerange:
362 360 return None
363 361 if len(linerange) > 1:
364 362 raise ErrorResponse(HTTP_BAD_REQUEST,
365 363 'redundant linerange parameter')
366 364 try:
367 365 fromline, toline = map(int, linerange[0].split(':', 1))
368 366 except ValueError:
369 367 raise ErrorResponse(HTTP_BAD_REQUEST,
370 368 'invalid linerange parameter')
371 369 try:
372 370 return util.processlinerange(fromline, toline)
373 371 except error.ParseError as exc:
374 372 raise ErrorResponse(HTTP_BAD_REQUEST, pycompat.bytestr(exc))
375 373
376 374 def formatlinerange(fromline, toline):
377 375 return '%d:%d' % (fromline + 1, toline)
378 376
379 377 def succsandmarkers(context, mapping):
380 378 repo = context.resource(mapping, 'repo')
381 379 itemmappings = templatekw.showsuccsandmarkers(context, mapping)
382 380 for item in itemmappings.tovalue(context, mapping):
383 381 item['successors'] = _siblings(repo[successor]
384 382 for successor in item['successors'])
385 383 yield item
386 384
387 385 # teach templater succsandmarkers is switched to (context, mapping) API
388 386 succsandmarkers._requires = {'repo', 'ctx'}
389 387
390 388 def whyunstable(context, mapping):
391 389 repo = context.resource(mapping, 'repo')
392 390 ctx = context.resource(mapping, 'ctx')
393 391
394 392 entries = obsutil.whyunstable(repo, ctx)
395 393 for entry in entries:
396 394 if entry.get('divergentnodes'):
397 395 entry['divergentnodes'] = _siblings(entry['divergentnodes'])
398 396 yield entry
399 397
400 398 whyunstable._requires = {'repo', 'ctx'}
401 399
402 400 def commonentry(repo, ctx):
403 401 node = ctx.node()
404 402 return {
405 403 # TODO: perhaps ctx.changectx() should be assigned if ctx is a
406 404 # filectx, but I'm not pretty sure if that would always work because
407 405 # fctx.parents() != fctx.changectx.parents() for example.
408 406 'ctx': ctx,
409 407 'rev': ctx.rev(),
410 408 'node': hex(node),
411 409 'author': ctx.user(),
412 410 'desc': ctx.description(),
413 411 'date': ctx.date(),
414 412 'extra': ctx.extra(),
415 413 'phase': ctx.phasestr(),
416 414 'obsolete': ctx.obsolete(),
417 415 'succsandmarkers': succsandmarkers,
418 416 'instabilities': [{"instability": i} for i in ctx.instabilities()],
419 417 'whyunstable': whyunstable,
420 418 'branch': nodebranchnodefault(ctx),
421 419 'inbranch': nodeinbranch(repo, ctx),
422 420 'branches': nodebranchdict(repo, ctx),
423 421 'tags': nodetagsdict(repo, node),
424 422 'bookmarks': nodebookmarksdict(repo, node),
425 423 'parent': lambda **x: parents(ctx),
426 424 'child': lambda **x: children(ctx),
427 425 }
428 426
429 427 def changelistentry(web, ctx):
430 428 '''Obtain a dictionary to be used for entries in a changelist.
431 429
432 430 This function is called when producing items for the "entries" list passed
433 431 to the "shortlog" and "changelog" templates.
434 432 '''
435 433 repo = web.repo
436 434 rev = ctx.rev()
437 435 n = ctx.node()
438 436 showtags = showtag(repo, web.tmpl, 'changelogtag', n)
439 437 files = listfilediffs(web.tmpl, ctx.files(), n, web.maxfiles)
440 438
441 439 entry = commonentry(repo, ctx)
442 440 entry.update(
443 441 allparents=lambda **x: parents(ctx),
444 442 parent=lambda **x: parents(ctx, rev - 1),
445 443 child=lambda **x: children(ctx, rev + 1),
446 444 changelogtag=showtags,
447 445 files=files,
448 446 )
449 447 return entry
450 448
451 449 def symrevorshortnode(req, ctx):
452 450 if 'node' in req.qsparams:
453 451 return templatefilters.revescape(req.qsparams['node'])
454 452 else:
455 453 return short(ctx.node())
456 454
457 455 def changesetentry(web, ctx):
458 456 '''Obtain a dictionary to be used to render the "changeset" template.'''
459 457
460 458 showtags = showtag(web.repo, web.tmpl, 'changesettag', ctx.node())
461 459 showbookmarks = showbookmark(web.repo, web.tmpl, 'changesetbookmark',
462 460 ctx.node())
463 461 showbranch = nodebranchnodefault(ctx)
464 462
465 463 files = []
466 464 parity = paritygen(web.stripecount)
467 465 for blockno, f in enumerate(ctx.files()):
468 466 template = 'filenodelink' if f in ctx else 'filenolink'
469 467 files.append(web.tmpl.generate(template, {
470 468 'node': ctx.hex(),
471 469 'file': f,
472 470 'blockno': blockno + 1,
473 471 'parity': next(parity),
474 472 }))
475 473
476 474 basectx = basechangectx(web.repo, web.req)
477 475 if basectx is None:
478 476 basectx = ctx.p1()
479 477
480 478 style = web.config('web', 'style')
481 479 if 'style' in web.req.qsparams:
482 480 style = web.req.qsparams['style']
483 481
484 482 diff = diffs(web, ctx, basectx, None, style)
485 483
486 484 parity = paritygen(web.stripecount)
487 485 diffstatsgen = diffstatgen(ctx, basectx)
488 486 diffstats = diffstat(web.tmpl, ctx, diffstatsgen, parity)
489 487
490 488 return dict(
491 489 diff=diff,
492 490 symrev=symrevorshortnode(web.req, ctx),
493 491 basenode=basectx.hex(),
494 492 changesettag=showtags,
495 493 changesetbookmark=showbookmarks,
496 494 changesetbranch=showbranch,
497 495 files=files,
498 496 diffsummary=lambda **x: diffsummary(diffstatsgen),
499 497 diffstat=diffstats,
500 498 archives=web.archivelist(ctx.hex()),
501 499 **pycompat.strkwargs(commonentry(web.repo, ctx)))
502 500
503 501 def listfilediffs(tmpl, files, node, max):
504 502 for f in files[:max]:
505 503 yield tmpl.generate('filedifflink', {'node': hex(node), 'file': f})
506 504 if len(files) > max:
507 505 yield tmpl.generate('fileellipses', {})
508 506
509 507 def diffs(web, ctx, basectx, files, style, linerange=None,
510 508 lineidprefix=''):
511 509
512 510 def prettyprintlines(lines, blockno):
513 511 for lineno, l in enumerate(lines, 1):
514 512 difflineno = "%d.%d" % (blockno, lineno)
515 513 if l.startswith('+'):
516 514 ltype = "difflineplus"
517 515 elif l.startswith('-'):
518 516 ltype = "difflineminus"
519 517 elif l.startswith('@'):
520 518 ltype = "difflineat"
521 519 else:
522 520 ltype = "diffline"
523 521 yield web.tmpl.generate(ltype, {
524 522 'line': l,
525 523 'lineno': lineno,
526 524 'lineid': lineidprefix + "l%s" % difflineno,
527 525 'linenumber': "% 8s" % difflineno,
528 526 })
529 527
530 528 repo = web.repo
531 529 if files:
532 530 m = match.exact(repo.root, repo.getcwd(), files)
533 531 else:
534 532 m = match.always(repo.root, repo.getcwd())
535 533
536 534 diffopts = patch.diffopts(repo.ui, untrusted=True)
537 535 node1 = basectx.node()
538 536 node2 = ctx.node()
539 537 parity = paritygen(web.stripecount)
540 538
541 539 diffhunks = patch.diffhunks(repo, node1, node2, m, opts=diffopts)
542 540 for blockno, (fctx1, fctx2, header, hunks) in enumerate(diffhunks, 1):
543 541 if style != 'raw':
544 542 header = header[1:]
545 543 lines = [h + '\n' for h in header]
546 544 for hunkrange, hunklines in hunks:
547 545 if linerange is not None and hunkrange is not None:
548 546 s1, l1, s2, l2 = hunkrange
549 547 if not mdiff.hunkinrange((s2, l2), linerange):
550 548 continue
551 549 lines.extend(hunklines)
552 550 if lines:
553 551 yield web.tmpl.generate('diffblock', {
554 552 'parity': next(parity),
555 553 'blockno': blockno,
556 554 'lines': prettyprintlines(lines, blockno),
557 555 })
558 556
559 557 def compare(tmpl, context, leftlines, rightlines):
560 558 '''Generator function that provides side-by-side comparison data.'''
561 559
562 560 def compline(type, leftlineno, leftline, rightlineno, rightline):
563 561 lineid = leftlineno and ("l%d" % leftlineno) or ''
564 562 lineid += rightlineno and ("r%d" % rightlineno) or ''
565 563 llno = '%d' % leftlineno if leftlineno else ''
566 564 rlno = '%d' % rightlineno if rightlineno else ''
567 565 return tmpl.generate('comparisonline', {
568 566 'type': type,
569 567 'lineid': lineid,
570 568 'leftlineno': leftlineno,
571 569 'leftlinenumber': "% 6s" % llno,
572 570 'leftline': leftline or '',
573 571 'rightlineno': rightlineno,
574 572 'rightlinenumber': "% 6s" % rlno,
575 573 'rightline': rightline or '',
576 574 })
577 575
578 576 def getblock(opcodes):
579 577 for type, llo, lhi, rlo, rhi in opcodes:
580 578 len1 = lhi - llo
581 579 len2 = rhi - rlo
582 580 count = min(len1, len2)
583 581 for i in xrange(count):
584 582 yield compline(type=type,
585 583 leftlineno=llo + i + 1,
586 584 leftline=leftlines[llo + i],
587 585 rightlineno=rlo + i + 1,
588 586 rightline=rightlines[rlo + i])
589 587 if len1 > len2:
590 588 for i in xrange(llo + count, lhi):
591 589 yield compline(type=type,
592 590 leftlineno=i + 1,
593 591 leftline=leftlines[i],
594 592 rightlineno=None,
595 593 rightline=None)
596 594 elif len2 > len1:
597 595 for i in xrange(rlo + count, rhi):
598 596 yield compline(type=type,
599 597 leftlineno=None,
600 598 leftline=None,
601 599 rightlineno=i + 1,
602 600 rightline=rightlines[i])
603 601
604 602 s = difflib.SequenceMatcher(None, leftlines, rightlines)
605 603 if context < 0:
606 604 yield tmpl.generate('comparisonblock',
607 605 {'lines': getblock(s.get_opcodes())})
608 606 else:
609 607 for oc in s.get_grouped_opcodes(n=context):
610 608 yield tmpl.generate('comparisonblock', {'lines': getblock(oc)})
611 609
612 610 def diffstatgen(ctx, basectx):
613 611 '''Generator function that provides the diffstat data.'''
614 612
615 613 stats = patch.diffstatdata(
616 614 util.iterlines(ctx.diff(basectx, noprefix=False)))
617 615 maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
618 616 while True:
619 617 yield stats, maxname, maxtotal, addtotal, removetotal, binary
620 618
621 619 def diffsummary(statgen):
622 620 '''Return a short summary of the diff.'''
623 621
624 622 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
625 623 return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
626 624 len(stats), addtotal, removetotal)
627 625
628 626 def diffstat(tmpl, ctx, statgen, parity):
629 627 '''Return a diffstat template for each file in the diff.'''
630 628
631 629 stats, maxname, maxtotal, addtotal, removetotal, binary = next(statgen)
632 630 files = ctx.files()
633 631
634 632 def pct(i):
635 633 if maxtotal == 0:
636 634 return 0
637 635 return (float(i) / maxtotal) * 100
638 636
639 637 fileno = 0
640 638 for filename, adds, removes, isbinary in stats:
641 639 template = 'diffstatlink' if filename in files else 'diffstatnolink'
642 640 total = adds + removes
643 641 fileno += 1
644 642 yield tmpl.generate(template, {
645 643 'node': ctx.hex(),
646 644 'file': filename,
647 645 'fileno': fileno,
648 646 'total': total,
649 647 'addpct': pct(adds),
650 648 'removepct': pct(removes),
651 649 'parity': next(parity),
652 650 })
653 651
654 652 class sessionvars(templateutil.wrapped):
655 653 def __init__(self, vars, start='?'):
656 654 self._start = start
657 655 self._vars = vars
658 656
659 657 def __getitem__(self, key):
660 658 return self._vars[key]
661 659
662 660 def __setitem__(self, key, value):
663 661 self._vars[key] = value
664 662
665 663 def __copy__(self):
666 664 return sessionvars(copy.copy(self._vars), self._start)
667 665
668 666 def itermaps(self, context):
669 667 separator = self._start
670 668 for key, value in sorted(self._vars.iteritems()):
671 669 yield {'name': key,
672 670 'value': pycompat.bytestr(value),
673 671 'separator': separator,
674 672 }
675 673 separator = '&'
676 674
677 675 def join(self, context, mapping, sep):
678 676 # could be '{separator}{name}={value|urlescape}'
679 677 raise error.ParseError(_('not displayable without template'))
680 678
681 679 def show(self, context, mapping):
682 680 return self.join(context, '')
683 681
684 682 def tovalue(self, context, mapping):
685 683 return self._vars
686 684
687 685 class wsgiui(uimod.ui):
688 686 # default termwidth breaks under mod_wsgi
689 687 def termwidth(self):
690 688 return 80
691 689
692 690 def getwebsubs(repo):
693 691 websubtable = []
694 692 websubdefs = repo.ui.configitems('websub')
695 693 # we must maintain interhg backwards compatibility
696 694 websubdefs += repo.ui.configitems('interhg')
697 695 for key, pattern in websubdefs:
698 696 # grab the delimiter from the character after the "s"
699 697 unesc = pattern[1:2]
700 698 delim = re.escape(unesc)
701 699
702 700 # identify portions of the pattern, taking care to avoid escaped
703 701 # delimiters. the replace format and flags are optional, but
704 702 # delimiters are required.
705 703 match = re.match(
706 704 br'^s%s(.+)(?:(?<=\\\\)|(?<!\\))%s(.*)%s([ilmsux])*$'
707 705 % (delim, delim, delim), pattern)
708 706 if not match:
709 707 repo.ui.warn(_("websub: invalid pattern for %s: %s\n")
710 708 % (key, pattern))
711 709 continue
712 710
713 711 # we need to unescape the delimiter for regexp and format
714 712 delim_re = re.compile(br'(?<!\\)\\%s' % delim)
715 713 regexp = delim_re.sub(unesc, match.group(1))
716 714 format = delim_re.sub(unesc, match.group(2))
717 715
718 716 # the pattern allows for 6 regexp flags, so set them if necessary
719 717 flagin = match.group(3)
720 718 flags = 0
721 719 if flagin:
722 720 for flag in flagin.upper():
723 721 flags |= re.__dict__[flag]
724 722
725 723 try:
726 724 regexp = re.compile(regexp, flags)
727 725 websubtable.append((regexp, format))
728 726 except re.error:
729 727 repo.ui.warn(_("websub: invalid regexp for %s: %s\n")
730 728 % (key, regexp))
731 729 return websubtable
General Comments 0
You need to be logged in to leave comments. Login now