hgkw/keyword.py
author Christian Ebert <blacktrash@gmx.net>
Mon, 08 Jan 2007 12:54:31 +0100
branchsolo-extension
changeset 78 474b415433a1
parent 77 048153ccf040
child 79 33eb5aa6f6e1
permissions -rw-r--r--
Unexpanded storage hopefully covered now by adding kwfilelog.add (again!) All filelog methods that act on text/data should be consistent. Principle: read expanded kw's; unexpanded kw's for "internal" workings. kwfilelog.iskwcandidate() to check whether keyword action required. More doc.

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

Supported $keywords$ 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 %s shortname
    Header:   /path/to/basename,v csetid %Y-%m-%d %H:%M:%S %s shortname

Simple setup in hgrc:

    # enable extension
    # keyword.py in hgext folder, specify full path otherwise
    hgext.keyword =
    
    # filename patterns for expansion are configured in this section
    [keyword]
    **.py = expand
    ...
'''

from mercurial.node import *
from mercurial.i18n import _
from mercurial import context, filelog, revlog, 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 kwrepo.commit and kwfilelog.read.
    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):

    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)

        def commit(self, files=None, text="", user=None, date=None,
                match=util.always, force=False, lock=None, wlock=None,
                force_editor=False, p1=None, p2=None, extra={}):

            commit = []
            remove = []
            changed = []
            use_dirstate = (p1 is None) # not rawcommit
            extra = extra.copy()

            if use_dirstate:
                if files:
                    for f in files:
                        s = self.dirstate.state(f)
                        if s in 'nmai':
                            commit.append(f)
                        elif s == 'r':
                            remove.append(f)
                        else:
                            ui.warn(_("%s not tracked!\n") % f)
                else:
                    changes = self.status(match=match)[:5]
                    modified, added, removed, deleted, unknown = changes
                    commit = modified + added
                    remove = removed
            else:
                commit = files

            if use_dirstate:
                p1, p2 = self.dirstate.parents()
                update_dirstate = True
            else:
                p1, p2 = p1, p2 or nullid
                update_dirstate = (self.dirstate.parents()[0] == p1)

            c1 = self.changelog.read(p1)
            c2 = self.changelog.read(p2)
            m1 = self.manifest.read(c1[0]).copy()
            m2 = self.manifest.read(c2[0])

            if use_dirstate:
                branchname = self.workingctx().branch()
                try:
                    branchname = branchname.decode('UTF-8').encode('UTF-8')
                except UnicodeDecodeError:
                    raise util.Abort(_('branch name not in UTF-8!'))
            else:
                branchname = ""

            if use_dirstate:
                oldname = c1[5].get("branch", "") # stored in UTF-8
                if not commit and not remove and not force and p2 == nullid and \
                       branchname == oldname:
                    ui.status(_("nothing changed\n"))
                    return None

            xp1 = hex(p1)
            if p2 == nullid: xp2 = ''
            else: xp2 = hex(p2)

            self.hook("precommit", throw=True, parent1=xp1, parent2=xp2)

            if not wlock:
                wlock = self.wlock()
            if not lock:
                lock = self.lock()
            tr = self.transaction()

            # check in files
            new = {}
            linkrev = self.changelog.count()
            commit.sort()
            is_exec = util.execfunc(self.root, m1.execf)
            is_link = util.linkfunc(self.root, m1.linkf)
            for f in commit:
                ui.note(f + "\n")
                try:
                    new[f] = self.filecommit(f, m1, m2, linkrev, tr, changed)
                    m1.set(f, is_exec(f), is_link(f))
                except OSError:
                    if use_dirstate:
                        ui.warn(_("trouble committing %s!\n") % f)
                        raise
                    else:
                        remove.append(f)

            # update manifest
            m1.update(new)
            remove.sort()
            removed = []

            for f in remove:
                if f in m1:
                    del m1[f]
                    removed.append(f)
            mn = self.manifest.add(m1, tr, linkrev, c1[0], c2[0], (new, removed))

            # add changeset
            new = new.keys()
            new.sort()

            user = user or ui.username()
            if not text or force_editor:
                edittext = []
                if text:
                    edittext.append(text)
                edittext.append("")
                edittext.append("HG: user: %s" % user)
                if p2 != nullid:
                    edittext.append("HG: branch merge")
                edittext.extend(["HG: changed %s" % f for f in changed])
                edittext.extend(["HG: removed %s" % f for f in removed])
                if not changed and not remove:
                    edittext.append("HG: no files changed")
                edittext.append("")
                # run editor in the repository root
                olddir = os.getcwd()
                os.chdir(self.root)
                text = ui.edit("\n".join(edittext), user)
                os.chdir(olddir)

            lines = [line.rstrip() for line in text.rstrip().splitlines()]
            while lines and not lines[0]:
                del lines[0]
            if not lines:
                return None
            text = '\n'.join(lines)
            if branchname:
                extra["branch"] = branchname
            n = self.changelog.add(mn, changed + removed, text, tr, p1, p2,
                                   user, date, extra)
            self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
                      parent2=xp2)

            # substitute keywords
            for f in kwfmatches(ui, self, changed):
                data = self.wfile(f).read()
                if not util.binary(data):
                    data, kwct = re_kw.subn(lambda m:
                            kwexpand(m, self, f, changeid=hex(n)), data)
                    if kwct:
                        ui.debug(_('overwriting %s expanding keywords\n' % f))
                        self.wfile(f, 'w').write(data)

            tr.close()

            if use_dirstate or update_dirstate:
                self.dirstate.setparents(n)
                if use_dirstate:
                    self.dirstate.update(new, "n")
                    self.dirstate.forget(removed)

            self.hook("commit", node=hex(n), parent1=xp1, parent2=xp2)
            return n

    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