[PATCH] extension for keyword substitution

Christian Ebert blacktrash at gmx.net
Sat Jan 6 08:36:23 CST 2007


# HG changeset patch
# User Christian Ebert <blacktrash at gmx.net>
# Date 1168097576 -3600
# Node ID f1ddbdb6ae1c24d031736c5296db79b605609d8d
# Parent  dfe87137ed143fc658489acc765d411937f33a43
extension for keyword substitution

proposal for a keyword extension without additional hooks,
but at the price of redefining commit().
fortunately it does nothing to files that are not explicitly configured
in the [keyword] section of hgrc.

diff -r dfe87137ed14 -r f1ddbdb6ae1c contrib/keyword.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/contrib/keyword.py	Sat Jan 06 16:32:56 2007 +0100
@@ -0,0 +1,281 @@
+# 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$ 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):
+        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 read(self, node):
+            data = super(kwfilelog, self).read(node)
+            if not util.binary(data) and \
+                    kwfmatches(ui, self._repo, [self._path]):
+                ui.debug(_('expanding keywords in %s\n' % self._path))
+                return re_kw.sub(lambda m:
+                        kwexpand(m, self._repo, self._path,
+                            fileid=node, filelog=self), data)
+            return data
+
+        def size(self, rev):
+            '''Overrides filelog's size() to use kwfilelog.read().'''
+            node = revlog.node(self, rev)
+            if super(kwfilelog, self).renamed(node):
+                return len(self.read(node))
+            return revlog.size(self, rev)
+
+        def cmp(self, node, text):
+            '''Overrides filelog's cmp() to use kwfilelog.read().'''
+            if super(kwfilelog, self).renamed(node):
+                t2 = self.read(node)
+                return t2 != text
+
+    filelog.filelog = kwfilelog
+    repo.__class__ = kwrepo


More information about the Mercurial-devel mailing list