# 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
#
# 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
# keyword = /full/path/to/keyword.py
# # or, if script in hgext folder:
# # 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 gettext as _
# above line for backwards compatibility of standalone version
from mercurial import commands, cmdutil, templater, util
from mercurial import context, filelog, revlog
from mercurial.node import bin
import os.path, 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 getmodulename():
'''Makes sure pretxncommit-hook can import keyword module
regardless of where its located.'''
for k, v in sys.modules.iteritems():
if v is None or not hasattr(v, '__file__'):
continue
if v.__file__.startswith(__file__):
return k
else:
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
return os.path.splitext(os.path.basename(__file__))[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)
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
# configure pretxncommit hook
repo.ui.setconfig('hooks', 'pretxncommit.keyword',
'python:%s.pretxnkw' % getmodulename())
def pretxnkw(ui, repo, hooktype, **args):
'''pretxncommit hook that collects candidates for keyword expansion
on commit and expands keywords in working dir.'''
cmd, sysargs, globalopts, cmdopts = commands.parse(ui, sys.argv[1:])[1:]
if repr(cmd).split()[1] in ('tag', 'import_'):
return
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)]
if not candidates:
return
kwt = kwtemplater(ui, repo)
node = bin(args['node'])
for f in candidates:
data = repo.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))
repo.wfile(f, 'w').write(data)