hgkw/keyword.py
author Christian Ebert <blacktrash@gmx.net>
Thu, 18 Jan 2007 01:50:18 +0100
branchkwmap-templates
changeset 110 b0b85b383f36
parent 109 b2cc6a8d4a18
child 111 94315baadcaf
permissions -rw-r--r--
Move all that can be done only once per repo into reposetup Actually kwrepo is not set up if there aren't any files configured for keyword substitution. Stuff that now is done at reposetup and not at every filelog init or hook: 1) filename matching function 2) compilation of keyword regex 3) templates and changeset templater kwtemplater as an appended class should prevent namespace conflicts.

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

It is recommended to enable this extension on a per-repo basis only.
You can still configure keywordmaps globally.

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
    # files matching patterns with value 'ignore' are ignored
    [keyword]
    **.py =
    x* = ignore
    ...
    # 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.i18n import gettext as _
# above line for backwards compatibility; can be changed to
#   from mercurial.i18n import _
# some day
from mercurial import context, filelog, revlog
from mercurial import commands, 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]))

def getkwconfig(ui, repo):
    inc = [pat for pat, opt in ui.configitems('keyword') if opt != 'ignore']
    if not inc:
        ui.warn(_('keyword: no filename globs for substitution\n'))
        return None, None
    exc = [pat for pat, opt in ui.configitems('keyword') if opt == 'ignore']
    return inc, exc


class kwtemplater(object):
    '''
    Sets up keyword templates, corresponding keyword regex, and
    provides keyword expansion function.

    If a repo is configured for keyword substitution, this class
    will be set as an (appendix) attribute to the repo.
    '''
    def __init__(self, ui, repo):
        self.ui = ui
        self.repo = repo
        self.templates = dict(ui.configitems('keywordmaps')) or deftemplates
        self.re_kw = re.compile(r'\$(%s)[^$]*?\$' %
                '|'.join(re.escape(k) for k in self.templates.keys()))
        templater.common_filters['utcdate'] = utcdate
        self.t = cmdutil.changeset_templater(ui, 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)
        kwsub = templater.firstline(self.ui.popbuffer())
        return '$%s: %s $' % (kw, kwsub)


def reposetup(ui, repo):
    '''Sets up repo, and filelog especially, as kwrepo and kwfilelog
    for keyword substitution.  This is done for local repos only.'''
    if not repo.local():
        return

    inc, exc = getkwconfig(ui, repo)
    if not inc:
        # no files configured for keyword substitution:
        # no need to burden repo with extra ballast
        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
            # check at init if file configured for keyword substition
            if not isinstance(repo, int) and repo.kwfmatcher(path):
                self.kwsub = True
            else:
                self.kwsub = False

        def iskwcandidate(self, data):
            '''Decides whether to act on keywords.'''
            return self.kwsub 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._repo.kwt.re_kw.sub(lambda m:
                        self._repo.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._repo.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._repo.kwt.re_kw.sub(r'$\1$', text)
            return super(kwfilelog, self).cmp(node, text)

    filelog.filelog = kwfilelog
    repo.__class__ = kwrepo

    # create filematching function once for repo
    setattr(repo, 'kwfmatcher',
            util.matcher(repo.root, inc=inc, exc=['.hg*']+exc)[1])
    # initialize kwtemplater once for repo
    setattr(repo, 'kwt', kwtemplater(ui, repo))

    # 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 inc, exc, mod


def pretxnkw(ui, repo, hooktype, **args):
    '''pretxncommit hook that collects candidates for keyword expansion
    on commit and expands keywords in working dir.'''

    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 = [f for f in modified+added if repo.kwfmatcher(f)]
    if not candidates:
        return

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