# HG changeset patch # User Christian Ebert # Date 1170915692 -3600 # Node ID f88ecc1a41faa8a9939a6a4d79e7104e386a3eb3 # Parent 47b45198a30d485bf146ac855771178cabbafc78# Parent 33eb5aa6f6e10018b72a43a28e47e71c83fe99f2 Discard 0.9.3-compat branch diff -r 33eb5aa6f6e1 -r f88ecc1a41fa .hgignore --- 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 diff -r 33eb5aa6f6e1 -r f88ecc1a41fa .hgtags --- 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 diff -r 33eb5aa6f6e1 -r f88ecc1a41fa hgkw/keyword.py --- 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 +# +# 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 +# . +# +# 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) diff -r 33eb5aa6f6e1 -r f88ecc1a41fa hgkw/version.py --- /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) diff -r 33eb5aa6f6e1 -r f88ecc1a41fa setup.py --- /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'], + ) diff -r 33eb5aa6f6e1 -r f88ecc1a41fa tests/test-keyword --- /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 <> $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 ' + +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 <>$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 <> log +firstline +secondline +EOF + +echo % commit +hg --debug commit -l log -d '1 0' -u 'User Name ' + +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 diff -r 33eb5aa6f6e1 -r f88ecc1a41fa tests/test-keyword.out --- /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 : firstline $ +$Id$ +% hg cat +$Id: a 375046bad9d3 Thu, 01 Jan 1970 00:00:01 +0000 user $ +$Xinfo: User Name : firstline $ +$Id$ +% switch off expansion +% cat +$Id: a 375046bad9d3 Thu, 01 Jan 1970 00:00:01 +0000 user $ +$Xinfo: User Name : firstline $ +$Id$ +% hg cat +$Id$ +$Xinfo$ +$Id$ +% update +2 files updated, 0 files merged, 0 files removed, 0 files unresolved +% cat +$Id$ +$Xinfo$ +$Id$