Discard 0.9.3-compat branch kwmap-templates
authorChristian Ebert <blacktrash@gmx.net>
Thu, 08 Feb 2007 07:21:32 +0100
branchkwmap-templates
changeset 127 f88ecc1a41fa
parent 126 47b45198a30d (diff)
parent 79 33eb5aa6f6e1 (current diff)
child 128 fe37939db543
Discard 0.9.3-compat branch
--- a/.hgignore	Mon Jan 08 13:07:52 2007 +0100
+++ b/.hgignore	Thu Feb 08 07:21:32 2007 +0100
@@ -1,7 +1,10 @@
 syntax: glob
 
 *.pyc
+*.pyo
 *~
 *.swp
 *.orig
 *.rej
+
+hgkw/__version__.py
--- a/.hgtags	Mon Jan 08 13:07:52 2007 +0100
+++ b/.hgtags	Thu Feb 08 07:21:32 2007 +0100
@@ -1,4 +1,5 @@
 536c1797202d57efb77bea098e10968ff01602ce universal_scheme
 ba000e29ecf3b8df09e0fd363a78cabbe3c2a78f cvs_scheme
 1fe48bf82d056f1ece05baccab888357c10c5ab8 r0.1
-4c5d9635b5170a15251256aa14d874df520949e5 pure_extension
+2e930f84224222ad6514a3c5dc6e00350e199e92 very_cvs
+99dc49c5bcfba9d5b412c5fa6d0bf3ba20d68df1 hgkw_standalone_setup
--- 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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgkw/version.py	Thu Feb 08 07:21:32 2007 +0100
@@ -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	Thu Feb 08 07:21:32 2007 +0100
@@ -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	Thu Feb 08 07:21:32 2007 +0100
@@ -0,0 +1,75 @@
+#!/bin/sh
+
+cat <<EOF >> $HGRCPATH
+[extensions]
+hgext.keyword =
+[keyword]
+* =
+b* = ignore
+EOF
+
+echo % help
+hg help keyword
+
+hg init a
+cd a
+echo '$Id$' > a
+echo '$Id$' > b
+echo % cat
+cat a b
+
+echo % default keyword expansion
+echo % commit
+hg --debug commit -A -m ab -d '0 0' -u 'User Name <user@example.com>'
+
+echo % cat
+cat a b
+echo % hg cat
+hg cat a b
+
+rm a b
+echo % update
+hg update
+echo % cat
+cat a b
+
+echo % custom keyword expansion
+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 % commit
+hg --debug commit -l log -d '1 0' -u 'User Name <user@example.com>'
+
+echo % cat
+cat a b
+echo % hg cat
+hg cat a b
+
+echo % switch off expansion
+rm $HGRCPATH
+
+echo % cat
+cat a b
+echo % hg cat
+hg cat a b
+
+echo % update
+rm a b
+hg update
+
+echo % cat
+cat a b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-keyword.out	Thu Feb 08 07:21:32 2007 +0100
@@ -0,0 +1,73 @@
+% 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 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.
+
+no commands defined
+% cat
+$Id$
+$Id$
+% default keyword expansion
+% commit
+adding a
+adding b
+a
+b
+calling hook pretxncommit.keyword: hgext.keyword.pretxnkw
+overwriting a expanding keywords
+% cat
+$Id: a,v b803250b3164 1970/01/01 00:00:00 user $
+$Id$
+% hg cat
+$Id: a,v b803250b3164 1970/01/01 00:00:00 user $
+$Id$
+% update
+2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% cat
+$Id: a,v b803250b3164 1970/01/01 00:00:00 user $
+$Id$
+% custom keyword expansion
+% cat
+$Id: a,v b803250b3164 1970/01/01 00:00:00 user $
+$Id$
+% hg cat
+$Id: a b803250b3164 Thu, 01 Jan 1970 00:00:00 +0000 user $
+$Id$
+% commit
+a
+calling hook pretxncommit.keyword: hgext.keyword.pretxnkw
+overwriting a expanding keywords
+% cat
+$Id: a 375046bad9d3 Thu, 01 Jan 1970 00:00:01 +0000 user $
+$Xinfo: User Name <user@example.com>: firstline $
+$Id$
+% hg cat
+$Id: a 375046bad9d3 Thu, 01 Jan 1970 00:00:01 +0000 user $
+$Xinfo: User Name <user@example.com>: firstline $
+$Id$
+% switch off expansion
+% cat
+$Id: a 375046bad9d3 Thu, 01 Jan 1970 00:00:01 +0000 user $
+$Xinfo: User Name <user@example.com>: firstline $
+$Id$
+% hg cat
+$Id$
+$Xinfo$
+$Id$
+% update
+2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% cat
+$Id$
+$Xinfo$
+$Id$