[PATCH 1 of 2] keyword: extension to expand RCS/CVS-like keywords in tracked files
Christian Ebert
blacktrash at gmx.net
Mon Mar 5 10:50:04 UTC 2007
# HG changeset patch
# User Christian Ebert <blacktrash at gmx.net>
# Date 1173091556 -3600
# Node ID e0bb330760dab5496b4c8d5d400b8cb90b91e803
# Parent 193e0f8d9a47af8c45d93a9761194750182537ab
keyword: extension to expand RCS/CVS-like keywords in tracked files
See doc and help for details.
diff -r 193e0f8d9a47 -r e0bb330760da hgext/keyword.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/keyword.py Mon Mar 05 11:45:56 2007 +0100
@@ -0,0 +1,277 @@
+# 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 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.
+#
+# Expansions spanning more than one line are truncated to their first line.
+# Incremental expansion (like CVS' $Log$) is not supported.
+#
+# Binary files are not touched.
+#
+# Setup in hgrc:
+#
+# # enable extension
+# # hgext.keyword =
+
+'''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.
+
+The exansion works in 2 modes:
+ 1) working mode: substitution takes place on every commit and
+ update of the working repository.
+ 2) archive mode: substitution is only triggered by "hg archive".
+
+Caveat: "hg import" might fail if the patches were exported from a
+repo with a different/no keyword setup, whereas "hg unbundle" is
+safe.
+
+Configuration is done in the [keyword] and [keywordmaps] sections of
+hgrc files.
+
+Example:
+ [keyword]
+ # filename patterns for expansion are configured in this section
+ **.py = ## expand keywords in all python files
+ x* = ignore ## but ignore files matching "x*"
+ ** = archive ## keywords in all textfiles are expanded
+ ## when creating a distribution
+ y* = noarchive ## keywords in files matching "y*" are not expanded
+ ## on archive creation
+ ...
+ [keywordmaps]
+ # custom hg template maps _replace_ the CVS-like default ones
+ HGdate = {date|rfc822date}
+ lastlog = {desc} ## same as {desc|firstline} in this context
+ checked in by = {author}
+ ...
+
+If no [keywordmaps] are configured the extension falls back on the
+following defaults:
+
+ 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
+'''
+
+from mercurial.i18n import _
+from mercurial import commands, fancyopts, templater, util
+from mercurial import cmdutil, context, filelog, revlog
+import 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 getcmd(ui):
+ '''Returns current hg command.'''
+ # commands.parse(ui, sys.argv[1:])[0] breaks "hg diff -r"
+ try:
+ args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts, {})
+ except fancyopts.getopt.GetoptError, inst:
+ raise commands.ParseError(None, inst)
+ if args:
+ cmd = args[0]
+ aliases, i = commands.findcmd(ui, cmd)
+ return aliases[0]
+
+class kwtemplater(object):
+ '''
+ Sets up keyword templates, corresponding keyword regex, and
+ provides keyword substitution functions.
+ '''
+ def __init__(self, ui, repo):
+ self.ui = ui
+ self.repo = repo
+ templates = dict(ui.configitems('keywordmaps'))
+ if templates:
+ # parse templates here for less overhead in kwsub matchfunc
+ for k in templates.keys():
+ templates[k] = templater.parsestring(templates[k],
+ quoted=False)
+ self.templates = templates 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 kwsub(self, mobj, path, node):
+ '''Substitutes keyword using corresponding template.'''
+ kw = mobj.group(1)
+ self.t.use_template(self.templates[kw])
+ self.ui.pushbuffer()
+ self.t.show(changenode=node, root=self.repo.root, file=path)
+ keywordsub = templater.firstline(self.ui.popbuffer())
+ return '$%s: %s $' % (kw, keywordsub)
+
+ def expand(self, path, node, filelog, data):
+ '''Returns data with expanded keywords.'''
+ if util.binary(data):
+ return data
+ c = context.filectx(self.repo, path, fileid=node, filelog=filelog)
+ cnode = c.node()
+ return self.re_kw.sub(lambda m: self.kwsub(m, path, cnode), data)
+
+ def shrink(self, text):
+ '''Returns text with all keyword substitutions removed.'''
+ if util.binary(text):
+ return text
+ return self.re_kw.sub(r'$\1$', text)
+
+ def overwrite(self, candidates, node):
+ '''Overwrites candidates in working dir expanding keywords.'''
+ files = []
+ for f in candidates:
+ data = self.repo.wfile(f).read()
+ if not util.binary(data):
+ data, kwct = self.re_kw.subn(lambda m:
+ self.kwsub(m, f, node), data)
+ if kwct:
+ self.ui.debug(_('overwriting %s expanding keywords\n') % f)
+ self.repo.wfile(f, 'w').write(data)
+ files.append(f)
+ if files:
+ self.repo.dirstate.update(files, 'n')
+
+class kwfilelog(filelog.filelog):
+ '''
+ Superclass over filelog to customize its read, add, cmp methods.
+ Keywords are "stored" unexpanded, and expanded on reading.
+ '''
+ def __init__(self, opener, path, kwtemplater,
+ defversion=revlog.REVLOG_DEFAULT_VERSION):
+ super(kwfilelog, self).__init__(opener, path, defversion)
+ self.path = path
+ self.kwtemplater = kwtemplater
+
+ def read(self, node):
+ '''Substitutes keywords when reading filelog.'''
+ data = super(kwfilelog, self).read(node)
+ return self.kwtemplater.expand(self.path, node, self, data)
+
+ def add(self, text, meta, tr, link, p1=None, p2=None):
+ '''Removes keyword substitutions when adding to filelog.'''
+ text = self.kwtemplater.shrink(text)
+ return super(kwfilelog, self).add(text,
+ meta, tr, link, p1=p1, p2=p2)
+
+ def cmp(self, node, text):
+ '''Removes keyword substitutions for comparison.'''
+ text = self.kwtemplater.shrink(text)
+ return super(kwfilelog, self).cmp(node, text)
+
+
+def reposetup(ui, repo):
+ '''Sets up repo as kwrepo for keyword substitution.
+ Overrides file method to return kwfilelog instead of filelog
+ if file matches user configuration.
+ Wraps commit to overwrite configured files with updated
+ keyword substitutions.
+ This is done for local repos only, and only if there are
+ files configured at all for keyword substitution.'''
+
+ if not repo.local():
+ return
+
+ archivemode = (getcmd(ui) == 'archive')
+
+ inc, exc, archive, noarchive = [], ['.hg*'], [], ['.hg*']
+ for pat, opt in ui.configitems('keyword'):
+ if opt == 'archive':
+ archive.append(pat)
+ elif opt == 'noarchive':
+ noarchive.append(pat)
+ elif opt == 'ignore':
+ exc.append(pat)
+ else:
+ inc.append(pat)
+ if archivemode:
+ inc, exc = archive, noarchive
+ 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:]
+ # only use kwfilelog when needed
+ if self.kwfmatcher(f):
+ kwt = kwtemplater(self.ui, self)
+ return kwfilelog(self.sopener, f, kwt, self.revlogversion)
+ else:
+ return filelog.filelog(self.sopener, f, 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.'''
+ wrelease = False
+ if not wlock:
+ wlock = self.wlock()
+ wrelease = True
+ try:
+ removed = self.status()[2]
+
+ 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 f not in removed
+ and not self._link(f) and self.kwfmatcher(f)]
+ if not candidates:
+ return node
+
+ kwt = kwtemplater(self.ui, self)
+ kwt.overwrite(candidates, node)
+ return node
+ finally:
+ if wrelease:
+ wlock.release()
+
+ repo.__class__ = kwrepo
More information about the Mercurial-devel
mailing list