Merge after backout stable
authorChristian Ebert <blacktrash@gmx.net>
Sat, 14 Aug 2010 09:42:14 +0100
branchstable
changeset 779 0970d7c7ff42
parent 777 cd294ce45931 (diff)
parent 778 ca11c8128f08 (current diff)
child 780 4c37b26c549c
Merge after backout
tests/test-keyword.out
--- 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."
--- 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<<EOF
@@ -157,6 +157,19 @@
 cat a
 hg diff | grep -v 'b/a'
 hg rollback
+
+echo % record file
+echo foo > msg
+# do not use "hg record -m" here!
+hg record -l msg -d '1 11'<<EOF
+y
+y
+y
+EOF
+echo % a should be clean
+hg status -A a
+rm msg
+hg rollback
 hg update -C
 
 echo % init --mq
@@ -270,8 +283,17 @@
 echo % cat a
 cat a
 
+echo % clone
+cd ..
+
+echo % expansion in dest
+hg --quiet clone Test globalconf
+cat globalconf/a
+echo % no expansion in dest
+hg --quiet --config 'keyword.**=ignore' clone Test localconf
+cat localconf/a
+
 echo % clone to test incoming
-cd ..
 hg clone -r1 Test Test-a
 cd Test-a
 cat <<EOF >> .hg/hgrc
--- 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 <user@example.com>: 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 <user@example.com>: firstline $
+% no expansion in dest
+expand $Id$
+do not process $Id:
+xxx $
+$Xinfo$
 % clone to test incoming
 requesting all changes
 adding changesets