hgkw/keyword.py
author Christian Ebert <blacktrash@gmx.net>
Tue, 16 Jan 2007 03:14:40 +0100
branchkwmap-templates
changeset 103 e086f7eb0198
parent 102 393d6f8ea83c
child 104 e8b9a500f2e1
permissions -rw-r--r--
Emergency brake against multiline expansion As expansion always happens ad hoc, is not tracked, multiline or incremental expansion do not seem worth the trouble. If at all, it would make sense with static, hardcoded keywords.

# keyword.py - keyword expansion for mercurial
# $Id$

'''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.

For in-depth discussion refer to
<http://www.selenic.com/mercurial/wiki/index.cgi/KeywordPlan>.

You can either use the default cvs-like keywords or provide your
own in hgrc.

Expansions spanning more than one line are truncated to their first line.
Incremental expansion (like CVS' $Log$) is not supported.

Default $keywords$ and their $keyword: substition $ are:
    Revision: changeset id
    Author:   username
    Date:     %Y/%m/%d %H:%M:%S [UTC]
    RCSFile:  basename,v
    Source:   /path/to/basename,v
    Id:       basename,v csetid %Y/%m/%d %H:%M:%S username
    Header:   /path/to/basename,v csetid %Y/%m/%d %H:%M:%S username

Simple setup in hgrc:

    # enable extension
    hgext.keyword = /full/path/to/script
    # or, if script in hgext folder:
    # hgext.keyword =
    
    # filename patterns for expansion are configured in this section
    [keyword]
    **.py = expand
    ...
    # in case you prefer your own keyword maps over the cvs-like defaults:
    [keywordmaps]
    HGdate = {date|rfc822date}
    lastlog = {desc} ## same as {desc|firstline} in this context
    checked in by = {author}
    ...
'''

from mercurial import cmdutil, templater, util
from mercurial.node import *
import os.path, re, sys, time

deftemplates = {
        'Revision': '{node|short}',
        'Author': '{author|user}',
        'Date': '{date|utcdate}',
        'RCSFile': '{file|basename},v',
        'Source': '{root}/{file},v',
        'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
        'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
        }

def utcdate(date):
    '''Returns hgdate in cvs-like UTC format.'''
    return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))

templater.common_filters['utcdate'] = utcdate

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 = []
    kwfmatchers = [util.matcher(repo.root, '', [pat], [], [])[1]
            for pat, opt in ui.configitems('keyword') if opt == 'expand']
    for f in files:
        for mf in kwfmatchers:
            if mf(f):
                candidates.append(f)
                break
    return candidates


class kwtemplater(object):
    '''
    Sets up keyword templates, corresponding keyword regex, and
    provides keyword expansion function.
    '''
    def __init__(self, ui, repo):
        self.ui = ui
        self.repo = repo
        templates = {}
        for k, v in self.ui.configitems('keywordmaps'):
            templates[k] = v
        self.templates = templates or deftemplates
        self.re_kw = re.compile(r'\$(%s)[^$]*?\$' %
                '|'.join(re.escape(k) for k in self.templates.keys()))
        self.t = cmdutil.changeset_templater(self.ui, self.repo,
                False, '', False)

    def expand(self, mobj, path, node):
        '''Expands keyword with corresponding template.'''
        kw = mobj.group(1)
        template = templater.parsestring(self.templates[kw], quoted=False)
        self.t.use_template(template)
        self.ui.pushbuffer()
        self.t.show(changenode=node, root=self.repo.root, file=path)
        expansion = templater.firstline(self.ui.popbuffer())
        return '$%s: %s $' % (kw, expansion)


def reposetup(ui, repo):
    from mercurial import context, 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
            self.kwt = kwtemplater(ui, self._repo)

        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):
                c = context.filectx(self._repo, self._path,
                                    fileid=node, filelog=self)
                return self.kwt.re_kw.sub(lambda m:
                        self.kwt.expand(m, self._path, c.node()), 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 = self.kwt.re_kw.sub(r'$\1$', text)
            return super(kwfilelog, self).add(text,
                            meta, tr, link, p1=p1, p2=p2)

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

    filelog.filelog = kwfilelog
    repo.__class__ = kwrepo
    # make pretxncommit hook import kwmodule regardless of where it's located
    for k, v in sys.modules.iteritems():
        if v is None:
            continue
        if not hasattr(v, '__file__'):
            continue
        if v.__file__.startswith(__file__):
            mod = k
            break
    else:
        sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
        mod = os.path.splitext(os.path.basename(__file__))[0]
    ui.setconfig('hooks', 'pretxncommit.keyword', 'python:%s.pretxnkw' % mod)
    del mod


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 _
    # above line for backwards compatibility; can be changed to
    #   from mercurial.i18n import _
    # some day
    from mercurial import commands

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

    files, match, anypats = cmdutil.matchpats(repo, sysargs, cmdopts)
    modified, added = repo.status(files=files, match=match)[:2]
    candidates = kwfmatches(ui, repo, modified+added)
    if not candidates:
        return

    kwt = kwtemplater(ui, repo)
    node = bin(args['node'])
    for f in candidates:
        data = repo.wfile(f).read()
        if not util.binary(data):
            data, kwct = kwt.re_kw.subn(lambda m: kwt.expand(m, f, node), data)
            if kwct:
                ui.debug(_('overwriting %s expanding keywords\n' % f))
                repo.wfile(f, 'w').write(data)