hgkw/keyword.py
author Christian Ebert <blacktrash@gmx.net>
Mon, 08 Jan 2007 18:27:18 +0100
branchself_initializing_hook
changeset 81 fd5d3a974ea7
parent 80 cee5fef33cf8
child 82 9bf0f7db5928
permissions -rw-r--r--
Implement self initializing pretxncommit hook This is more expensive than overriding commit(), but a) uses more "official" interface b) less code c) easier to maintain Get all changes up to cee5fef33cf8 in here as well.

# keyword.py - keyword expansion for mercurial

'''keyword expansion hack against the grain of a DSCM

This extension lets you expand RCS/CVS-like keywords in a Mercurial
repository.

There are many good reasons why this is not needed in a distributed
SCM, still it may be useful in very small projects based on single
files (like LaTeX packages), that are mostly addressed to an audience
not running a version control system.

Supported $keywords$ and their $keyword: substition $ are:
    Revision: changeset id
    Author:   full username
    Date:     %a %b %d %H:%M:%S %Y %z $
    RCSFile:  basename,v
    Source:   /path/to/basename,v
    Id:       basename,v csetid %Y-%m-%d %H:%M:%S %z shortname
    Header:   /path/to/basename,v csetid %Y-%m-%d %H:%M:%S %z shortname

Install:
    Copy keyword.py in hgext folder.

Simple setup in hgrc:

    # enable extension
    hgext.keyword =
    
    # filename patterns for expansion are configured in this section
    [keyword]
    **.py = expand
    ...
'''

from mercurial import context, util
import os.path, re


re_kw = re.compile(
        r'\$(Id|Header|Author|Date|Revision|RCSFile|Source)[^$]*?\$')


def kwexpand(matchobj, repo, path, changeid=None, fileid=None, filelog=None):
    '''Called by kwfilelog.read and pretxnkw.
    Sets supported keywords as local variables and evaluates them to
    their expansion if matchobj is equal to string representation.'''
    c = context.filectx(repo, path,
            changeid=changeid, fileid=fileid, filelog=filelog)
    date = c.date()
    Revision = c.changectx()
    Author = c.user()
    RCSFile = os.path.basename(path)+',v'
    Source = repo.wjoin(path)+',v'
    Date = util.datestr(date=date)
    revdateauth = '%s %s %s' % (Revision,
            util.datestr(date=date, format=util.defaultdateformats[0]),
            util.shortuser(Author))
    Header = '%s %s' % (Source, revdateauth)
    Id = '%s %s' % (RCSFile, revdateauth)
    return '$%s: %s $' % (matchobj.group(1), eval(matchobj.group(1)))

def kwfmatches(ui, repo, files):
    '''Selects candidates for keyword substitution
    configured in keyword section in hgrc.'''
    files = [f for f in files if not f.startswith('.hg')]
    if not files:
        return []
    candidates = []
    fmatchers = [util.matcher(repo.root, '', [pat], [], [])[1]
            for pat, opt in ui.configitems('keyword')
            if opt == 'expand']
    for f in files:
        for mf in fmatchers:
            if mf(f):
                candidates.append(f)
                break
    return candidates


def reposetup(ui, repo):
    from mercurial import filelog, revlog

    if not repo.local():
        return

    class kwrepo(repo.__class__):
        def file(self, f):
            if f[0] == '/':
                f = f[1:]
            return filelog.filelog(self.sopener, f, self, self.revlogversion)

    class kwfilelog(filelog.filelog):
        '''
        Superclass over filelog to customize it's read, add, cmp methods.
        Keywords are "stored" unexpanded, and expanded on reading.
        '''
        def __init__(self, opener, path, repo,
                     defversion=revlog.REVLOG_DEFAULT_VERSION):
            super(kwfilelog, self).__init__(opener, path, defversion)
            self._repo = repo
            self._path = path

        def iskwcandidate(self, data):
            '''Decides whether to act on keywords.'''
            return (kwfmatches(ui, self._repo, [self._path])
                    and not util.binary(data))

        def read(self, node):
            '''Substitutes keywords when reading filelog.'''
            data = super(kwfilelog, self).read(node)
            if self.iskwcandidate(data):
                return re_kw.sub(lambda m:
                        kwexpand(m, self._repo, self._path,
                            fileid=node, filelog=self), data)
            return data

        def add(self, text, meta, tr, link, p1=None, p2=None):
            '''Removes keyword substitutions when adding to filelog.'''
            if self.iskwcandidate(text):
                text = re_kw.sub(r'$\1$', text)
            return super(kwfilelog, self).add(text,
                    meta, tr, link, p1=None, p2=None)

        def cmp(self, node, text):
            '''Removes keyword substitutions for comparison.'''
            if self.iskwcandidate(text):
                text = re_kw.sub(r'$\1$', text)
            return super(kwfilelog, self).cmp(node, text)

    filelog.filelog = kwfilelog
    repo.__class__ = kwrepo
    ui.setconfig('hooks',
            'pretxncommit.keyword', 'python:hgext.keyword.pretxnkw')


def pretxnkw(ui, repo, hooktype, **args):
    '''pretxncommit hook that collects candidates for keyword expansion
    on commit and expands keywords in working dir.'''
    from mercurial.i18n import gettext as _
    from mercurial import cmdutil, commands
    import sys

    if hooktype != 'pretxncommit':
        return True

    cmd, sysargs, globalopts, cmdopts = commands.parse(ui, sys.argv[1:])[1:]
    if repr(cmd).split()[1] in ('tag', 'import_'):
        return False

    files, match, anypats = cmdutil.matchpats(repo, sysargs, cmdopts)
    modified, added = repo.status(files=files, match=match)[:2]

    for f in kwfmatches(ui, repo, modified+added):
        data = repo.wfile(f).read()
        if not util.binary(data):
            data, kwct = re_kw.subn(lambda m:
                    kwexpand(m, repo, f, changeid=args['node']),
                    data)
            if kwct:
                ui.debug(_('overwriting %s expanding keywords\n' % f))
                repo.wfile(f, 'w').write(data)