view mercurial/hgweb/webutil.py @ 17202:1ae119269ddc

hgweb: side-by-side comparison functionality Adds new web command to the core, ``comparison``, which enables colorful side-by-side change display, which for some might be much easier to work with than the standard line diff output. The idea how to implement comes from the SonicHq extension. The web interface gets a new link to call the comparison functionality. It lets users configure the amount of context lines around change blocks, or to show full files - check help (also in this changeset) for details and defaults. The setting in hgrc can be overridden by adding ``context=<value>`` to the request query string. The comparison creates addressable lines, so as to enable sharing links to specific lines, just as standard diff does. Incorporates updates to all web related styles. Known limitations: * the column diff is done against the first parent, just as the standard diff * this change allows examining diffs for single files only (as I am not sure if examining the whole changeset in this way would be helpful) * syntax highlighting of the output changes is not performed (enabling the highlight extension has no influence on it)
author wujek srujek
date Sun, 08 Jul 2012 17:17:02 +0200
parents e7bf09acd410
children f2d6b4f8e78c
line wrap: on
line source

# 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, mimetypes, copy
from mercurial import match, patch, scmutil, error, ui, util
from mercurial.i18n import _
from mercurial.node import hex, nullid
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 revnavgen(pos, pagelen, limit, nodefunc):
    def seq(factor, limit=None):
        if limit:
            yield limit
            if limit >= 20 and limit <= 40:
                yield 50
        else:
            yield 1 * factor
            yield 3 * factor
        for f in seq(factor * 10):
            yield f

    navbefore = []
    navafter = []

    last = 0
    for f in seq(1, pagelen):
        if f < pagelen or f <= last:
            continue
        if f > limit:
            break
        last = f
        if pos + f < limit:
            navafter.append(("+%d" % f, hex(nodefunc(pos + f).node())))
        if pos - f >= 0:
            navbefore.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node())))

    navafter.append(("tip", "tip"))
    try:
        navbefore.insert(0, ("(0)", hex(nodefunc('0').node())))
    except error.RepoError:
        pass

    def gen(l):
        def f(**map):
            for label, node in l:
                yield {"label": label, "node": node}
        return f

    return (dict(before=gen(navbefore), after=gen(navafter)),)

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):
    return _siblings(ctx.parents(), hide)

def children(ctx, hide=None):
    return _siblings(ctx.children(), hide)

def renamelink(fctx):
    r = fctx.renamed()
    if r:
        return [dict(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 scmutil.canonpath(repo.root, '', path)

def changectx(repo, req):
    changeid = "tip"
    if 'node' in req.form:
        changeid = req.form['node'][0]
    elif 'manifest' in req.form:
        changeid = req.form['manifest'][0]

    try:
        ctx = repo[changeid]
    except error.RepoError:
        man = repo.manifest
        ctx = repo[man.linkrev(man.rev(man.lookup(changeid)))]

    return ctx

def filectx(repo, req):
    path = cleanpath(repo, req.form['file'][0])
    if 'node' in req.form:
        changeid = req.form['node'][0]
    else:
        changeid = req.form['filenode'][0]
    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, 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)
    parents = ctx.parents()
    node1 = parents and parents[0].node() or nullid
    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, ctx, path, context):
    '''Generator function that provides side-by-side comparison data.'''

    def filelines(f):
        if util.binary(f.data()):
            mt = mimetypes.guess_type(f.path())[0]
            if not mt:
                mt = 'application/octet-stream'
            return [_('(binary file %s, hash: %s)') % (mt, hex(f.filenode()))]
        return f.data().splitlines()

    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])

    if path in ctx:
        fctx = ctx[path]
        rightrev = fctx.filerev()
        rightnode = fctx.filenode()
        rightlines = filelines(fctx)
        parents = fctx.parents()
        if not parents:
            leftrev = -1
            leftnode = nullid
            leftlines = ()
        else:
            pfctx = parents[0]
            leftrev = pfctx.filerev()
            leftnode = pfctx.filenode()
            leftlines = filelines(pfctx)
    else:
        rightrev = -1
        rightnode = nullid
        rightlines = ()
        fctx = ctx.parents()[0][path]
        leftrev = fctx.filerev()
        leftnode = fctx.filenode()
        leftlines = filelines(fctx)

    s = difflib.SequenceMatcher(None, leftlines, rightlines)
    if context < 0:
        blocks = [tmpl('comparisonblock', lines=getblock(s.get_opcodes()))]
    else:
        blocks = (tmpl('comparisonblock', lines=getblock(oc))
                     for oc in s.get_grouped_opcodes(n=context))

    yield tmpl('comparison',
               leftrev=leftrev,
               leftnode=hex(leftnode),
               rightrev=rightrev,
               rightnode=hex(rightnode),
               blocks=blocks)

def diffstatgen(ctx):
    '''Generator function that provides the diffstat data.'''

    stats = patch.diffstatdata(util.iterlines(ctx.diff()))
    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 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