--- a/hgkw/keyword.py Sat Aug 14 09:30:42 2010 +0100
+++ b/hgkw/keyword.py Sat Aug 14 09:42:14 2010 +0100
@@ -35,8 +35,8 @@
change history. The mechanism can be regarded as a convenience for the
current user or for archive distribution.
-Configuration is done in the [keyword] and [keywordmaps] sections of
-hgrc files.
+Configuration is done in the [keyword], [keywordset] and [keywordmaps]
+sections of hgrc files.
Example::
@@ -45,6 +45,10 @@
**.py =
x* = ignore
+ [keywordset]
+ # prefer svn- over cvs-like default keywordmaps
+ svn = True
+
NOTE: the more specific you are in your filename patterns the less you
lose speed in huge repositories.
@@ -52,8 +56,11 @@
control run :hg:`kwdemo`. See :hg:`help templates` for a list of
available templates and filters.
-An additional date template filter {date|utcdate} is provided. It
-returns a date like "2006/09/18 15:13:13".
+Three additional date template filters are provided::
+
+ utcdate "2006/09/18 15:13:13"
+ svnutcdate "2006-09-18 15:13:13Z"
+ svnisodate "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
The default template mappings (view with :hg:`kwdemo -d`) can be
replaced with customized keywords and templates. Again, run
@@ -74,7 +81,6 @@
from mercurial import commands, cmdutil, dispatch, filelog, revlog, extensions
from mercurial import patch, localrepo, templater, templatefilters, util, match
from mercurial.hgweb import webcommands
-from mercurial.node import nullid
from mercurial.i18n import _
import re, shutil, tempfile
@@ -94,21 +100,24 @@
# names of extensions using dorecord
recordextensions = 'record'
-# provide cvs-like UTC date filter
+# date like in cvs' $Date
utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S')
+# date like in svn's $Date
+svnisodate = lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
+# date like in svn's $Id
+svnutcdate = lambda x: util.datestr((x[0], 0), '%Y-%m-%d %H:%M:%SZ')
# make keyword tools accessible
-kwtools = {'templater': None, 'hgcmd': '', 'inc': [], 'exc': ['.hg*']}
+kwtools = {'templater': None, 'hgcmd': ''}
-class kwtemplater(object):
- '''
- Sets up keyword templates, corresponding keyword regex, and
- provides keyword substitution functions.
- '''
+def _defaultkwmaps(ui):
+ '''Returns default keywordmaps according to keywordset configuration.'''
templates = {
'Revision': '{node|short}',
'Author': '{author|user}',
+ }
+ kwsets = ({
'Date': '{date|utcdate}',
'RCSfile': '{file|basename},v',
'RCSFile': '{file|basename},v', # kept for backwards compatibility
@@ -116,13 +125,26 @@
'Source': '{root}/{file},v',
'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
- }
+ }, {
+ 'Date': '{date|svnisodate}',
+ 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
+ 'LastChangedRevision': '{node|short}',
+ 'LastChangedBy': '{author|user}',
+ 'LastChangedDate': '{date|svnisodate}',
+ })
+ templates.update(kwsets[ui.configbool('keywordset', 'svn')])
+ return templates
- def __init__(self, ui, repo):
+class kwtemplater(object):
+ '''
+ Sets up keyword templates, corresponding keyword regex, and
+ provides keyword substitution functions.
+ '''
+
+ def __init__(self, ui, repo, inc, exc):
self.ui = ui
self.repo = repo
- self.match = match.match(repo.root, '', [],
- kwtools['inc'], kwtools['exc'])
+ self.match = match.match(repo.root, '', [], inc, exc)
self.restrict = kwtools['hgcmd'] in restricted.split()
self.record = kwtools['hgcmd'] in recordcommands.split()
@@ -130,11 +152,15 @@
if kwmaps: # override default templates
self.templates = dict((k, templater.parsestring(v, False))
for k, v in kwmaps)
+ else:
+ self.templates = _defaultkwmaps(self.ui)
escaped = map(re.escape, self.templates.keys())
kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
self.re_kw = re.compile(kwpat)
- templatefilters.filters['utcdate'] = utcdate
+ templatefilters.filters.update({'utcdate': utcdate,
+ 'svnisodate': svnisodate,
+ 'svnutcdate': svnutcdate})
def substitute(self, data, path, ctx, subfunc):
'''Replaces keywords in data with expanded template.'''
@@ -162,15 +188,14 @@
Caveat: localrepository._link fails on Windows.'''
return self.match(path) and not 'l' in flagfunc(path)
- def overwrite(self, node, expand, candidates):
+ def overwrite(self, ctx, candidates, iswctx, expand):
'''Overwrites selected files expanding/shrinking keywords.'''
- ctx = self.repo[node]
- mf = ctx.manifest()
- if node is not None: # commit, record
- candidates = [f for f in ctx.files() if f in mf]
+ if self.record:
+ candidates = [f for f in ctx.files() if f in ctx]
candidates = [f for f in candidates if self.iskwfile(f, ctx.flags)]
if candidates:
- self.restrict = True # do not expand when reading
+ self.restrict = True # do not expand when reading
+ mf = ctx.manifest()
msg = (expand and _('overwriting %s expanding keywords\n')
or _('overwriting %s shrinking keywords\n'))
for f in candidates:
@@ -181,7 +206,7 @@
if util.binary(data):
continue
if expand:
- if node is None:
+ if iswctx:
ctx = self.repo.filectx(f, fileid=mf[f]).changectx()
data, found = self.substitute(data, f, ctx,
self.re_kw.subn)
@@ -190,8 +215,10 @@
if found:
self.ui.note(msg % f)
self.repo.wwrite(f, data, mf.flags(f))
- if node is None:
+ if iswctx:
self.repo.dirstate.normal(f)
+ elif self.record:
+ self.repo.dirstate.normallookup(f)
self.restrict = False
def shrinktext(self, text):
@@ -257,7 +284,8 @@
def _kwfwrite(ui, repo, expand, *pats, **opts):
'''Selects files and passes them to kwtemplater.overwrite.'''
- if repo.dirstate.parents()[1] != nullid:
+ wctx = repo[None]
+ if len(wctx.parents()) > 1:
raise util.Abort(_('outstanding uncommitted merge'))
kwt = kwtools['templater']
wlock = repo.wlock()
@@ -266,7 +294,7 @@
modified, added, removed, deleted, unknown, ignored, clean = status
if modified or added or removed or deleted:
raise util.Abort(_('outstanding uncommitted changes'))
- kwt.overwrite(None, expand, clean)
+ kwt.overwrite(wctx, clean, True, expand)
finally:
wlock.release()
@@ -281,7 +309,7 @@
Use -d/--default to disable current configuration.
- See "hg help templates" for information on templates and filters.
+ See :hg:`help templates` for information on templates and filters.
'''
def demoitems(section, items):
ui.write('[%s]\n' % section)
@@ -313,14 +341,14 @@
kwmaps = dict(ui.configitems('keywordmaps'))
elif opts.get('default'):
ui.status(_('\n\tconfiguration using default keyword template maps\n'))
- kwmaps = kwtemplater.templates
+ kwmaps = _defaultkwmaps(ui)
if uikwmaps:
ui.status(_('\tdisabling current template maps\n'))
for k, v in kwmaps.iteritems():
ui.setconfig('keywordmaps', k, v)
else:
ui.status(_('\n\tconfiguration using current keyword template maps\n'))
- kwmaps = dict(uikwmaps) or kwtemplater.templates
+ kwmaps = dict(uikwmaps) or _defaultkwmaps(ui)
uisetup(ui)
reposetup(ui, repo)
@@ -329,7 +357,7 @@
demoitems('keywordmaps', kwmaps.iteritems())
keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
repo.wopener(fn, 'w').write(keywords)
- repo.add([fn])
+ repo[None].add([fn])
ui.note(_('\nkeywords written to %s:\n') % fn)
ui.note(keywords)
repo.dirstate.setbranch('demobranch')
@@ -409,23 +437,15 @@
def uisetup(ui):
- '''Collects [keyword] config in kwtools.
- Monkeypatches dispatch._parse if needed.'''
-
- for pat, opt in ui.configitems('keyword'):
- if opt != 'ignore':
- kwtools['inc'].append(pat)
- else:
- kwtools['exc'].append(pat)
+ ''' Monkeypatches dispatch._parse to retrieve user command.'''
- if kwtools['inc']:
- def kwdispatch_parse(orig, ui, args):
- '''Monkeypatch dispatch._parse to obtain running hg command.'''
- cmd, func, args, options, cmdoptions = orig(ui, args)
- kwtools['hgcmd'] = cmd
- return cmd, func, args, options, cmdoptions
+ def kwdispatch_parse(orig, ui, args):
+ '''Monkeypatch dispatch._parse to obtain running hg command.'''
+ cmd, func, args, options, cmdoptions = orig(ui, args)
+ kwtools['hgcmd'] = cmd
+ return cmd, func, args, options, cmdoptions
- extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
+ extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
def reposetup(ui, repo):
'''Sets up repo as kwrepo for keyword substitution.
@@ -436,15 +456,23 @@
Monkeypatches patch and webcommands.'''
try:
- if (not repo.local() or not kwtools['inc']
- or kwtools['hgcmd'] in nokwcommands.split()
+ if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
or '.hg' in util.splitpath(repo.root)
or repo._url.startswith('bundle:')):
return
except AttributeError:
pass
- kwtools['templater'] = kwt = kwtemplater(ui, repo)
+ inc, exc = [], ['.hg*']
+ for pat, opt in ui.configitems('keyword'):
+ if opt != 'ignore':
+ inc.append(pat)
+ else:
+ exc.append(pat)
+ if not inc:
+ return
+
+ kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
class kwrepo(repo.__class__):
def file(self, f):
@@ -469,7 +497,8 @@
n = super(kwrepo, self).commitctx(ctx, error)
# no lock needed, only called from repo.commit() which already locks
if not kwt.record:
- kwt.overwrite(n, True, None)
+ kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
+ False, True)
return n
# monkeypatches
@@ -504,8 +533,9 @@
# therefore compare nodes before and after
ctx = repo['.']
ret = orig(ui, repo, commitfunc, *pats, **opts)
- if ctx != repo['.']:
- kwt.overwrite('.', True, None)
+ recordctx = repo['.']
+ if ctx != recordctx:
+ kwt.overwrite(recordctx, None, False, True)
return ret
finally:
wlock.release()
@@ -528,7 +558,8 @@
'kwdemo':
(demo,
[('d', 'default', None, _('show default keyword template maps')),
- ('f', 'rcfile', '', _('read maps from rcfile'))],
+ ('f', 'rcfile', '',
+ _('read maps from rcfile'), _('FILE'))],
_('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
'kwexpand': (expand, commands.walkopts,
_('hg kwexpand [OPTION]... [FILE]...')),
--- a/tests/run-tests.py Sat Aug 14 09:30:42 2010 +0100
+++ b/tests/run-tests.py Sat Aug 14 09:42:14 2010 +0100
@@ -52,6 +52,7 @@
import sys
import tempfile
import time
+import re
closefds = os.name == 'posix'
def Popen4(cmd, bufsize=-1):
@@ -441,6 +442,94 @@
def alarmed(signum, frame):
raise Timeout
+def pytest(test, options):
+ py3kswitch = options.py3k_warnings and ' -3' or ''
+ cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test)
+ vlog("# Running", cmd)
+ return run(cmd, options)
+
+def shtest(test, options):
+ cmd = '"%s"' % test
+ vlog("# Running", cmd)
+ return run(cmd, options)
+
+def battest(test, options):
+ # To reliably get the error code from batch files on WinXP,
+ # the "cmd /c call" prefix is needed. Grrr
+ cmd = 'cmd /c call "%s"' % testpath
+ vlog("# Running", cmd)
+ return run(cmd, options)
+
+def tsttest(test, options):
+ t = open(test)
+ out = []
+ script = []
+ salt = "SALT" + str(time.time())
+
+ pos = prepos = -1
+ after = {}
+ expected = {}
+ for n, l in enumerate(t):
+ if l.startswith(' $ '): # commands
+ after.setdefault(pos, []).append(l)
+ prepos = pos
+ pos = n
+ script.append('echo %s %s\n' % (salt, n))
+ script.append(l[4:])
+ elif l.startswith(' > '): # continuations
+ after.setdefault(prepos, []).append(l)
+ script.append(l[4:])
+ elif l.startswith(' '): # results
+ # queue up a list of expected results
+ expected.setdefault(pos, []).append(l[2:])
+ else:
+ # non-command/result - queue up for merged output
+ after.setdefault(pos, []).append(l)
+
+ fd, name = tempfile.mkstemp(suffix='hg-tst')
+
+ try:
+ for l in script:
+ os.write(fd, l)
+ os.close(fd)
+
+ cmd = '/bin/sh "%s"' % name
+ vlog("# Running", cmd)
+ exitcode, output = run(cmd, options)
+ finally:
+ os.remove(name)
+
+ def rematch(el, l):
+ try:
+ return re.match(el, l)
+ except re.error:
+ # el is an invalid regex
+ return False
+
+ pos = -1
+ postout = []
+ for n, l in enumerate(output):
+ if l.startswith(salt):
+ if pos in after:
+ postout += after.pop(pos)
+ pos = int(l.split()[1])
+ else:
+ el = None
+ if pos in expected and expected[pos]:
+ el = expected[pos].pop(0)
+
+ if el == l: # perfect match (fast)
+ postout.append(" " + l)
+ elif el and rematch(el, l): # fallback regex match
+ postout.append(" " + el)
+ else: # mismatch - let diff deal with it
+ postout.append(" " + l)
+
+ if pos in after:
+ postout += after.pop(pos)
+
+ return exitcode, postout
+
def run(cmd, options):
"""Run command in a sub-process, capturing the output (stdout and stderr).
Return a tuple (exitcode, output). output is None in debug mode."""
@@ -537,15 +626,15 @@
lctest = test.lower()
if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
- py3kswitch = options.py3k_warnings and ' -3' or ''
- cmd = '%s%s "%s"' % (PYTHON, py3kswitch, testpath)
+ runner = pytest
elif lctest.endswith('.bat'):
# do not run batch scripts on non-windows
if os.name != 'nt':
return skip("batch script")
- # To reliably get the error code from batch files on WinXP,
- # the "cmd /c call" prefix is needed. Grrr
- cmd = 'cmd /c call "%s"' % testpath
+ runner = battest
+ elif lctest.endswith('.t'):
+ runner = tsttest
+ ref = testpath
else:
# do not run shell scripts on windows
if os.name == 'nt':
@@ -555,7 +644,7 @@
return fail("does not exist")
elif not os.access(testpath, os.X_OK):
return skip("not executable")
- cmd = '"%s"' % testpath
+ runner = shtest
# Make a tmp subdirectory to work in
tmpd = os.path.join(HGTMP, test)
@@ -565,8 +654,7 @@
if options.timeout > 0:
signal.alarm(options.timeout)
- vlog("# Running", cmd)
- ret, out = run(cmd, options)
+ ret, out = runner(testpath, options)
vlog("# Ret was:", ret)
if options.timeout > 0:
@@ -807,7 +895,10 @@
print "Accept this change? [n] ",
answer = sys.stdin.readline().strip()
if answer.lower() in "y yes".split():
- rename(test + ".err", test + ".out")
+ if test.endswith(".t"):
+ rename(test + ".err", test)
+ else:
+ rename(test + ".err", test + ".out")
tested += 1
fails.pop()
continue
@@ -944,7 +1035,7 @@
for test in args:
if (test.startswith("test-") and '~' not in test and
('.' not in test or test.endswith('.py') or
- test.endswith('.bat'))):
+ test.endswith('.bat') or test.endswith('.t'))):
tests.append(test)
if not tests:
print "# Ran 0 tests, 0 skipped, 0 failed."