--- a/hgkw/keyword.py Mon Jan 15 15:38:36 2007 +0100
+++ b/hgkw/keyword.py Fri Mar 30 09:08:48 2007 +0200
@@ -1,145 +1,260 @@
-# keyword.py - keyword expansion for mercurial
+# keyword.py - keyword expansion for Mercurial
+#
+# Copyright 2007 Christian Ebert <blacktrash@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
+#
+# 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
+# keyword = /full/path/to/keyword.py
+# # or, if script in hgext folder:
+# # hgext.keyword =
-This extension lets you expand RCS/CVS-like keywords in a Mercurial
-repository.
+'''keyword expansion in local repositories
-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.
+This extension expands RCS/CVS-like or self-customized keywords in
+the text files selected by your configuration.
-Supported $keywords$ and their $keyword: substition $ are:
- Revision: changeset id
- Author: short 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 shortname
- Header: /path/to/basename,v csetid %Y/%m/%d %H:%M:%S shortname
+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.
-Simple setup in hgrc:
+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}
+ ...
- # enable extension
- hgext.keyword =
- # or, if script not in hgext folder:
- # hgext.keyword = /full/path/to/script
-
- # filename patterns for expansion are configured in this section
- [keyword]
- **.py = expand
- ...
+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 import context, util
-import os.path, re, sys, time
-
-re_kw = re.compile(
- r'\$(Id|Header|Author|Date|Revision|RCSFile|Source)[^$]*?\$')
+from mercurial.node import *
+try:
+ from mercurial.demandload import * # stable
+ from mercurial.i18n import gettext as _
+ demandload(globals(), 'mercurial:commands,fancyopts,templater,util')
+ demandload(globals(), 'mercurial:cmdutil,context,filelog')
+ demandload(globals(), 'os re sys time')
+except ImportError: # demandimport
+ from mercurial.i18n import _
+ from mercurial import commands, fancyopts, templater, util
+ from mercurial import cmdutil, context, filelog
+ import os, re, sys, time
-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 = []
- kwfmatchers = [util.matcher(repo.root, '', [pat], [], [])[1]
- for pat, opt in ui.configitems('keyword') if opt == 'expand']
- for f in files:
- for mf in kwfmatchers:
- if mf(f):
- candidates.append(f)
- break
- return candidates
+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 kwfilectx(context.filectx):
+class kwtemplater(object):
'''
- Provides keyword expansion functions based on file context.
+ Sets up keyword templates, corresponding keyword regex, and
+ provides keyword substitution functions.
'''
- def __init__(self, repo, path, changeid=None, fileid=None, filelog=None):
- context.filectx.__init__(self, repo, path, changeid, fileid, filelog)
- def Revision(self):
- return str(self.changectx())
- def Author(self):
- return util.shortuser(self.user())
- def Date(self):
- return utcdate(self.date())
- def RCSFile(self):
- return os.path.basename(self._path)+',v'
- def Source(self):
- return self._repo.wjoin(self._path)+',v'
- def Header(self):
- return ' '.join(
- [self.Source(), self.Revision(), self.Date(), self.Author()])
- def Id(self):
- return ' '.join(
- [self.RCSFile(), self.Revision(), self.Date(), self.Author()])
- def expand(self, mobj):
- '''Called from kwexpand, evaluates keyword.'''
+ 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
+ try:
+ self.t = cmdutil.changeset_templater(ui, repo,
+ False, '', False)
+ except TypeError:
+ # depending on hg rev changeset_templater has extra "brinfo" arg
+ self.t = cmdutil.changeset_templater(ui, repo,
+ False, None, '', False)
+
+ def kwsub(self, mobj, path, node):
+ '''Substitutes keyword using corresponding template.'''
kw = mobj.group(1)
- return '$%s: %s $' % (kw, eval('self.%s()' % kw))
+ 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.'''
+ 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)
+
+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):
+ super(kwfilelog, self).__init__(opener, path)
+ 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):
- from mercurial import filelog, revlog
+ '''Sets up repo as kwrepo for keyword substitution.
+ Overrides file method to return kwfilelog instead of filelog
+ if file matches user configuration.
+ Uses self-initializing pretxncommit-hook 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(repo.ui) == 'archive')
+
+ inc, exc, archive, noarchive = [], ['.hg*'], [], ['.hg*']
+ for pat, opt in repo.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:]
- 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
-
- def iskwcandidate(self, data):
- '''Decides whether to act on keywords.'''
- return (kwfmatches(ui, self._repo, [self._path])
- and not util.binary(data))
+ # only use kwfilelog when needed
+ if self.kwfmatcher(f):
+ kwt = kwtemplater(self.ui, self)
+ return kwfilelog(self.sopener, f, kwt)
+ else:
+ return filelog.filelog(self.sopener, f)
- def read(self, node):
- '''Substitutes keywords when reading filelog.'''
- data = super(kwfilelog, self).read(node)
- if self.iskwcandidate(data):
- kwfctx = kwfilectx(self._repo, self._path,
- fileid=node, filelog=self)
- return re_kw.sub(kwfctx.expand, data)
- return data
+ repo.__class__ = kwrepo
- 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=p1, p2=p2)
-
- 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
# make pretxncommit hook import kwmodule regardless of where it's located
for k, v in sys.modules.iteritems():
if v is None:
@@ -159,11 +274,6 @@
def pretxnkw(ui, repo, hooktype, **args):
'''pretxncommit hook that collects candidates for keyword expansion
on commit and expands keywords in working dir.'''
- from mercurial.i18n import gettext as _
- # above line for backwards compatibility; can be changed to
- # from mercurial.i18n import _
- # some day
- from mercurial import cmdutil, commands
cmd, sysargs, globalopts, cmdopts = commands.parse(ui, sys.argv[1:])[1:]
if repr(cmd).split()[1] in ('tag', 'import_'):
@@ -171,12 +281,9 @@
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)
+ and not os.path.islink(repo.wjoin(f))]
- for f in kwfmatches(ui, repo, modified+added):
- data = repo.wfile(f).read()
- if not util.binary(data):
- kwfctx = kwfilectx(repo, f, changeid=args['node'])
- data, kwct = re_kw.subn(kwfctx.expand, data)
- if kwct:
- ui.debug(_('overwriting %s expanding keywords\n' % f))
- repo.wfile(f, 'w').write(data)
+ if candidates:
+ kwt = kwtemplater(ui, repo)
+ kwt.overwrite(candidates, bin(args['node']))