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