--- a/hgkw/keyword.py Fri Mar 30 09:08:48 2007 +0200
+++ b/hgkw/keyword.py Wed Jul 18 22:33:24 2007 +0200
@@ -20,9 +20,6 @@
# 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.
#
@@ -37,76 +34,72 @@
'''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 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.
+for the current user or archive distribution.
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}
- ...
+ [extensions]
+ hgext.keyword =
+
+ [keyword]
+ # expand keywords in every python file except those matching "x*"
+ **.py =
+ x* = ignore
+
+Note: the more specific you are in your [keyword] filename patterns
+ the less you lose speed in huge repos.
+
+For a [keywordmaps] template mapping and expansion demonstration
+run "hg kwdemo".
-If no [keywordmaps] are configured the extension falls back on the
-following defaults:
+An additional date template filter {date|utcdate} is provided.
+
+You can replace the default template mappings with customized keywords
+and templates of your choice.
+Again, run "hg kwdemo" to control the results of your config changes.
- 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
+When you change keyword configuration, especially the active keywords,
+and do not want to store expanded keywords in change history, run
+"hg kwshrink", and then change configuration.
+
+Caveat: "hg import" fails if the patch context contains an active
+ keyword. In that case run "hg kwshrink", reimport, and then
+ "hg kwexpand".
+ Or, better, use bundle/unbundle to share changes.
'''
-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
+from mercurial import commands, cmdutil, context, fancyopts
+from mercurial import filelog, localrepo, templater, util, hg
+from mercurial.i18n import gettext as _
+# findcmd might be in cmdutil or commands
+# depending on mercurial version
+if hasattr(cmdutil, 'findcmd'):
+ findcmd = cmdutil.findcmd
+else:
+ findcmd = commands.findcmd
+import os, re, shutil, sys, tempfile, time
+
+commands.optionalrepo += ' kwdemo'
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}',
- }
+ '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}',
+}
+
+nokwcommands = ('add', 'addremove', 'bundle', 'clone', 'copy', 'export',
+ 'incoming', 'outgoing', 'push', 'remove', 'rename', 'rollback')
def utcdate(date):
'''Returns hgdate in cvs-like UTC format.'''
@@ -121,51 +114,85 @@
raise commands.ParseError(None, inst)
if args:
cmd = args[0]
- aliases, i = commands.findcmd(ui, cmd)
+ aliases, i = findcmd(ui, cmd)
return aliases[0]
+def keywordmatcher(ui, repo):
+ '''Collects include/exclude filename patterns for expansion
+ candidates of current configuration. Returns filename matching
+ function if include patterns exist, None otherwise.'''
+ inc, exc = [], ['.hg*']
+ for pat, opt in ui.configitems('keyword'):
+ if opt != 'ignore':
+ inc.append(pat)
+ else:
+ exc.append(pat)
+ if not inc:
+ return None
+ return util.matcher(repo.root, inc=inc, exc=exc)[1]
+
class kwtemplater(object):
'''
Sets up keyword templates, corresponding keyword regex, and
provides keyword substitution functions.
'''
- def __init__(self, ui, repo):
+ def __init__(self, ui, repo, path='', node=None, expand=True):
self.ui = ui
self.repo = repo
+ self.path = path
+ self.node = node
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)
+ 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)
+ escaped = [re.escape(k) for k in self.templates.keys()]
+ self.re_kw = re.compile(r'\$(%s)[^$]*?\$' % '|'.join(escaped))
+ if expand:
+ 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)
+ else:
+ self.t = None
- def kwsub(self, mobj, path, node):
+ def ctxnode(self, node):
+ '''Obtains missing node from file context.'''
+ if not self.node:
+ c = context.filectx(self.repo, self.path, fileid=node)
+ self.node = c.node()
+
+ def kwsub(self, mobj):
'''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)
+ self.t.show(changenode=self.node, root=self.repo.root, file=self.path)
keywordsub = templater.firstline(self.ui.popbuffer())
return '$%s: %s $' % (kw, keywordsub)
- def expand(self, path, node, filelog, data):
- '''Returns data with expanded keywords.'''
+ def expand(self, node, data):
+ '''Returns data with keywords expanded.'''
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)
+ self.ctxnode(node)
+ return self.re_kw.sub(self.kwsub, data)
+
+ def process(self, node, data):
+ '''Returns a tuple: data, count.
+ Count is number of keywords/keyword substitutions.
+ Keywords in data are expanded, if templater was initialized.'''
+ if util.binary(data):
+ return data, None
+ if self.t:
+ self.ctxnode(node)
+ return self.re_kw.subn(self.kwsub, data)
+ return data, self.re_kw.search(data)
def shrink(self, text):
'''Returns text with all keyword substitutions removed.'''
@@ -173,117 +200,228 @@
return text
return self.re_kw.sub(r'$\1$', text)
- def overwrite(self, candidates, node):
- '''Overwrites candidates in working dir expanding keywords.'''
+ def overwrite(self, candidates, man, commit=True):
+ '''Overwrites files in working directory if keywords are detected.
+ Keywords are expanded if keyword templater is initialized,
+ otherwise their substitution is removed.'''
+ expand = self.t is not None
+ action = ('shrinking', 'expanding')[expand]
+ notify = (self.ui.note, self.ui.debug)[commit]
+ 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)
+ fp = self.repo.file(f, kwcnt=True, kwexp=expand)
+ data, cnt = fp.read(man[f])
+ if cnt:
+ notify(_('overwriting %s %s keywords\n') % (f, action))
+ try:
+ self.repo.wwrite(f, data, man.flags(f))
+ except AttributeError:
+ # older versions want file descriptor as 3. optional arg
+ self.repo.wwrite(f, 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.
+ Subclass of filelog to hook into its read, add, cmp methods.
+ Keywords are "stored" unexpanded, and processed on reading.
'''
- def __init__(self, opener, path, kwtemplater):
+ def __init__(self, opener, path, kwtemplater, kwcnt):
super(kwfilelog, self).__init__(opener, path)
- self.path = path
self.kwtemplater = kwtemplater
+ self.kwcnt = kwcnt
def read(self, node):
- '''Substitutes keywords when reading filelog.'''
+ '''Passes data through kwemplater methods for
+ either unconditional keyword expansion
+ or counting of keywords and substitution method
+ set by the calling overwrite function.'''
data = super(kwfilelog, self).read(node)
- return self.kwtemplater.expand(self.path, node, self, data)
+ if not self.kwcnt:
+ return self.kwtemplater.expand(node, data)
+ return self.kwtemplater.process(node, 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)
+ 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)
+ if self.renamed(node):
+ t2 = super(kwfilelog, self).read(node)
+ return t2 != text
return super(kwfilelog, self).cmp(node, text)
+def overwrite(ui, repo, files=None, expand=True):
+ '''Expands/shrinks keywords in working directory.'''
+ wlock = repo.wlock()
+ try:
+ ctx = repo.changectx()
+ if not ctx:
+ raise hg.RepoError(_('no changeset found'))
+ for changed in repo.status()[:4]:
+ if changed:
+ raise util.Abort(_('local changes detected'))
+ kwfmatcher = keywordmatcher(ui, repo)
+ if kwfmatcher is None:
+ ui.warn(_('no files configured for keyword expansion\n'))
+ return
+ m = ctx.manifest()
+ if files:
+ files = [f for f in files if f in m.keys()]
+ else:
+ files = m.keys()
+ files = [f for f in files if kwfmatcher(f) and not os.path.islink(f)]
+ if not files:
+ ui.warn(_('given files not tracked or '
+ 'not configured for expansion\n'))
+ return
+ kwt = kwtemplater(ui, repo, node=ctx.node(), expand=expand)
+ kwt.overwrite(files, m, commit=False)
+ finally:
+ wlock.release()
+
+
+def shrink(ui, repo, *args):
+ '''revert expanded keywords in working directory
+
+ run before:
+ disabling keyword expansion
+ changing keyword expansion configuration
+ or if you experience problems with "hg import"
+ '''
+ overwrite(ui, repo, files=args, expand=False)
+
+def expand(ui, repo, *args):
+ '''expand keywords in working directory
+
+ run after (re)enabling keyword expansion
+ '''
+ overwrite(ui, repo, files=args)
+
+def demo(ui, repo, *args, **opts):
+ '''print [keywordmaps] configuration and an expansion example
+
+ show current, custom, or default keyword template maps and their expansion
+ '''
+ msg = 'hg keyword config and expansion example'
+ kwstatus = 'current'
+ fn = 'demo.txt'
+ tmpdir = tempfile.mkdtemp('', 'kwdemo.')
+ ui.note(_('creating temporary repo at %s\n') % tmpdir)
+ _repo = localrepo.localrepository(ui, path=tmpdir, create=True)
+ # for backwards compatibility
+ ui = _repo.ui
+ ui.setconfig('keyword', fn, '')
+ if opts['default']:
+ kwstatus = 'default'
+ kwmaps = deftemplates
+ else:
+ if args or opts['rcfile']:
+ kwstatus = 'custom'
+ for tmap in args:
+ k, v = tmap.split('=', 1)
+ ui.setconfig('keywordmaps', k.strip(), v.strip())
+ if opts['rcfile']:
+ ui.readconfig(opts['rcfile'])
+ kwmaps = dict(ui.configitems('keywordmaps')) or deftemplates
+ if ui.configitems('keywordmaps'):
+ for k, v in kwmaps.items():
+ ui.setconfig('keywordmaps', k, v)
+ reposetup(ui, _repo)
+ ui.status(_('config with %s keyword template maps:\n') % kwstatus)
+ ui.write('[keyword]\n%s =\n[keywordmaps]\n' % fn)
+ for k, v in kwmaps.items():
+ ui.write('%s = %s\n' % (k, v))
+ path = _repo.wjoin(fn)
+ keywords = '$' + '$\n$'.join(kwmaps.keys()) + '$\n'
+ _repo.wopener(fn, 'w').write(keywords)
+ _repo.add([fn])
+ ui.note(_('\n%s keywords written to %s:\n') % (kwstatus, path))
+ ui.note(keywords)
+ ui.note(_("\nhg --repository '%s' commit\n") % tmpdir)
+ _repo.commit(text=msg)
+ pathinfo = ('', ' in %s' % path)[ui.verbose]
+ ui.status(_('\n%s keywords expanded%s:\n') % (kwstatus, pathinfo))
+ ui.write(_repo.wread(fn))
+ ui.debug(_('\nremoving temporary repo %s\n') % tmpdir)
+ shutil.rmtree(tmpdir)
+
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.
- Uses self-initializing pretxncommit-hook to overwrite configured files with
- updated keyword substitutions.
+ 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():
+ # for backwards compatibility
+ ui = repo.ui
+
+ if not repo.local() or getcmd(ui) in nokwcommands:
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:
+ kwfmatcher = keywordmatcher(ui, repo)
+ if kwfmatcher is None:
return
- repo.kwfmatcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
-
class kwrepo(repo.__class__):
- def file(self, f):
+ def file(self, f, kwcnt=False, kwexp=True):
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)
+ if kwfmatcher(f):
+ kwt = kwtemplater(ui, self, path=f, expand=kwexp)
+ return kwfilelog(self.sopener, f, kwt, kwcnt)
else:
return filelog.filelog(self.sopener, f)
+ 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={}):
+ wrelease = False
+ if not wlock:
+ wlock = self.wlock()
+ wrelease = True
+ try:
+ removed = self.status(node1=p1, node2=p2, files=files,
+ match=match, wlock=wlock)[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
+
+ cl = self.changelog.read(node)
+ candidates = [f for f in cl[3] if kwfmatcher(f)
+ and f not in removed
+ and not os.path.islink(self.wjoin(f))]
+ if candidates:
+ m = self.manifest.read(cl[0])
+ kwt = kwtemplater(ui, self, node=node)
+ kwt.overwrite(candidates, m)
+ return node
+ finally:
+ if wrelease:
+ wlock.release()
+
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:
- continue
- if not hasattr(v, '__file__'):
- continue
- if v.__file__.startswith(__file__):
- mod = k
- break
- else:
- sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
- mod = os.path.splitext(os.path.basename(__file__))[0]
- ui.setconfig('hooks', 'pretxncommit.keyword', 'python:%s.pretxnkw' % mod)
- del mod
-
-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)
- and not os.path.islink(repo.wjoin(f))]
-
- if candidates:
- kwt = kwtemplater(ui, repo)
- kwt.overwrite(candidates, bin(args['node']))
+cmdtable = {
+ 'kwdemo':
+ (demo,
+ [('d', 'default', None, _('show default keyword template maps')),
+ ('f', 'rcfile', [], _('read maps from RCFILE'))],
+ _('hg kwdemo [-d || [-f RCFILE] TEMPLATEMAP ...]')),
+ 'kwshrink': (shrink, [], _('hg kwshrink [NAME] ...')),
+ 'kwexpand': (expand, [], _('hg kwexpand [NAME] ...')),
+}