# HG changeset patch # User Christian Ebert # Date 1281775334 -3600 # Node ID 0970d7c7ff423e499d11761a0fc8ac21923c4cd9 # Parent cd294ce45931bba6c0bb044824b8cf39afe2178c# Parent ca11c8128f0820280fc2b241a20aec9d227842bd Merge after backout diff -r ca11c8128f08 -r 0970d7c7ff42 hgkw/keyword.py --- 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]...')), diff -r ca11c8128f08 -r 0970d7c7ff42 tests/run-tests.py --- 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." diff -r ca11c8128f08 -r 0970d7c7ff42 tests/test-keyword --- a/tests/test-keyword Sat Aug 14 09:30:42 2010 +0100 +++ b/tests/test-keyword Sat Aug 14 09:42:14 2010 +0100 @@ -142,7 +142,7 @@ echo % compare changenodes in a c cat a c -echo % record +echo % record chunk python -c \ 'l=open("a").readlines();l.insert(1,"foo\n");l.append("bar\n");open("a","w").writelines(l);' hg record -d '1 10' -m rectest< msg +# do not use "hg record -m" here! +hg record -l msg -d '1 11'<> .hg/hgrc diff -r ca11c8128f08 -r 0970d7c7ff42 tests/test-keyword.out --- a/tests/test-keyword.out Sat Aug 14 09:30:42 2010 +0100 +++ b/tests/test-keyword.out Sat Aug 14 09:42:14 2010 +0100 @@ -132,7 +132,7 @@ xxx $ $Id: c,v 40a904bbbe4c 1970/01/01 00:00:01 user $ tests for different changenodes -% record +% record chunk diff --git a/a b/a 2 hunks, 2 lines changed examine changes to 'a'? [Ynsfdaq?] @@ -163,6 +163,24 @@ do not process $Id: xxx $ +bar +rolling back to revision 2 (undo commit) +% record file +diff --git a/a b/a +2 hunks, 2 lines changed +examine changes to 'a'? [Ynsfdaq?] +@@ -1,3 +1,4 @@ + expand $Id$ ++foo + do not process $Id: + xxx $ +record change 1/2 to 'a'? [Ynsfdaq?] +@@ -2,2 +3,3 @@ + do not process $Id: + xxx $ ++bar +record change 2/2 to 'a'? [Ynsfdaq?] +% a should be clean +C a rolling back to revision 3 (undo commit) 1 files updated, 0 files merged, 0 files removed, 0 files unresolved % init --mq @@ -315,6 +333,17 @@ do not process $Id: xxx $ $Xinfo: User Name : firstline $ +% clone +% expansion in dest +expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $ +do not process $Id: +xxx $ +$Xinfo: User Name : firstline $ +% no expansion in dest +expand $Id$ +do not process $Id: +xxx $ +$Xinfo$ % clone to test incoming requesting all changes adding changesets