[PATCH 1 of 2] keyword: expand RCS/CVS-like keywords in local directory

Christian Ebert blacktrash at gmx.net
Thu Feb 8 08:56:34 CST 2007


# HG changeset patch
# User Christian Ebert <blacktrash at gmx.net>
# Date 1170945749 -3600
# Node ID 9a2007f392b4d95e115b7c1aaf8b30de0c89c2b5
# Parent  5b1f663ef86d68ce11d70de8e5ab61d93341a18c
keyword: expand RCS/CVS-like keywords in local directory

Configuration is done in the [keyword] and [keywordmaps] sections
of the rcfile(s).
Keywords and their expansions can be customized using
hg template mappings.

This extension breaks the tabu of changing the working directory,
but following a very defensive policy:
a) candidates for keyword expansion must be configured explicitly.
b) only text files are touched.
c) state without expansion is always stored:
   for trouble shooting just disable the extension.

diff -r 5b1f663ef86d -r 9a2007f392b4 hgext/keyword.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/keyword.py	Thu Feb 08 15:42:29 2007 +0100
@@ -0,0 +1,217 @@
+# keyword.py - keyword expansion for Mercurial
+#
+# Copyright 2007 Christian Ebert <blacktrash at gmx.net>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+#
+# $Id: keyword.py,v 9a2007f392b4 2007-02-08 15:42 +0100 blacktrash $
+#
+# Keyword expansion hack against the grain of a DSCM
+#
+# 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>.
+#
+# Keyword expansion is based on Mercurial's changeset template mappings.
+# The extension provides an additional UTC-date filter ({date|utcdate}).
+#
+# The user has the choice either to create his own keywords and their
+# expansions or to use the CVS-like default ones.
+#
+# 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
+#
+# Expansions spanning more than one line are truncated to their first line.
+# Incremental expansion (like CVS' $Log$) is not supported.
+#
+# Simple setup in hgrc:
+#
+#     # enable extension
+#     # 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}
+#     ...
+
+'''keyword expansion in local repositories
+
+This extension expands RCS/CVS-like or self-customized keywords in
+the text files selected by your configuration.
+
+Keywords are only expanded in local repositories and not logged by
+Mercurial internally. The mechanism can be regarded as a convenience
+for the current user and may be turned off anytime.
+
+Substitution takes place on every commit and update of the working
+repository.
+
+Configuration is done in the [keyword] and [keywordmaps] sections of
+hgrc files.
+'''
+
+from mercurial.i18n import _
+from mercurial import cmdutil, templater, util
+from mercurial import context, filelog, revlog
+import os.path, re, 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]))
+
+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
+        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 using 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 = [], ['.hg*']
+    for pat, opt in repo.ui.configitems('keyword'):
+        if opt != 'ignore':
+            inc.append(pat)
+        else:
+            exc.append(pat)
+    if not inc:
+        return
+
+    repo.kwfmatcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
+
+    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={}):
+            '''Wraps commit, expanding keywords of committed and
+            configured files in working directory.'''
+
+            node = super(kwrepo, self).commit(files=files,
+                    text=text, user=user, date=date,
+                    match=match, force=force, lock=lock, wlock=wlock,
+                    force_editor=force_editor, p1=p1, p2=p2, extra=extra)
+            if node is None:
+                return node
+
+            candidates = self.changelog.read(node)[3]
+            candidates = [f for f in candidates
+                    if self.kwfmatcher(f) and os.path.isfile(self.wjoin(f))]
+            if not candidates:
+                return node
+
+            kwt = kwtemplater(self.ui, self)
+            overwrite = []
+            for f in candidates:
+                data = self.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))
+                        self.wfile(f, 'w').write(data)
+                        overwrite.append(f)
+            self.dirstate.update(overwrite, 'n')
+            return node
+
+    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
+            # only init kwtemplater if needed
+            if not isinstance(repo, int) and repo.kwfmatcher(path):
+                self.kwt = kwtemplater(repo.ui, repo)
+            else:
+                self.kwt = None
+
+        def iskwcandidate(self, data):
+            '''Decides whether to act on keywords.'''
+            return self.kwt is not None 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


More information about the Mercurial-devel mailing list