--- a/hgkw/keyword.py Mon Jan 08 13:07:52 2007 +0100
+++ b/hgkw/keyword.py Thu Feb 08 07:21:32 2007 +0100
@@ -1,246 +1,158 @@
-# 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
+#
+# 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 hack against the grain of a DSCM
+'''keyword expansion in local repositories
+
+This extension expands RCS/CVS-like or self-customized keywords in
+the text files selected by your configuration.
-This extension lets you expand RCS/CVS-like keywords in a Mercurial
+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.
-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:
+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
- # 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
- ...
-'''
+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}',
+ }
-from mercurial.node import *
-from mercurial.i18n import gettext as _
-from mercurial import context, filelog, revlog, util
-import os.path, re
-
-
-re_kw = re.compile(
- r'\$(Id|Header|Author|Date|Revision|RCSFile|Source)[^$]*?\$')
-
+def utcdate(date):
+ '''Returns hgdate in cvs-like UTC format.'''
+ return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
-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 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]
-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
+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={}):
-
- 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()
- for f in commit:
- ui.note(f + "\n")
- try:
- new[f] = self.filecommit(f, m1, m2, linkrev, tr, changed)
- m1.set(f, util.is_exec(self.wjoin(f), m1.execf(f)))
- except IOError:
- if use_dirstate:
- ui.warn(_("trouble committing %s!\n") % f)
- raise
- else:
- remove.append(f)
-
- # update manifest
- m1.update(new)
- remove.sort()
-
- for f in remove:
- if f in m1:
- del m1[f]
- mn = self.manifest.add(m1, tr, linkrev, c1[0], c2[0], (new, remove))
-
- # 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 remove])
- 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 + remove, 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):
'''
Superclass over filelog to customize it's read, add, cmp methods.
@@ -251,33 +163,66 @@
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 (kwfmatches(ui, self._repo, [self._path])
- and not util.binary(data))
+ 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):
- return re_kw.sub(lambda m:
- kwexpand(m, self._repo, self._path,
- fileid=node, filelog=self), 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 = re_kw.sub(r'$\1$', text)
+ text = self.kwt.re_kw.sub(r'$\1$', text)
return super(kwfilelog, self).add(text,
- meta, tr, link, p1=None, p2=None)
+ 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)
+ 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)