/mercurial/hgweb/webutil.py
Python | 411 lines | 393 code | 6 blank | 12 comment | 8 complexity | 0e35bdcd19bf41255cd032d0507ee3f8 MD5 | raw file
Possible License(s): GPL-2.0
- # hgweb/webutil.py - utility library for the web interface.
- #
- # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
- # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
- #
- # This software may be used and distributed according to the terms of the
- # GNU General Public License version 2 or any later version.
- import os, copy
- from mercurial import match, patch, error, ui, util, pathutil, context
- from mercurial.i18n import _
- from mercurial.node import hex, nullid
- from common import ErrorResponse
- from common import HTTP_NOT_FOUND
- import difflib
- def up(p):
- if p[0] != "/":
- p = "/" + p
- if p[-1] == "/":
- p = p[:-1]
- up = os.path.dirname(p)
- if up == "/":
- return "/"
- return up + "/"
- def _navseq(step, firststep=None):
- if firststep:
- yield firststep
- if firststep >= 20 and firststep <= 40:
- firststep = 50
- yield firststep
- assert step > 0
- assert firststep > 0
- while step <= firststep:
- step *= 10
- while True:
- yield 1 * step
- yield 3 * step
- step *= 10
- class revnav(object):
- def __init__(self, repo):
- """Navigation generation object
- :repo: repo object we generate nav for
- """
- # used for hex generation
- self._revlog = repo.changelog
- def __nonzero__(self):
- """return True if any revision to navigate over"""
- return self._first() is not None
- def _first(self):
- """return the minimum non-filtered changeset or None"""
- try:
- return iter(self._revlog).next()
- except StopIteration:
- return None
- def hex(self, rev):
- return hex(self._revlog.node(rev))
- def gen(self, pos, pagelen, limit):
- """computes label and revision id for navigation link
- :pos: is the revision relative to which we generate navigation.
- :pagelen: the size of each navigation page
- :limit: how far shall we link
- The return is:
- - a single element tuple
- - containing a dictionary with a `before` and `after` key
- - values are generator functions taking arbitrary number of kwargs
- - yield items are dictionaries with `label` and `node` keys
- """
- if not self:
- # empty repo
- return ({'before': (), 'after': ()},)
- targets = []
- for f in _navseq(1, pagelen):
- if f > limit:
- break
- targets.append(pos + f)
- targets.append(pos - f)
- targets.sort()
- first = self._first()
- navbefore = [("(%i)" % first, self.hex(first))]
- navafter = []
- for rev in targets:
- if rev not in self._revlog:
- continue
- if pos < rev < limit:
- navafter.append(("+%d" % abs(rev - pos), self.hex(rev)))
- if 0 < rev < pos:
- navbefore.append(("-%d" % abs(rev - pos), self.hex(rev)))
- navafter.append(("tip", "tip"))
- data = lambda i: {"label": i[0], "node": i[1]}
- return ({'before': lambda **map: (data(i) for i in navbefore),
- 'after': lambda **map: (data(i) for i in navafter)},)
- class filerevnav(revnav):
- def __init__(self, repo, path):
- """Navigation generation object
- :repo: repo object we generate nav for
- :path: path of the file we generate nav for
- """
- # used for iteration
- self._changelog = repo.unfiltered().changelog
- # used for hex generation
- self._revlog = repo.file(path)
- def hex(self, rev):
- return hex(self._changelog.node(self._revlog.linkrev(rev)))
- def _siblings(siblings=[], hiderev=None):
- siblings = [s for s in siblings if s.node() != nullid]
- if len(siblings) == 1 and siblings[0].rev() == hiderev:
- return
- for s in siblings:
- d = {'node': s.hex(), 'rev': s.rev()}
- d['user'] = s.user()
- d['date'] = s.date()
- d['description'] = s.description()
- d['branch'] = s.branch()
- if util.safehasattr(s, 'path'):
- d['file'] = s.path()
- yield d
- def parents(ctx, hide=None):
- if (isinstance(ctx, context.basefilectx) and
- ctx.changectx().rev() != ctx.linkrev()):
- return _siblings([ctx._repo[ctx.linkrev()]], hide)
- return _siblings(ctx.parents(), hide)
- def children(ctx, hide=None):
- return _siblings(ctx.children(), hide)
- def renamelink(fctx):
- r = fctx.renamed()
- if r:
- return [{'file': r[0], 'node': hex(r[1])}]
- return []
- def nodetagsdict(repo, node):
- return [{"name": i} for i in repo.nodetags(node)]
- def nodebookmarksdict(repo, node):
- return [{"name": i} for i in repo.nodebookmarks(node)]
- def nodebranchdict(repo, ctx):
- branches = []
- branch = ctx.branch()
- # If this is an empty repo, ctx.node() == nullid,
- # ctx.branch() == 'default'.
- try:
- branchnode = repo.branchtip(branch)
- except error.RepoLookupError:
- branchnode = None
- if branchnode == ctx.node():
- branches.append({"name": branch})
- return branches
- def nodeinbranch(repo, ctx):
- branches = []
- branch = ctx.branch()
- try:
- branchnode = repo.branchtip(branch)
- except error.RepoLookupError:
- branchnode = None
- if branch != 'default' and branchnode != ctx.node():
- branches.append({"name": branch})
- return branches
- def nodebranchnodefault(ctx):
- branches = []
- branch = ctx.branch()
- if branch != 'default':
- branches.append({"name": branch})
- return branches
- def showtag(repo, tmpl, t1, node=nullid, **args):
- for t in repo.nodetags(node):
- yield tmpl(t1, tag=t, **args)
- def showbookmark(repo, tmpl, t1, node=nullid, **args):
- for t in repo.nodebookmarks(node):
- yield tmpl(t1, bookmark=t, **args)
- def cleanpath(repo, path):
- path = path.lstrip('/')
- return pathutil.canonpath(repo.root, '', path)
- def changeidctx (repo, changeid):
- try:
- ctx = repo[changeid]
- except error.RepoError:
- man = repo.manifest
- ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]
- return ctx
- def changectx (repo, req):
- changeid = "tip"
- if 'node' in req.form:
- changeid = req.form['node'][0]
- ipos=changeid.find(':')
- if ipos != -1:
- changeid = changeid[(ipos + 1):]
- elif 'manifest' in req.form:
- changeid = req.form['manifest'][0]
- return changeidctx(repo, changeid)
- def basechangectx(repo, req):
- if 'node' in req.form:
- changeid = req.form['node'][0]
- ipos=changeid.find(':')
- if ipos != -1:
- changeid = changeid[:ipos]
- return changeidctx(repo, changeid)
- return None
- def filectx(repo, req):
- if 'file' not in req.form:
- raise ErrorResponse(HTTP_NOT_FOUND, 'file not given')
- path = cleanpath(repo, req.form['file'][0])
- if 'node' in req.form:
- changeid = req.form['node'][0]
- elif 'filenode' in req.form:
- changeid = req.form['filenode'][0]
- else:
- raise ErrorResponse(HTTP_NOT_FOUND, 'node or filenode not given')
- try:
- fctx = repo[changeid][path]
- except error.RepoError:
- fctx = repo.filectx(path, fileid=changeid)
- return fctx
- def listfilediffs(tmpl, files, node, max):
- for f in files[:max]:
- yield tmpl('filedifflink', node=hex(node), file=f)
- if len(files) > max:
- yield tmpl('fileellipses')
- def diffs(repo, tmpl, ctx, basectx, files, parity, style):
- def countgen():
- start = 1
- while True:
- yield start
- start += 1
- blockcount = countgen()
- def prettyprintlines(diff, blockno):
- for lineno, l in enumerate(diff.splitlines(True)):
- lineno = "%d.%d" % (blockno, lineno + 1)
- if l.startswith('+'):
- ltype = "difflineplus"
- elif l.startswith('-'):
- ltype = "difflineminus"
- elif l.startswith('@'):
- ltype = "difflineat"
- else:
- ltype = "diffline"
- yield tmpl(ltype,
- line=l,
- lineid="l%s" % lineno,
- linenumber="% 8s" % lineno)
- if files:
- m = match.exact(repo.root, repo.getcwd(), files)
- else:
- m = match.always(repo.root, repo.getcwd())
- diffopts = patch.diffopts(repo.ui, untrusted=True)
- if basectx is None:
- parents = ctx.parents()
- node1 = parents and parents[0].node() or nullid
- else:
- node1 = basectx.node()
- node2 = ctx.node()
- block = []
- for chunk in patch.diff(repo, node1, node2, m, opts=diffopts):
- if chunk.startswith('diff') and block:
- blockno = blockcount.next()
- yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
- lines=prettyprintlines(''.join(block), blockno))
- block = []
- if chunk.startswith('diff') and style != 'raw':
- chunk = ''.join(chunk.splitlines(True)[1:])
- block.append(chunk)
- blockno = blockcount.next()
- yield tmpl('diffblock', parity=parity.next(), blockno=blockno,
- lines=prettyprintlines(''.join(block), blockno))
- def compare(tmpl, context, leftlines, rightlines):
- '''Generator function that provides side-by-side comparison data.'''
- def compline(type, leftlineno, leftline, rightlineno, rightline):
- lineid = leftlineno and ("l%s" % leftlineno) or ''
- lineid += rightlineno and ("r%s" % rightlineno) or ''
- return tmpl('comparisonline',
- type=type,
- lineid=lineid,
- leftlinenumber="% 6s" % (leftlineno or ''),
- leftline=leftline or '',
- rightlinenumber="% 6s" % (rightlineno or ''),
- rightline=rightline or '')
- def getblock(opcodes):
- for type, llo, lhi, rlo, rhi in opcodes:
- len1 = lhi - llo
- len2 = rhi - rlo
- count = min(len1, len2)
- for i in xrange(count):
- yield compline(type=type,
- leftlineno=llo + i + 1,
- leftline=leftlines[llo + i],
- rightlineno=rlo + i + 1,
- rightline=rightlines[rlo + i])
- if len1 > len2:
- for i in xrange(llo + count, lhi):
- yield compline(type=type,
- leftlineno=i + 1,
- leftline=leftlines[i],
- rightlineno=None,
- rightline=None)
- elif len2 > len1:
- for i in xrange(rlo + count, rhi):
- yield compline(type=type,
- leftlineno=None,
- leftline=None,
- rightlineno=i + 1,
- rightline=rightlines[i])
- s = difflib.SequenceMatcher(None, leftlines, rightlines)
- if context < 0:
- yield tmpl('comparisonblock', lines=getblock(s.get_opcodes()))
- else:
- for oc in s.get_grouped_opcodes(n=context):
- yield tmpl('comparisonblock', lines=getblock(oc))
- def diffstatgen(ctx, basectx):
- '''Generator function that provides the diffstat data.'''
- stats = patch.diffstatdata(util.iterlines(ctx.diff(basectx)))
- maxname, maxtotal, addtotal, removetotal, binary = patch.diffstatsum(stats)
- while True:
- yield stats, maxname, maxtotal, addtotal, removetotal, binary
- def diffsummary(statgen):
- '''Return a short summary of the diff.'''
- stats, maxname, maxtotal, addtotal, removetotal, binary = statgen.next()
- return _(' %d files changed, %d insertions(+), %d deletions(-)\n') % (
- len(stats), addtotal, removetotal)
- def diffstat(tmpl, ctx, statgen, parity):
- '''Return a diffstat template for each file in the diff.'''
- stats, maxname, maxtotal, addtotal, removetotal, binary = statgen.next()
- files = ctx.files()
- def pct(i):
- if maxtotal == 0:
- return 0
- return (float(i) / maxtotal) * 100
- fileno = 0
- for filename, adds, removes, isbinary in stats:
- template = filename in files and 'diffstatlink' or 'diffstatnolink'
- total = adds + removes
- fileno += 1
- yield tmpl(template, node=ctx.hex(), file=filename, fileno=fileno,
- total=total, addpct=pct(adds), removepct=pct(removes),
- parity=parity.next())
- class sessionvars(object):
- def __init__(self, vars, start='?'):
- self.start = start
- self.vars = vars
- def __getitem__(self, key):
- return self.vars[key]
- def __setitem__(self, key, value):
- self.vars[key] = value
- def __copy__(self):
- return sessionvars(copy.copy(self.vars), self.start)
- def __iter__(self):
- separator = self.start
- for key, value in sorted(self.vars.iteritems()):
- yield {'name': key, 'value': str(value), 'separator': separator}
- separator = '&'
- class wsgiui(ui.ui):
- # default termwidth breaks under mod_wsgi
- def termwidth(self):
- return 80