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