Discard self_initializing_hook branch kwmap-templates
authorChristian Ebert <blacktrash@gmx.net>
Wed, 18 Jul 2007 22:33:24 +0200
branchkwmap-templates
changeset 192 0d2a6c9f8343
parent 191 4c3775e904b6 (diff)
parent 157 64dce6787d82 (current diff)
child 193 7a775a8f6fb9
Discard self_initializing_hook branch
hgkw/keyword.py
--- a/.hgignore	Fri Mar 30 09:08:48 2007 +0200
+++ b/.hgignore	Wed Jul 18 22:33:24 2007 +0200
@@ -1,7 +1,10 @@
 syntax: glob
 
 *.pyc
+*.pyo
 *~
 *.swp
 *.orig
 *.rej
+
+hgkw/__version__.py
--- a/.hgtags	Fri Mar 30 09:08:48 2007 +0200
+++ b/.hgtags	Wed Jul 18 22:33:24 2007 +0200
@@ -2,3 +2,6 @@
 ba000e29ecf3b8df09e0fd363a78cabbe3c2a78f cvs_scheme
 1fe48bf82d056f1ece05baccab888357c10c5ab8 r0.1
 2e930f84224222ad6514a3c5dc6e00350e199e92 very_cvs
+99dc49c5bcfba9d5b412c5fa6d0bf3ba20d68df1 hgkw_standalone_setup
+15e8cd7f5295728b089fc8ba236c0f079572fb1d nohook
+0c8b7e5c25a6b9a0d2782eaa3748eb546f5a254f archive
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MANIFEST.in	Wed Jul 18 22:33:24 2007 +0200
@@ -0,0 +1,5 @@
+# $Id$
+
+exclude hgkw/__version__.py
+
+include tests/test-keyword tests/test-keyword.out
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README.txt	Wed Jul 18 22:33:24 2007 +0200
@@ -0,0 +1,45 @@
+$Id$
+
+keyword extension for Mercurial SCM
+===================================
+
+install
+-------
+
+Either copy hgkw/keyword.py into the hgext directory of your
+Mercurial installation.
+Then add the lines:
+
+[extensions]
+hgext.keyword =
+
+to your hgrc file.
+
+Or run "python setup.py install".
+See also "pyton setup.py --help".
+Then add the line:
+
+[extensions]
+keyword = /path/to/hgkw/keyword.py
+
+to your hgrc, where /path/to/ is somewhere in your $PYTHONPATH.
+
+
+first steps and online help
+---------------------------
+
+$ hg keyword help
+$ hg kwdemo
+
+
+testing
+-------
+
+Copy hgkw/keyword.py into the hgext directory of your Mercurial
+source tree. Copy tests/test-keyword, tests/test-keyword.out into
+the tests directory of your Mercurial source tree. Change to that
+directory and run:
+
+$ python run-tests.py test-keyword
+
+and then keep your fingers crossed ...
--- 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] ...')),
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgkw/version.py	Wed Jul 18 22:33:24 2007 +0200
@@ -0,0 +1,46 @@
+# $Id$
+
+'''version.py - hgkw version.
+Code stolen from Mercurial, and simplified for my needs.
+'''
+
+import os, time
+
+unknown_version = 'unknown'
+
+def getversion(doreload=False):
+    try:
+        import hgkw.__version__
+        if doreload:
+            reload(hgkw.__version__)
+        version = hgkw.__version__.version
+    except ImportError:
+        version = unknown_version
+    return version
+
+def rememberversion(version=None):
+    if not version and os.path.isdir('.hg'):
+        # get version from Mercurial
+        p = os.popen('hg --quiet identify 2> %s' % os.devnull)
+        ident = p.read()[:-1]
+        if not p.close() and ident:
+            if ident[-1] != '+':
+                version = ident
+            else:
+                version = ident[:-1]
+                version += time.strftime('+%Y%m%d')
+    if version and version != getversion(): # write version
+        directory = os.path.dirname(__file__)
+        for suff in ['py', 'pyc', 'pyo']:
+            try:
+                os.unlink(os.path.join(directory, '__version__.%s' % suff))
+            except OSError:
+                pass
+        f = open(os.path.join(directory, '__version__.py'), 'w')
+        try:
+            f.write('# this file is auto-generated\n')
+            f.write('version = %r\n' % version)
+        finally:
+            f.close()
+        # reload file
+        getversion(doreload=True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py	Wed Jul 18 22:33:24 2007 +0200
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+# $Id$
+
+from distutils.core import setup
+import hgkw.version
+
+# specify version, Mercurial version otherwise
+version = ''
+
+hgkw.version.rememberversion(version)
+
+setup(name='hgkw',
+        version=hgkw.version.getversion(),
+        description='Mercurial keyword extension (standalone)',
+        author='Christian Ebert',
+        author_email='blacktrash@gmx.net',
+        license='GNU GPL',
+        packages=['hgkw'],
+        )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-keyword	Wed Jul 18 22:33:24 2007 +0200
@@ -0,0 +1,133 @@
+#!/bin/sh
+
+cat <<EOF >> $HGRCPATH
+[extensions]
+hgext.keyword =
+[keyword]
+* =
+b = ignore
+EOF
+
+echo % help
+hg help keyword
+
+echo % hg kwdemo
+hg --quiet kwdemo --default \
+| sed -e 's![^ ][^ ]*demo.txt,v!/TMP/demo.txt,v!' \
+ -e 's/,v [a-z0-9][a-z0-9]* /,v xxxxxxxxxxxx /' \
+ -e '/[$]Revision/ s/: [a-z0-9][a-z0-9]* /: xxxxxxxxxxxx /' \
+ -e 's! 20[0-9][0-9]/[01][0-9]/[0-3][0-9] [0-2][0-9]:[0-6][0-9]:[0-6][0-9]! 2000/00/00 00:00:00!'
+
+hg init Test
+cd Test
+
+echo % kwshrink should abort in empty/invalid repo
+hg kwshrink
+
+echo 'expand $Id$' > a
+echo 'ignore $Id$' > b
+echo % cat
+cat a b
+
+echo % default keyword expansion
+echo % commit
+hg --debug commit -A -mab -d '0 0' -u 'User Name <user@example.com>'
+echo % status
+hg status
+echo % identify
+hg --quiet identify
+echo % cat
+cat a b
+echo % hg cat
+hg cat a b
+
+echo % touch
+touch a b
+echo % status
+hg status
+
+rm a b
+echo % update
+hg update
+echo % cat
+cat a b
+
+echo % copy
+hg cp a c
+echo % commit
+hg --debug commit -ma2c -d '1 0' -u 'User Name <user@example.com>'
+echo % cat a c
+cat a c
+echo % touch copied c
+touch c
+echo % status
+
+echo % rollback
+hg rollback
+echo % status
+hg status
+echo % update -C
+hg update --clean
+
+echo % custom keyword expansion
+echo % try with kwdemo
+hg --quiet kwdemo "Xinfo = {author}: {desc}"
+
+cat <<EOF >>$HGRCPATH
+[keywordmaps]
+Id = {file} {node|short} {date|rfc822date} {author|user}
+Xinfo = {author}: {desc}
+EOF
+
+echo % cat
+cat a b
+echo % hg cat
+hg cat a b
+
+echo '$Xinfo$' >> a
+cat <<EOF >> log
+firstline
+secondline
+EOF
+
+echo % interrupted commit
+HGEDITOR=false hg commit
+echo % status
+hg status
+
+echo % commit
+hg --debug commit -l log -d '2 0' -u 'User Name <user@example.com>'
+rm log
+echo % status
+hg status
+
+echo % cat
+cat a b
+echo % hg cat
+hg cat a b
+
+cd ..
+hg clone -r0 Test Test-a
+cd Test-a
+cat <<EOF >> .hg/hgrc
+[paths]
+default = ../Test
+EOF
+echo % incoming
+# remove path to temp dir
+hg incoming | sed -e 's/^\(comparing with \).*\(test-keyword.*\)/\1\2/'
+
+echo % switch off expansion
+cd ../Test
+echo % kwshrink
+hg --debug kwshrink
+echo % cat
+cat a b
+echo % hg cat
+hg cat a b
+
+rm $HGRCPATH
+echo % cat
+cat a b
+echo % hg cat
+hg cat a b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-keyword.out	Wed Jul 18 22:33:24 2007 +0200
@@ -0,0 +1,175 @@
+% help
+keyword extension - 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 or archive distribution.
+
+Configuration is done in the [keyword] and [keywordmaps] sections of
+hgrc files.
+
+Example:
+    [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".
+
+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.
+
+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.
+
+list of commands (use "hg help -v keyword" to show aliases and global options):
+
+ kwdemo     print [keywordmaps] configuration and an expansion example
+ kwexpand   expand keywords in working directory
+ kwshrink   revert expanded keywords in working directory
+% hg kwdemo
+[keyword]
+demo.txt =
+[keywordmaps]
+RCSFile = {file|basename},v
+Author = {author|user}
+Header = {root}/{file},v {node|short} {date|utcdate} {author|user}
+Source = {root}/{file},v
+Date = {date|utcdate}
+Id = {file|basename},v {node|short} {date|utcdate} {author|user}
+Revision = {node|short}
+$RCSFile: demo.txt,v $
+$Author: test $
+$Header: /TMP/demo.txt,v xxxxxxxxxxxx 2000/00/00 00:00:00 test $
+$Source: /TMP/demo.txt,v $
+$Date: 2000/00/00 00:00:00 $
+$Id: demo.txt,v xxxxxxxxxxxx 2000/00/00 00:00:00 test $
+$Revision: xxxxxxxxxxxx $
+% kwshrink should abort in empty/invalid repo
+abort: no changeset found!
+% cat
+expand $Id$
+ignore $Id$
+% default keyword expansion
+% commit
+adding a
+adding b
+a
+b
+overwriting a expanding keywords
+% status
+% identify
+65cbcc9534b0
+% cat
+expand $Id: a,v 65cbcc9534b0 1970/01/01 00:00:00 user $
+ignore $Id$
+% hg cat
+expand $Id: a,v 65cbcc9534b0 1970/01/01 00:00:00 user $
+ignore $Id$
+% touch
+% status
+% update
+2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% cat
+expand $Id: a,v 65cbcc9534b0 1970/01/01 00:00:00 user $
+ignore $Id$
+% copy
+% commit
+c
+ c: copy a:e6cc15c9eb5fd3c09ec691b667cf6ccd6dfb936e
+overwriting c expanding keywords
+% cat a c
+expand $Id: a,v 65cbcc9534b0 1970/01/01 00:00:00 user $
+expand $Id: c,v 9460ba56f8d0 1970/01/01 00:00:01 user $
+% touch copied c
+% status
+% rollback
+rolling back last transaction
+% status
+A c
+% update -C
+0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+% custom keyword expansion
+% try with kwdemo
+[keyword]
+demo.txt =
+[keywordmaps]
+Xinfo = {author}: {desc}
+$Xinfo: test: hg keyword config and expansion example $
+% cat
+expand $Id: a,v 65cbcc9534b0 1970/01/01 00:00:00 user $
+ignore $Id$
+% hg cat
+expand $Id: a 65cbcc9534b0 Thu, 01 Jan 1970 00:00:00 +0000 user $
+ignore $Id$
+% interrupted commit
+abort: edit failed: false exited with status 1
+transaction abort!
+rollback completed
+% status
+M a
+? log
+% commit
+a
+overwriting a expanding keywords
+% status
+% cat
+expand $Id: a 6ade9dd7b017 Thu, 01 Jan 1970 00:00:02 +0000 user $
+$Xinfo: User Name <user@example.com>: firstline $
+ignore $Id$
+% hg cat
+expand $Id: a 6ade9dd7b017 Thu, 01 Jan 1970 00:00:02 +0000 user $
+$Xinfo: User Name <user@example.com>: firstline $
+ignore $Id$
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 2 changes to 2 files
+2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% incoming
+searching for changes
+changeset:   1:6ade9dd7b017
+tag:         tip
+user:        User Name <user@example.com>
+date:        Thu Jan 01 00:00:02 1970 +0000
+summary:     firstline
+
+% switch off expansion
+% kwshrink
+overwriting a shrinking keywords
+% cat
+expand $Id$
+$Xinfo$
+ignore $Id$
+% hg cat
+expand $Id: a 6ade9dd7b017 Thu, 01 Jan 1970 00:00:02 +0000 user $
+$Xinfo: User Name <user@example.com>: firstline $
+ignore $Id$
+% cat
+expand $Id$
+$Xinfo$
+ignore $Id$
+% hg cat
+expand $Id$
+$Xinfo$
+ignore $Id$