# HG changeset patch # User Christian Ebert # Date 1175238528 -7200 # Node ID 64dce6787d829d963e343b6465786fe2384fcc44 # Parent 121da9c0a32533fa46e8aad13a2bbe132fae650c Incorporate changes in self_initializing_hook branch Implement configurable expansion based on Mercurial templates. NOTE: Relying on pretxncommit-hook to write in working directory might break in the future as it relies on a buggy race condidition. [issue273] diff -r 121da9c0a325 -r 64dce6787d82 hgkw/keyword.py --- 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 +# +# 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 +# . +# +# 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']))