(0.9.2compat) merge with default 0.9.2compat
authorChristian Ebert <blacktrash@gmx.net>
Wed, 14 Sep 2011 16:19:33 +0100
branch0.9.2compat
changeset 984 a4ea5c2d3ff3
parent 707 e5feed5534d7 (diff)
parent 982 639bb4617c70 (current diff)
(0.9.2compat) merge with default Adapt preserving filemode. Fix a typo. Clean up last merge.
README
hgkw/keyword.py
tests/bundles/test-keyword.hg
tests/get-with-headers.py
tests/run-tests.py
tests/test-keyword
tests/test-keyword.out
tests/test-keyword.t
--- a/README	Sat Sep 10 13:23:41 2011 +0100
+++ b/README	Wed Sep 14 16:19:33 2011 +0100
@@ -3,16 +3,25 @@
 keyword extension for Mercurial SCM
 ===================================
 
-CAVEAT: Please use the keyword extension distributed with
-Mercurial > 1.0.2!
-For Mercurial 0.9.2 to 1.0.2 install the 0.9.2compat branch.
+[ standalone and backwards compatible version ]
 
-The default and stable branches are meant for development.
+CAVEAT: Only use this version with Mercurial 0.9.2 to 1.0.2!
+With Mercurial > 1.0.2 use the extension included in the
+distribution.
 
 install
 -------
 
-Run "python setup.py install".
+Either copy hgkw/keyword.py into the hgext directory of your
+Mercurial installation.
+Then add the lines:
+
+[extensions]
+hgext.keyword =
+
+to your hgrc file.
+
+Or run "python setup.py install".
 See also "pyton setup.py --help".
 Then add the line:
 
@@ -32,5 +41,11 @@
 testing
 -------
 
-$ cd tests
-$ ./run-tests.py --with-hg=/path/to/hg
+Copy hgkw/keyword.py into the hgext directory of your Mercurial
+source tree. Copy tests/test-keyword, tests/test-keyword.out into
+the tests directory of your Mercurial source tree. Change to that
+directory and run:
+
+$ python run-tests.py test-keyword
+
+and then keep your fingers crossed ...
--- a/hgkw/keyword.py	Sat Sep 10 13:23:41 2011 +0100
+++ b/hgkw/keyword.py	Wed Sep 14 16:19:33 2011 +0100
@@ -21,6 +21,14 @@
 #
 # Binary files are not touched.
 #
+# Setup in hgrc:
+#
+#   [extensions]
+#   # enable extension
+#   keyword = /full/path/to/hgkw/keyword.py
+#   # or, if script in canonical hgext folder:
+#   # keyword =
+#
 # Files to act upon/ignore are specified in the [keyword] section.
 # Customized keyword template mappings in the [keywordmaps] section.
 #
@@ -35,115 +43,211 @@
 change history. The mechanism can be regarded as a convenience for the
 current user or for archive distribution.
 
-Keywords expand to the changeset data pertaining to the latest change
-relative to the working directory parent of each file.
+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::
+Example:
 
     [keyword]
     # expand keywords in every python file except those matching "x*"
     **.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.
+NOTE: the more specific you are in your filename patterns the less you
+lose speed in huge repositories.
 
 For [keywordmaps] template mapping and expansion demonstration and
-control run :hg:`kwdemo`. See :hg:`help templates` for a list of
-available templates and filters.
+control run "hg kwdemo".
 
-Three additional date template filters are provided:
+An additional date template filter {date|utcdate} is provided. It
+returns a date like "2006/09/18 15:13:13".
 
-:``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 "hg
+kwdemo" to control the results of your config changes.
 
-The default template mappings (view with :hg:`kwdemo -d`) can be
-replaced with customized keywords and templates. Again, run
-:hg:`kwdemo` to control the results of your configuration changes.
-
-Before changing/disabling active keywords, you must run :hg:`kwshrink`
-to avoid storing expanded keywords in the change history.
+Before changing/disabling active keywords, run "hg kwshrink" to avoid
+the risk of inadvertently storing expanded keywords in the change
+history.
 
 To force expansion after enabling it, or a configuration change, run
-:hg:`kwexpand`.
+"hg kwexpand".
+
+Also, when committing with the record extension or using mq's qrecord,
+be aware that keywords cannot be updated. Again, run "hg kwexpand" on
+the files in question to update keyword expansions after all changes
+have been checked in.
 
 Expansions spanning more than one line and incremental expansions,
 like CVS' $Log$, are not supported. A keyword template map "Log =
 {desc}" expands to the first line of the changeset description.
+
+Caveat: With Mercurial versions prior to 4574925db5c0 "hg import" might
+        cause rejects if the patch context contains an active keyword. In that
+        case run "hg kwshrink", and then reimport. Or, better, use
+        bundle/unbundle to share changes.
 '''
 
-from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
-from mercurial import localrepo, match, patch, templatefilters, templater, util
-from mercurial import scmutil
-from mercurial.hgweb import webcommands
-from mercurial.i18n import _
-import os, re, shutil, tempfile
+from mercurial import commands, cmdutil, fancyopts, filelog
+from mercurial import localrepo, patch, revlog, templater, util
+from mercurial.node import nullid, hex
+from mercurial.i18n import gettext as _
+import getopt, os, re, shutil, tempfile, time
 
 commands.optionalrepo += ' kwdemo'
 
-cmdtable = {}
-command = cmdutil.command(cmdtable)
-
 # hg commands that do not act on keywords
-nokwcommands = ('add addremove annotate bundle export grep incoming init log'
-                ' outgoing push tip verify convert email glog')
+nokwcommands = ('add addremove annotate bundle copy export grep incoming init'
+                ' log outgoing push rename rollback tip verify'
+                ' convert email glog')
 
 # hg commands that trigger expansion only when writing to working dir,
 # not when reading filelog, and unexpand when reading from working dir
-restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
-
-# names of extensions using dorecord
-recordextensions = 'record'
-
-colortable = {
-    'kwfiles.enabled': 'green bold',
-    'kwfiles.deleted': 'cyan bold underline',
-    'kwfiles.enabledunknown': 'green',
-    'kwfiles.ignored': 'bold',
-    'kwfiles.ignoredunknown': 'none'
-}
+restricted = ('merge record resolve qfold qimport qnew qpush qrefresh qrecord'
+              ' transplant')
 
-# date like in cvs' $Date
-def utcdate(text):
-    ''':utcdate: Date. Returns a UTC-date in this format: "2009/08/18 11:00:13".
-    '''
-    return util.datestr((text[0], 0), '%Y/%m/%d %H:%M:%S')
-# date like in svn's $Date
-def svnisodate(text):
-    ''':svnisodate: Date. Returns a date in this format: "2009-08-18 13:00:13
-    +0200 (Tue, 18 Aug 2009)".
-    '''
-    return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
-# date like in svn's $Id
-def svnutcdate(text):
-    ''':svnutcdate: Date. Returns a UTC-date in this format: "2009-08-18
-    11:00:13Z".
-    '''
-    return util.datestr((text[0], 0), '%Y-%m-%d %H:%M:%SZ')
+# provide cvs-like UTC date filter
+utcdate = lambda x: time.strftime('%Y/%m/%d %H:%M:%S',
+                                  time.gmtime(float(x[0])))
 
-templatefilters.filters.update({'utcdate': utcdate,
-                                'svnisodate': svnisodate,
-                                'svnutcdate': svnutcdate})
+def textsafe(s):
+    '''Safe version of util.binary with reversed logic.
+    Note: argument may not be None, which is allowed for util.binary.'''
+    return '\0' not in s
 
 # make keyword tools accessible
 kwtools = {'templater': None, 'hgcmd': ''}
 
-def _defaultkwmaps(ui):
-    '''Returns default keywordmaps according to keywordset configuration.'''
+# monkeypatch argument parsing
+# due to backwards compatibility this can't be done in uisetup
+# uisetup introduced with extensions module in 930ed513c864
+def _kwdispatch_parse(ui, args):
+    '''Monkeypatch dispatch._parse to obtain running hg command.'''
+    cmd, func, args, options, cmdoptions = _dispatch_parse(ui, args)
+    kwtools['hgcmd'] = cmd
+    return cmd, func, args, options, cmdoptions
+
+try:
+    # cmdutil.parse moves to dispatch._parse in 18a9fbb5cd78
+    from mercurial import dispatch
+    _dispatch_parse = dispatch._parse
+    dispatch._parse = _kwdispatch_parse
+except ImportError:
+    try:
+        # commands.parse moves to cmdutil.parse in 0c61124ad877
+        _dispatch_parse = cmdutil.parse
+        cmdutil.parse = _kwdispatch_parse
+    except AttributeError:
+        _dispatch_parse = commands.parse
+        commands.parse = _kwdispatch_parse
+
+try:
+    # templatefilters module introduced in 9f1e6ab76069
+    from mercurial import templatefilters
+    template_filters = templatefilters.filters
+    template_firstline = templatefilters.firstline
+except ImportError:
+    template_filters = templater.common_filters
+    template_firstline = templater.firstline
+
+def _wwrite(repo, f, data, mf):
+    '''Makes repo.wwrite backwards compatible.'''
+    # 656e06eebda7 removed file descriptor argument
+    # 67982d3ee76c added flags argument
+    try:
+        repo.wwrite(f, data, mf.flags(f))
+    except (AttributeError, TypeError):
+        repo.wwrite(f, data)
+
+def _normal(repo, files):
+    '''Backwards compatible repo.dirstate.normal/update.'''
+    # 6fd953d5faea introduced dirstate.normal()
+    try:
+        for f in files:
+            repo.dirstate.normal(f)
+    except AttributeError:
+        repo.dirstate.update(files, 'n')
+
+def _pathto(repo, f, cwd=None):
+    '''kwfiles behaves similar to status, using pathto since 78b6add1f966.'''
+    try:
+        return repo.pathto(f, cwd)
+    except AttributeError:
+        return f
+
+'''Default match argument for commit, depending on version.'''
+if hasattr(cmdutil, 'match'):
+    _defmatch = None
+else:
+    _defmatch = util.always
+
+# commands.parse/cmdutil.parse returned nothing for
+# "hg diff --rev" before 88803a69b24a due to bug in fancyopts
+def _fancyopts(args, options, state):
+    '''Fixed fancyopts from a9b7e425674f.'''
+    namelist = []
+    shortlist = ''
+    argmap = {}
+    defmap = {}
+
+    for short, name, default, comment in options:
+        # convert opts to getopt format
+        oname = name
+        name = name.replace('-', '_')
+
+        argmap['-' + short] = argmap['--' + oname] = name
+        defmap[name] = default
+
+        # copy defaults to state
+        if isinstance(default, list):
+            state[name] = default[:]
+        elif callable(default):
+            print "whoa", name, default
+            state[name] = None
+        else:
+            state[name] = default
+
+        # does it take a parameter?
+        if not (default is None or default is True or default is False):
+            if short: short += ':'
+            if oname: oname += '='
+        if short:
+            shortlist += short
+        if name:
+            namelist.append(oname)
+
+    # parse arguments
+    opts, args = getopt.getopt(args, shortlist, namelist)
+
+    # transfer result to state
+    for opt, val in opts:
+        name = argmap[opt]
+        t = type(defmap[name])
+        if t is type(fancyopts):
+            state[name] = defmap[name](val)
+        elif t is type(1):
+            state[name] = int(val)
+        elif t is type(''):
+            state[name] = val
+        elif t is type([]):
+            state[name].append(val)
+        elif t is type(None) or t is type(False):
+            state[name] = True
+
+    # return unparsed args
+    return args
+
+fancyopts.fancyopts = _fancyopts
+
+
+class kwtemplater(object):
+    '''
+    Sets up keyword templates, corresponding keyword regex, and
+    provides keyword substitution functions.
+    '''
     templates = {
         'Revision': '{node|short}',
         'Author': '{author|user}',
-    }
-    kwsets = ({
         'Date': '{date|utcdate}',
         'RCSfile': '{file|basename},v',
         'RCSFile': '{file|basename},v', # kept for backwards compatibility
@@ -151,157 +255,137 @@
         '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 _shrinktext(text, subfunc):
-    '''Helper for keyword expansion removal in text.
-    Depending on subfunc also returns number of substitutions.'''
-    return subfunc(r'$\1$', text)
-
-def _preselect(wstatus, changed):
-    '''Retrieves modfied and added files from a working directory state
-    and returns the subset of each contained in given changed files
-    retrieved from a change context.'''
-    modified, added = wstatus[:2]
-    modified = [f for f in modified if f in changed]
-    added = [f for f in added if f in changed]
-    return modified, added
-
-
-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, '', [], inc, exc)
+        self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
         self.restrict = kwtools['hgcmd'] in restricted.split()
-        self.record = False
 
         kwmaps = self.ui.configitems('keywordmaps')
         if kwmaps: # override default templates
-            self.templates = dict((k, templater.parsestring(v, False))
-                                  for k, v in kwmaps)
-        else:
-            self.templates = _defaultkwmaps(self.ui)
+            kwmaps = [(k, templater.parsestring(v, False))
+                      for (k, v) in kwmaps]
+            self.templates = dict(kwmaps)
+        escaped = map(re.escape, self.templates.keys())
+        kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
+        self.re_kw = re.compile(kwpat)
 
-    @util.propertycache
-    def escape(self):
-        '''Returns bar-separated and escaped keywords.'''
-        return '|'.join(map(re.escape, self.templates.keys()))
+        template_filters['utcdate'] = utcdate
+        self.ct = self._changeset_templater()
 
-    @util.propertycache
-    def rekw(self):
-        '''Returns regex for unexpanded keywords.'''
-        return re.compile(r'\$(%s)\$' % self.escape)
+    def _changeset_templater(self):
+        '''Backwards compatible cmdutil.changeset_templater.'''
+        # before 1e0b94cfba0e there was an extra "brinfo" argument
+        try:
+            return cmdutil.changeset_templater(self.ui, self.repo,
+                                               False, '', False)
+        except TypeError:
+            return cmdutil.changeset_templater(self.ui, self.repo,
+                                               False, None, '', False)
 
-    @util.propertycache
-    def rekwexp(self):
-        '''Returns regex for expanded keywords.'''
-        return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape)
+    def getnode(self, path, fnode):
+        '''Derives changenode from file path and filenode.'''
+        # used by kwfilelog.read and kwexpand
+        c = self.repo.filectx(path, fileid=fnode)
+        return c.node()
 
-    def substitute(self, data, path, ctx, subfunc):
+    def substitute(self, data, path, node, subfunc):
         '''Replaces keywords in data with expanded template.'''
         def kwsub(mobj):
             kw = mobj.group(1)
-            ct = cmdutil.changeset_templater(self.ui, self.repo,
-                                             False, None, '', False)
-            ct.use_template(self.templates[kw])
+            self.ct.use_template(self.templates[kw])
             self.ui.pushbuffer()
-            ct.show(ctx, root=self.repo.root, file=path)
-            ekw = templatefilters.firstline(self.ui.popbuffer())
-            return '$%s: %s $' % (kw, ekw)
+            self.ct.show(changenode=node, root=self.repo.root, file=path)
+            return '$%s: %s $' % (kw, template_firstline(self.ui.popbuffer()))
         return subfunc(kwsub, data)
 
-    def linkctx(self, path, fileid):
-        '''Similar to filelog.linkrev, but returns a changectx.'''
-        return self.repo.filectx(path, fileid=fileid).changectx()
-
     def expand(self, path, node, data):
         '''Returns data with keywords expanded.'''
-        if not self.restrict and self.match(path) and not util.binary(data):
-            ctx = self.linkctx(path, node)
-            return self.substitute(data, path, ctx, self.rekw.sub)
+        if not self.restrict and self.matcher(path) and textsafe(data):
+            changenode = self.getnode(path, node)
+            return self.substitute(data, path, changenode, self.re_kw.sub)
         return data
 
-    def iskwfile(self, cand, ctx):
-        '''Returns subset of candidates which are configured for keyword
-        expansion are not symbolic links.'''
-        return [f for f in cand if self.match(f) and not 'l' in ctx.flags(f)]
+    def iskwfile(self, path, islink):
+        '''Returns true if path matches [keyword] pattern
+        and is not a symbolic link.
+        Caveat: localrepository._link fails on Windows.'''
+        return self.matcher(path) and not islink(path)
 
-    def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
+    def overwrite(self, node, expand, candidates):
         '''Overwrites selected files expanding/shrinking keywords.'''
-        if self.restrict or lookup or self.record: # exclude kw_copy
-            candidates = self.iskwfile(candidates, ctx)
-        if not candidates:
-            return
-        kwcmd = self.restrict and lookup # kwexpand/kwshrink
-        if self.restrict or expand and lookup:
+        # repo[changeid] introduced in f6c00b17387c
+        if node is not None:     # commit
+            try:
+                ctx = self.repo[node]
+            except TypeError:
+                ctx = self.repo.changectx(node)
             mf = ctx.manifest()
-        if self.restrict or rekw:
-            re_kw = self.rekw
-        else:
-            re_kw = self.rekwexp
-        if expand:
-            msg = _('overwriting %s expanding keywords\n')
+            candidates = [f for f in ctx.files() if f in mf]
+        else:                    # kwexpand/kwshrink
+            try:
+                ctx = self.repo['.']
+            except TypeError:
+                ctx = self.repo.changectx()
+            mf = ctx.manifest()
+        if hasattr(ctx, 'flags'):
+            # 51b0e799352f
+            islink = lambda p: 'l' in ctx.flags(p)
         else:
-            msg = _('overwriting %s shrinking keywords\n')
-        for f in candidates:
-            if self.restrict:
-                data = self.repo.file(f).read(mf[f])
-            else:
-                data = self.repo.wread(f)
-            if util.binary(data):
-                continue
+            islink = mf.linkf
+        candidates = [f for f in candidates if self.iskwfile(f, islink)]
+        if candidates:
+            self.restrict = True # do not expand when reading
             if expand:
-                if lookup:
-                    ctx = self.linkctx(f, mf[f])
-                data, found = self.substitute(data, f, ctx, re_kw.subn)
-            elif self.restrict:
-                found = re_kw.search(data)
+                msg = _('overwriting %s expanding keywords\n')
             else:
-                data, found = _shrinktext(data, re_kw.subn)
-            if found:
-                self.ui.note(msg % f)
-                fp = self.repo.wopener(f, "wb", atomictemp=True)
-                fp.write(data)
-                fp.close()
-                if kwcmd:
-                    self.repo.dirstate.normal(f)
-                elif self.record:
-                    self.repo.dirstate.normallookup(f)
+                msg = _('overwriting %s shrinking keywords\n')
+            overwritten = []
+            for f in candidates:
+                fp = self.repo.file(f)
+                data = fp.read(mf[f])
+                if not textsafe(data):
+                    continue
+                if expand:
+                    changenode = node or self.getnode(f, mf[f])
+                    data, found = self.substitute(data, f, changenode,
+                                                  self.re_kw.subn)
+                else:
+                    found = self.re_kw.search(data)
+                if found:
+                    self.ui.note(msg % f)
+                    fpath = self.repo.wjoin(f)
+                    mode = os.lstat(fpath).st_mode
+                    self.repo.wwrite(f, data, mf.flags(f))
+                    os.chmod(fpath, mode)
+                    overwritten.append(f)
+            _normal(self.repo, overwritten)
+            self.restrict = False
+
+    def shrinktext(self, text):
+        '''Unconditionally removes all keyword substitutions from text.'''
+        return self.re_kw.sub(r'$\1$', text)
 
     def shrink(self, fname, text):
         '''Returns text with all keyword substitutions removed.'''
-        if self.match(fname) and not util.binary(text):
-            return _shrinktext(text, self.rekwexp.sub)
+        if self.matcher(fname) and textsafe(text):
+            return self.shrinktext(text)
         return text
 
     def shrinklines(self, fname, lines):
         '''Returns lines with keyword substitutions removed.'''
-        if self.match(fname):
+        if self.matcher(fname):
             text = ''.join(lines)
-            if not util.binary(text):
-                return _shrinktext(text, self.rekwexp.sub).splitlines(True)
+            if textsafe(text):
+                return self.shrinktext(text).splitlines(True)
         return lines
 
     def wread(self, fname, data):
         '''If in restricted mode returns data read from wdir with
         keyword substitutions removed.'''
-        if self.restrict:
-            return self.shrink(fname, data)
-        return data
+        return self.restrict and self.shrink(fname, data) or data
 
 class kwfilelog(filelog.filelog):
     '''
@@ -316,8 +400,6 @@
     def read(self, node):
         '''Expands keywords when reading filelog.'''
         data = super(kwfilelog, self).read(node)
-        if self.renamed(node):
-            return data
         return self.kwt.expand(self.path, node, data)
 
     def add(self, text, meta, tr, link, p1=None, p2=None):
@@ -328,39 +410,48 @@
     def cmp(self, node, text):
         '''Removes keyword substitutions for comparison.'''
         text = self.kwt.shrink(self.path, text)
-        return super(kwfilelog, self).cmp(node, text)
+        if self.renamed(node):
+            t2 = super(kwfilelog, self).read(node)
+            return t2 != text
+        return revlog.revlog.cmp(self, node, text)
 
-def _status(ui, repo, wctx, kwt, *pats, **opts):
+def _status(ui, repo, kwt, unknown, *pats, **opts):
     '''Bails out if [keyword] configuration is not active.
     Returns status of working directory.'''
     if kwt:
-        return repo.status(match=scmutil.match(wctx, pats, opts), clean=True,
-                           unknown=opts.get('unknown') or opts.get('all'))
+        try:
+            # 0159b7a36184 ff.
+            matcher = cmdutil.match(repo, pats, opts)
+            try:
+                # 4faaa0535ea7
+                return repo.status(match=matcher, unknown=unknown, clean=True)
+            except TypeError:
+                return repo.status(match=matcher, list_clean=True)
+        except AttributeError:
+            files, match, anypats = cmdutil.matchpats(repo, pats, opts)
+            return repo.status(files=files, match=match, list_clean=True)
     if ui.configitems('keyword'):
         raise util.Abort(_('[keyword] patterns cannot match'))
     raise util.Abort(_('no [keyword] patterns configured'))
 
 def _kwfwrite(ui, repo, expand, *pats, **opts):
     '''Selects files and passes them to kwtemplater.overwrite.'''
-    wctx = repo[None]
-    if len(wctx.parents()) > 1:
+    if repo.dirstate.parents()[1] != nullid:
         raise util.Abort(_('outstanding uncommitted merge'))
     kwt = kwtools['templater']
-    wlock = repo.wlock()
+    status = _status(ui, repo, kwt, False, *pats, **opts)
+    modified, added, removed, deleted, unknown, ignored, clean = status
+    if modified or added or removed or deleted:
+        raise util.Abort(_('outstanding uncommitted changes'))
+    wlock = lock = None
     try:
-        status = _status(ui, repo, wctx, kwt, *pats, **opts)
-        modified, added, removed, deleted, unknown, ignored, clean = status
-        if modified or added or removed or deleted:
-            raise util.Abort(_('outstanding uncommitted changes'))
-        kwt.overwrite(wctx, clean, True, expand)
+        wlock = repo.wlock()
+        lock = repo.lock()
+        kwt.overwrite(None, expand, clean)
     finally:
-        wlock.release()
+        del wlock, lock
 
-@command('kwdemo',
-         [('d', 'default', None, _('show default keyword template maps')),
-          ('f', 'rcfile', '',
-           _('read maps from rcfile'), _('FILE'))],
-         _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'))
+
 def demo(ui, repo, *args, **opts):
     '''print [keywordmaps] configuration and an expansion example
 
@@ -371,22 +462,19 @@
     and using -f/--rcfile to source an external hgrc file.
 
     Use -d/--default to disable current configuration.
-
-    See :hg:`help templates` for information on templates and filters.
     '''
     def demoitems(section, items):
+        items.sort()
         ui.write('[%s]\n' % section)
-        for k, v in sorted(items):
+        for k, v in items:
             ui.write('%s = %s\n' % (k, v))
 
+    kwstatus = 'current'
     fn = 'demo.txt'
     tmpdir = tempfile.mkdtemp('', 'kwdemo.')
     ui.note(_('creating temporary repository at %s\n') % tmpdir)
     repo = localrepo.localrepository(ui, tmpdir, True)
     ui.setconfig('keyword', fn, '')
-    svn = ui.configbool('keywordset', 'svn')
-    # explicitly set keywordset for demo output
-    ui.setconfig('keywordset', 'svn', svn)
 
     uikwmaps = ui.configitems('keywordmaps')
     if args or opts.get('rcfile'):
@@ -394,10 +482,7 @@
         if uikwmaps:
             ui.status(_('\textending current template maps\n'))
         if opts.get('default') or not uikwmaps:
-            if svn:
-                ui.status(_('\toverriding default svn keywordset\n'))
-            else:
-                ui.status(_('\toverriding default cvs keywordset\n'))
+            ui.status(_('\toverriding default template maps\n'))
         if opts.get('rcfile'):
             ui.readconfig(opts.get('rcfile'))
         if args:
@@ -409,34 +494,37 @@
             ui.readconfig(repo.join('hgrc'))
         kwmaps = dict(ui.configitems('keywordmaps'))
     elif opts.get('default'):
-        if svn:
-            ui.status(_('\n\tconfiguration using default svn keywordset\n'))
-        else:
-            ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
-        kwmaps = _defaultkwmaps(ui)
+        ui.status(_('\n\tconfiguration using default keyword template maps\n'))
+        kwmaps = kwtemplater.templates
         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'))
-        if uikwmaps:
-            kwmaps = dict(uikwmaps)
-        else:
-            kwmaps = _defaultkwmaps(ui)
+        kwmaps = dict(uikwmaps) or kwtemplater.templates
 
-    uisetup(ui)
     reposetup(ui, repo)
-    ui.write('[extensions]\nkeyword =\n')
+    for k, v in ui.configitems('extensions'):
+        if k.endswith('keyword'):
+            extension = '%s = %s' % (k, v)
+            break
+    ui.status(_('\n\tconfig using %s keyword template maps\n') % kwstatus)
+    ui.write('[extensions]\n%s\n' % extension)
     demoitems('keyword', ui.configitems('keyword'))
-    demoitems('keywordset', ui.configitems('keywordset'))
-    demoitems('keywordmaps', kwmaps.iteritems())
-    keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
-    repo.wopener.write(fn, keywords)
-    repo[None].add([fn])
+    demoitems('keywordmaps', kwmaps.items())
+    kwkeys = kwmaps.keys()
+    kwkeys.sort()
+    keywords = '$' + '$\n$'.join(kwkeys) + '$\n'
+    repo.wopener(fn, 'w').write(keywords)
+    repo.add([fn])
     ui.note(_('\nkeywords written to %s:\n') % fn)
     ui.note(keywords)
-    repo.dirstate.setbranch('demobranch')
+    try:
+        repo.dirstate.setbranch('demobranch')
+    except AttributeError:
+        # before 7e1c8a565a4f
+        repo.opener('branch', 'w').write('demobranch\n')
     for name, cmd in ui.configitems('hooks'):
         if name.split('.', 1)[0].find('commit') > -1:
             repo.ui.setconfig('hooks', name, '')
@@ -447,7 +535,6 @@
     ui.write(repo.wread(fn))
     shutil.rmtree(tmpdir, ignore_errors=True)
 
-@command('kwexpand', commands.walkopts, _('hg kwexpand [OPTION]... [FILE]...'))
 def expand(ui, repo, *pats, **opts):
     '''expand keywords in the working directory
 
@@ -458,12 +545,6 @@
     # 3rd argument sets expansion to True
     _kwfwrite(ui, repo, True, *pats, **opts)
 
-@command('kwfiles',
-         [('A', 'all', None, _('show keyword status flags of all files')),
-          ('i', 'ignore', None, _('show files excluded from expansion')),
-          ('u', 'unknown', None, _('only show unknown (not tracked) files')),
-         ] + commands.walkopts,
-         _('hg kwfiles [OPTION]... [FILE]...'))
 def files(ui, repo, *pats, **opts):
     '''show files configured for keyword expansion
 
@@ -474,11 +555,11 @@
     execution by including only files that are actual candidates for
     expansion.
 
-    See :hg:`help keyword` on how to construct patterns both for
+    See "hg help keyword" on how to construct patterns both for
     inclusion and exclusion of files.
 
     With -A/--all and -v/--verbose the codes used to show the status
-    of files are::
+    of files are:
 
       K = keyword expansion candidate
       k = keyword expansion candidate (not tracked)
@@ -486,35 +567,48 @@
       i = ignored (not tracked)
     '''
     kwt = kwtools['templater']
-    wctx = repo[None]
-    status = _status(ui, repo, wctx, kwt, *pats, **opts)
+    status = _status(ui, repo, kwt, *pats, **opts)
     cwd = pats and repo.getcwd() or ''
     modified, added, removed, deleted, unknown, ignored, clean = status
     files = []
     if not opts.get('unknown') or opts.get('all'):
-        files = sorted(modified + added + clean)
-    kwfiles = kwt.iskwfile(files, wctx)
-    kwdeleted = kwt.iskwfile(deleted, wctx)
-    kwunknown = kwt.iskwfile(unknown, wctx)
+        try:
+            # f67d1468ac50
+            files = util.sort(modified + added + clean)
+        except AttributeError:
+            files = modified + added + clean
+            files.sort()
+    try:
+        # f6c00b17387c
+        wctx = repo[None]
+    except TypeError:
+        wctx = repo.workingctx()
+    if hasattr(wctx, 'flags'):
+        islink = lambda p: 'l' in wctx.flags(p)
+    elif hasattr(wctx, 'fileflags'):
+        islink = lambda p: 'l' in wctx.fileflags(p)
+    else:
+        mf = wctx.manifest()
+        islink = mf.linkf
+    kwfiles = [f for f in files if kwt.iskwfile(f, islink)]
+    kwunknown = [f for f in unknown if kwt.iskwfile(f, islink)]
     if not opts.get('ignore') or opts.get('all'):
-        showfiles = kwfiles, kwdeleted, kwunknown
+        showfiles = kwfiles, kwunknown
     else:
-        showfiles = [], [], []
+        showfiles = [], []
     if opts.get('all') or opts.get('ignore'):
         showfiles += ([f for f in files if f not in kwfiles],
                       [f for f in unknown if f not in kwunknown])
-    kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
-    kwstates = zip('K!kIi', showfiles, kwlabels)
-    for char, filenames, kwstate in kwstates:
+    for char, filenames in zip('KkIi', showfiles):
         fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
         for f in filenames:
-            ui.write(fmt % repo.pathto(f, cwd), label='kwfiles.' + kwstate)
+            ui.write(fmt % _pathto(repo, f, cwd))
 
-@command('kwshrink', commands.walkopts, _('hg kwshrink [OPTION]... [FILE]...'))
 def shrink(ui, repo, *pats, **opts):
     '''revert expanded keywords in the working directory
 
-    Must be run before changing/disabling active keywords.
+    Run before changing/disabling active keywords or if you experience
+    problems with "hg import" or "hg merge".
 
     kwshrink refuses to run if given files contain local changes.
     '''
@@ -522,17 +616,6 @@
     _kwfwrite(ui, repo, False, *pats, **opts)
 
 
-def uisetup(ui):
-    ''' Monkeypatches dispatch._parse to retrieve user command.'''
-
-    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)
-
 def reposetup(ui, repo):
     '''Sets up repo as kwrepo for keyword substitution.
     Overrides file method to return kwfilelog instead of filelog
@@ -543,7 +626,7 @@
 
     try:
         if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
-            or '.hg' in util.splitpath(repo.root)
+            or '.hg' in repo.root.split(os.sep)
             or repo._url.startswith('bundle:')):
             return
     except AttributeError:
@@ -570,132 +653,181 @@
             data = super(kwrepo, self).wread(filename)
             return kwt.wread(filename, data)
 
-        def commit(self, *args, **opts):
-            # use custom commitctx for user commands
-            # other extensions can still wrap repo.commitctx directly
-            self.commitctx = self.kwcommitctx
+        def _commit(self, files, text, user, date, match, force, lock, wlock,
+                    force_editor, p1, p2, extra, empty_ok):
+            '''Private commit wrapper for backwards compatibility.'''
             try:
-                return super(kwrepo, self).commit(*args, **opts)
-            finally:
-                del self.commitctx
+                return super(kwrepo,
+                             self).commit(files=files, text=text,
+                                          user=user, date=date, match=match,
+                                          force=force, lock=lock, wlock=wlock,
+                                          force_editor=force_editor,
+                                          p1=p1, p2=p2, extra=extra)
+            except TypeError:
+                try:
+                    return super(kwrepo,
+                                 self).commit(files=files, text=text,
+                                              user=user, date=date,
+                                              match=match, force=force,
+                                              force_editor=force_editor,
+                                              p1=p1, p2=p2, extra=extra,
+                                              empty_ok=empty_ok)
+                except TypeError:
+                    return super(kwrepo,
+                                 self).commit(files=files, text=text,
+                                              user=user, date=date,
+                                              match=match, force=force,
+                                              force_editor=force_editor,
+                                              p1=p1, p2=p2, extra=extra)
 
-        def kwcommitctx(self, ctx, error=False):
-            n = super(kwrepo, self).commitctx(ctx, error)
-            # no lock needed, only called from repo.commit() which already locks
-            if not kwt.record:
-                restrict = kwt.restrict
-                kwt.restrict = True
-                kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
-                              False, True)
-                kwt.restrict = restrict
-            return n
+        def commit(self, files=None, text='', user=None, date=None,
+                   match=_defmatch, force=False, lock=None, wlock=None,
+                   force_editor=False, p1=None, p2=None, extra={},
+                   empty_ok=False):
+            # (w)lock arguments removed in 126f527b3ba3
+            # so they are None or what was passed to commit
+            # use private _(w)lock for deletion
+            _lock = lock
+            _wlock = wlock
+            del wlock, lock
+            _p1 = _p2 = None
+            try:
+                if not _wlock:
+                    _wlock = self.wlock()
+                if not _lock:
+                    _lock = self.lock()
+                # store and postpone commit hooks
+                commithooks = {}
+                for name, cmd in ui.configitems('hooks'):
+                    if name.split('.', 1)[0] == 'commit':
+                        commithooks[name] = cmd
+                        ui.setconfig('hooks', name, '')
+                if commithooks:
+                    # store parents for commit hook environment
+                    if p1 is None:
+                        _p1, _p2 = self.dirstate.parents()
+                    else:
+                        _p1, _p2 = p1, p2 or nullid
+                    _p1 = hex(_p1)
+                    if _p2 == nullid:
+                        _p2 = ''
+                    else:
+                        _p2 = hex(_p2)
 
-        def rollback(self, dryrun=False):
-            wlock = self.wlock()
-            try:
-                if not dryrun:
-                    changed = self['.'].files()
-                ret = super(kwrepo, self).rollback(dryrun)
-                if not dryrun:
-                    ctx = self['.']
-                    modified, added = _preselect(self[None].status(), changed)
-                    kwt.overwrite(ctx, modified, True, True)
-                    kwt.overwrite(ctx, added, True, False)
-                return ret
+                n = self._commit(files, text, user, date, match, force, _lock,
+                                 _wlock, force_editor, p1, p2, extra, empty_ok)
+
+                # restore commit hooks
+                for name, cmd in commithooks.iteritems():
+                    ui.setconfig('hooks', name, cmd)
+                if n is not None:
+                    kwt.overwrite(n, True, None)
+                    self.hook('commit', node=n, parent1=_p1, parent2=_p2)
+                return n
             finally:
-                wlock.release()
+                del _wlock, _lock
+
+    repo.__class__ = kwrepo
 
     # monkeypatches
-    def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
-        '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
-        rejects or conflicts due to expanded keywords in working dir.'''
-        orig(self, ui, gp, backend, store, eolmode)
-        # shrink keywords read from working dir
-        self.lines = kwt.shrinklines(self.fname, self.lines)
+    try:
+        # avoid spurious rejects if patchfile is available
+        def kwpatchfile_init(self, ui, fname, missing=False):
+            '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
+            rejects or conflicts due to expanded keywords in working dir.'''
+            try:
+                patchfile_init(self, ui, fname, missing)
+            except TypeError:
+                # "missing" arg added in e90e72c6b4c7
+                patchfile_init(self, ui, fname)
+            self.lines = kwt.shrinklines(self.fname, self.lines)
 
-    def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
-                opts=None, prefix=''):
-        '''Monkeypatch patch.diff to avoid expansion.'''
-        kwt.restrict = True
-        return orig(repo, node1, node2, match, changes, opts, prefix)
-
-    def kwweb_skip(orig, web, req, tmpl):
-        '''Wraps webcommands.x turning off keyword expansion.'''
-        kwt.match = util.never
-        return orig(web, req, tmpl)
+        patchfile_init = patch.patchfile.__init__
+        patch.patchfile.__init__ = kwpatchfile_init
+    except AttributeError:
+        pass
 
-    def kw_copy(orig, ui, repo, pats, opts, rename=False):
-        '''Wraps cmdutil.copy so that copy/rename destinations do not
-        contain expanded keywords.
-        Note that the source of a regular file destination may also be a
-        symlink:
-        hg cp sym x                -> x is symlink
-        cp sym x; hg cp -A sym x   -> x is file (maybe expanded keywords)
-        For the latter we have to follow the symlink to find out whether its
-        target is configured for expansion and we therefore must unexpand the
-        keywords in the destination.'''
-        orig(ui, repo, pats, opts, rename)
-        if opts.get('dry_run'):
-            return
-        wctx = repo[None]
-        cwd = repo.getcwd()
+    def kw_diff(repo, node1=None, node2=None, files=None, match=_defmatch,
+                 fp=None, changes=None, opts=None):
+        # only expand if comparing against working dir
+        if node2 is not None:
+            kwt.matcher = util.never
+        elif node1 is not None and node1 != repo.dirstate.parents()[0]:
+            kwt.restrict = True
+        try:
+            patch_diff(repo, node1=node1, node2=node2, files=files,
+                       match=match, fp=fp, changes=changes, opts=opts)
+        except TypeError:
+            patch_diff(repo, node1=node1, node2=node2, match=match, fp=fp,
+                       changes=changes, opts=opts)
 
-        def haskwsource(dest):
-            '''Returns true if dest is a regular file and configured for
-            expansion or a symlink which points to a file configured for
-            expansion. '''
-            source = repo.dirstate.copied(dest)
-            if 'l' in wctx.flags(source):
-                source = scmutil.canonpath(repo.root, cwd,
-                                           os.path.realpath(source))
-            return kwt.match(source)
+    patch_diff = patch.diff
+    if not kwt.restrict:
+        patch.diff = kw_diff
+
+    try:
+        from mercurial.hgweb import webcommands
+        def kwweb_annotate(web, req, tmpl):
+            '''Wraps webcommands.annotate turning off keyword expansion.'''
+            kwt.matcher = util.never
+            return webcommands_annotate(web, req, tmpl)
+
+        def kwweb_changeset(web, req, tmpl):
+            '''Wraps webcommands.changeset turning off keyword expansion.'''
+            kwt.matcher = util.never
+            return webcommands_changeset(web, req, tmpl)
 
-        candidates = [f for f in repo.dirstate.copies() if
-                      not 'l' in wctx.flags(f) and haskwsource(f)]
-        kwt.overwrite(wctx, candidates, False, False)
+        def kwweb_filediff(web, req, tmpl):
+            '''Wraps webcommands.filediff turning off keyword expansion.'''
+            kwt.matcher = util.never
+            return webcommands_filediff(web, req, tmpl)
+
+        webcommands_annotate = webcommands.annotate
+        webcommands_changeset = webcommands.changeset
+        webcommands_filediff = webcommands.filediff
+
+        webcommands.annotate = kwweb_annotate
+        webcommands.changeset = webcommands.rev = kwweb_changeset
+        webcommands.filediff = webcommands.diff = kwweb_filediff
 
-    def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
-        '''Wraps record.dorecord expanding keywords after recording.'''
-        wlock = repo.wlock()
-        try:
-            # record returns 0 even when nothing has changed
-            # therefore compare nodes before and after
-            kwt.record = True
-            ctx = repo['.']
-            wstatus = repo[None].status()
-            ret = orig(ui, repo, commitfunc, *pats, **opts)
-            recctx = repo['.']
-            if ctx != recctx:
-                modified, added = _preselect(wstatus, recctx.files())
-                kwt.restrict = False
-                kwt.overwrite(recctx, modified, False, True)
-                kwt.overwrite(recctx, added, False, True, True)
-                kwt.restrict = True
-            return ret
-        finally:
-            wlock.release()
+    except ImportError:
+        from mercurial.hgweb.hgweb_mod import hgweb
+        def kwweb_do_annotate(self, req):
+            kwt.matcher = util.never
+            hgweb_do_annotate(self, req)
+
+        def kwweb_do_changeset(self, req):
+            kwt.matcher = util.never
+            hgweb_do_changeset(self, req)
+
+        def kwweb_do_filediff(self, req):
+            kwt.matcher = util.never
+            hgweb_do_filediff(self, req)
 
-    def kwfilectx_cmp(orig, self, fctx):
-        # keyword affects data size, comparing wdir and filelog size does
-        # not make sense
-        if (fctx._filerev is None and
-            (self._repo._encodefilterpats or
-             kwt.match(fctx.path()) and not 'l' in fctx.flags()) or
-            self.size() == fctx.size()):
-            return self._filelog.cmp(self._filenode, fctx.data())
-        return True
+        hgweb_do_annotate = hgweb.do_annotate
+        hgweb_do_changeset = hgweb.do_changeset
+        hgweb_do_filediff = hgweb.do_filediff
+
+        hgweb.do_annotate = kwweb_do_annotate
+        hgweb.do_changeset = hgweb.do_rev = kwweb_do_changeset
+        hgweb.do_filediff = hgweb.do_diff = kwweb_do_filediff
+
 
-    extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
-    extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
-    extensions.wrapfunction(patch, 'diff', kw_diff)
-    extensions.wrapfunction(cmdutil, 'copy', kw_copy)
-    for c in 'annotate changeset rev filediff diff'.split():
-        extensions.wrapfunction(webcommands, c, kwweb_skip)
-    for name in recordextensions.split():
-        try:
-            record = extensions.find(name)
-            extensions.wrapfunction(record, 'dorecord', kw_dorecord)
-        except KeyError:
-            pass
-
-    repo.__class__ = kwrepo
+cmdtable = {
+    'kwdemo':
+        (demo,
+         [('d', 'default', None, _('show default keyword template maps')),
+          ('f', 'rcfile', '', _('read maps from rcfile'))],
+         _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
+    'kwexpand': (expand, commands.walkopts,
+                 _('hg kwexpand [OPTION]... [FILE]...')),
+    'kwfiles':
+        (files,
+         [('A', 'all', None, _('show keyword status flags of all files')),
+          ('i', 'ignore', None, _('show files excluded from expansion')),
+          ('u', 'unknown', None, _('only show unknown (not tracked) files')),
+         ] + commands.walkopts,
+         _('hg kwfiles [OPTION]... [FILE]...')),
+    'kwshrink': (shrink, commands.walkopts,
+                 _('hg kwshrink [OPTION]... [FILE]...')),
+}
Binary file tests/bundles/test-keyword.hg has changed
--- a/tests/get-with-headers.py	Sat Sep 10 13:23:41 2011 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-#!/usr/bin/env python
-
-"""This does HTTP GET requests given a host:port and path and returns
-a subset of the headers plus the body of the result."""
-
-import httplib, sys, re
-
-try:
-    import msvcrt, os
-    msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
-    msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
-except ImportError:
-    pass
-
-headers = [h.lower() for h in sys.argv[3:]]
-conn = httplib.HTTPConnection(sys.argv[1])
-conn.request("GET", sys.argv[2])
-response = conn.getresponse()
-print response.status, response.reason
-for h in headers:
-    if response.getheader(h, None) is not None:
-        print "%s: %s" % (h, response.getheader(h))
-print
-data = response.read()
-sys.stdout.write(data)
-
-if 200 <= response.status <= 299:
-    sys.exit(0)
-sys.exit(1)
--- a/tests/run-tests.py	Sat Sep 10 13:23:41 2011 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1204 +0,0 @@
-#!/usr/bin/env python
-#
-# run-tests.py - Run a set of tests on Mercurial
-#
-# Copyright 2006 Matt Mackall <mpm@selenic.com>
-#
-# This software may be used and distributed according to the terms of the
-# GNU General Public License version 2 or any later version.
-
-# Modifying this script is tricky because it has many modes:
-#   - serial (default) vs parallel (-jN, N > 1)
-#   - no coverage (default) vs coverage (-c, -C, -s)
-#   - temp install (default) vs specific hg script (--with-hg, --local)
-#   - tests are a mix of shell scripts and Python scripts
-#
-# If you change this script, it is recommended that you ensure you
-# haven't broken it by running it in various modes with a representative
-# sample of test scripts.  For example:
-#
-#  1) serial, no coverage, temp install:
-#      ./run-tests.py test-s*
-#  2) serial, no coverage, local hg:
-#      ./run-tests.py --local test-s*
-#  3) serial, coverage, temp install:
-#      ./run-tests.py -c test-s*
-#  4) serial, coverage, local hg:
-#      ./run-tests.py -c --local test-s*      # unsupported
-#  5) parallel, no coverage, temp install:
-#      ./run-tests.py -j2 test-s*
-#  6) parallel, no coverage, local hg:
-#      ./run-tests.py -j2 --local test-s*
-#  7) parallel, coverage, temp install:
-#      ./run-tests.py -j2 -c test-s*          # currently broken
-#  8) parallel, coverage, local install:
-#      ./run-tests.py -j2 -c --local test-s*  # unsupported (and broken)
-#  9) parallel, custom tmp dir:
-#      ./run-tests.py -j2 --tmpdir /tmp/myhgtests
-#
-# (You could use any subset of the tests: test-s* happens to match
-# enough that it's worth doing parallel runs, few enough that it
-# completes fairly quickly, includes both shell and Python scripts, and
-# includes some scripts that run daemon processes.)
-
-from distutils import version
-import difflib
-import errno
-import optparse
-import os
-import shutil
-import subprocess
-import signal
-import sys
-import tempfile
-import time
-import re
-import threading
-
-processlock = threading.Lock()
-
-closefds = os.name == 'posix'
-def Popen4(cmd, wd, timeout):
-    processlock.acquire()
-    p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd,
-                         close_fds=closefds,
-                         stdin=subprocess.PIPE, stdout=subprocess.PIPE,
-                         stderr=subprocess.STDOUT)
-    processlock.release()
-
-    p.fromchild = p.stdout
-    p.tochild = p.stdin
-    p.childerr = p.stderr
-
-    p.timeout = False
-    if timeout:
-        def t():
-            start = time.time()
-            while time.time() - start < timeout and p.returncode is None:
-                time.sleep(1)
-            p.timeout = True
-            if p.returncode is None:
-                terminate(p)
-        threading.Thread(target=t).start()
-
-    return p
-
-# reserved exit code to skip test (used by hghave)
-SKIPPED_STATUS = 80
-SKIPPED_PREFIX = 'skipped: '
-FAILED_PREFIX  = 'hghave check failed: '
-PYTHON = sys.executable
-IMPL_PATH = 'PYTHONPATH'
-if 'java' in sys.platform:
-    IMPL_PATH = 'JYTHONPATH'
-
-requiredtools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"]
-
-defaults = {
-    'jobs': ('HGTEST_JOBS', 1),
-    'timeout': ('HGTEST_TIMEOUT', 180),
-    'port': ('HGTEST_PORT', 20059),
-    'shell': ('HGTEST_SHELL', '/bin/sh'),
-}
-
-def parselistfiles(files, listtype, warn=True):
-    entries = dict()
-    for filename in files:
-        try:
-            path = os.path.expanduser(os.path.expandvars(filename))
-            f = open(path, "r")
-        except IOError, err:
-            if err.errno != errno.ENOENT:
-                raise
-            if warn:
-                print "warning: no such %s file: %s" % (listtype, filename)
-            continue
-
-        for line in f.readlines():
-            line = line.split('#', 1)[0].strip()
-            if line:
-                entries[line] = filename
-
-        f.close()
-    return entries
-
-def parseargs():
-    parser = optparse.OptionParser("%prog [options] [tests]")
-
-    # keep these sorted
-    parser.add_option("--blacklist", action="append",
-        help="skip tests listed in the specified blacklist file")
-    parser.add_option("--whitelist", action="append",
-        help="always run tests listed in the specified whitelist file")
-    parser.add_option("-C", "--annotate", action="store_true",
-        help="output files annotated with coverage")
-    parser.add_option("--child", type="int",
-        help="run as child process, summary to given fd")
-    parser.add_option("-c", "--cover", action="store_true",
-        help="print a test coverage report")
-    parser.add_option("-d", "--debug", action="store_true",
-        help="debug mode: write output of test scripts to console"
-             " rather than capturing and diff'ing it (disables timeout)")
-    parser.add_option("-f", "--first", action="store_true",
-        help="exit on the first test failure")
-    parser.add_option("--inotify", action="store_true",
-        help="enable inotify extension when running tests")
-    parser.add_option("-i", "--interactive", action="store_true",
-        help="prompt to accept changed output")
-    parser.add_option("-j", "--jobs", type="int",
-        help="number of jobs to run in parallel"
-             " (default: $%s or %d)" % defaults['jobs'])
-    parser.add_option("--keep-tmpdir", action="store_true",
-        help="keep temporary directory after running tests")
-    parser.add_option("-k", "--keywords",
-        help="run tests matching keywords")
-    parser.add_option("-l", "--local", action="store_true",
-        help="shortcut for --with-hg=<testdir>/../hg")
-    parser.add_option("-n", "--nodiff", action="store_true",
-        help="skip showing test changes")
-    parser.add_option("-p", "--port", type="int",
-        help="port on which servers should listen"
-             " (default: $%s or %d)" % defaults['port'])
-    parser.add_option("--pure", action="store_true",
-        help="use pure Python code instead of C extensions")
-    parser.add_option("-R", "--restart", action="store_true",
-        help="restart at last error")
-    parser.add_option("-r", "--retest", action="store_true",
-        help="retest failed tests")
-    parser.add_option("-S", "--noskips", action="store_true",
-        help="don't report skip tests verbosely")
-    parser.add_option("--shell", type="string",
-        help="shell to use (default: $%s or %s)" % defaults['shell'])
-    parser.add_option("-t", "--timeout", type="int",
-        help="kill errant tests after TIMEOUT seconds"
-             " (default: $%s or %d)" % defaults['timeout'])
-    parser.add_option("--tmpdir", type="string",
-        help="run tests in the given temporary directory"
-             " (implies --keep-tmpdir)")
-    parser.add_option("-v", "--verbose", action="store_true",
-        help="output verbose messages")
-    parser.add_option("--view", type="string",
-        help="external diff viewer")
-    parser.add_option("--with-hg", type="string",
-        metavar="HG",
-        help="test using specified hg script rather than a "
-             "temporary installation")
-    parser.add_option("-3", "--py3k-warnings", action="store_true",
-        help="enable Py3k warnings on Python 2.6+")
-    parser.add_option('--extra-config-opt', action="append",
-                      help='set the given config opt in the test hgrc')
-
-    for option, (envvar, default) in defaults.items():
-        defaults[option] = type(default)(os.environ.get(envvar, default))
-    parser.set_defaults(**defaults)
-    (options, args) = parser.parse_args()
-
-    # jython is always pure
-    if 'java' in sys.platform or '__pypy__' in sys.modules:
-        options.pure = True
-
-    if options.with_hg:
-        if not (os.path.isfile(options.with_hg) and
-                os.access(options.with_hg, os.X_OK)):
-            parser.error('--with-hg must specify an executable hg script')
-        if not os.path.basename(options.with_hg) == 'hg':
-            sys.stderr.write('warning: --with-hg should specify an hg script\n')
-    if options.local:
-        testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
-        hgbin = os.path.join(os.path.dirname(testdir), 'hg')
-        if not os.access(hgbin, os.X_OK):
-            parser.error('--local specified, but %r not found or not executable'
-                         % hgbin)
-        options.with_hg = hgbin
-
-    options.anycoverage = options.cover or options.annotate
-    if options.anycoverage:
-        try:
-            import coverage
-            covver = version.StrictVersion(coverage.__version__).version
-            if covver < (3, 3):
-                parser.error('coverage options require coverage 3.3 or later')
-        except ImportError:
-            parser.error('coverage options now require the coverage package')
-
-    if options.anycoverage and options.local:
-        # this needs some path mangling somewhere, I guess
-        parser.error("sorry, coverage options do not work when --local "
-                     "is specified")
-
-    global vlog
-    if options.verbose:
-        if options.jobs > 1 or options.child is not None:
-            pid = "[%d]" % os.getpid()
-        else:
-            pid = None
-        def vlog(*msg):
-            iolock.acquire()
-            if pid:
-                print pid,
-            for m in msg:
-                print m,
-            print
-            sys.stdout.flush()
-            iolock.release()
-    else:
-        vlog = lambda *msg: None
-
-    if options.tmpdir:
-        options.tmpdir = os.path.expanduser(options.tmpdir)
-
-    if options.jobs < 1:
-        parser.error('--jobs must be positive')
-    if options.interactive and options.jobs > 1:
-        print '(--interactive overrides --jobs)'
-        options.jobs = 1
-    if options.interactive and options.debug:
-        parser.error("-i/--interactive and -d/--debug are incompatible")
-    if options.debug:
-        if options.timeout != defaults['timeout']:
-            sys.stderr.write(
-                'warning: --timeout option ignored with --debug\n')
-        options.timeout = 0
-    if options.py3k_warnings:
-        if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
-            parser.error('--py3k-warnings can only be used on Python 2.6+')
-    if options.blacklist:
-        options.blacklist = parselistfiles(options.blacklist, 'blacklist')
-    if options.whitelist:
-        options.whitelisted = parselistfiles(options.whitelist, 'whitelist',
-                                             warn=options.child is None)
-    else:
-        options.whitelisted = {}
-
-    return (options, args)
-
-def rename(src, dst):
-    """Like os.rename(), trade atomicity and opened files friendliness
-    for existing destination support.
-    """
-    shutil.copy(src, dst)
-    os.remove(src)
-
-def splitnewlines(text):
-    '''like str.splitlines, but only split on newlines.
-    keep line endings.'''
-    i = 0
-    lines = []
-    while True:
-        n = text.find('\n', i)
-        if n == -1:
-            last = text[i:]
-            if last:
-                lines.append(last)
-            return lines
-        lines.append(text[i:n + 1])
-        i = n + 1
-
-def parsehghaveoutput(lines):
-    '''Parse hghave log lines.
-    Return tuple of lists (missing, failed):
-      * the missing/unknown features
-      * the features for which existence check failed'''
-    missing = []
-    failed = []
-    for line in lines:
-        if line.startswith(SKIPPED_PREFIX):
-            line = line.splitlines()[0]
-            missing.append(line[len(SKIPPED_PREFIX):])
-        elif line.startswith(FAILED_PREFIX):
-            line = line.splitlines()[0]
-            failed.append(line[len(FAILED_PREFIX):])
-
-    return missing, failed
-
-def showdiff(expected, output, ref, err):
-    print
-    for line in difflib.unified_diff(expected, output, ref, err):
-        sys.stdout.write(line)
-
-def findprogram(program):
-    """Search PATH for a executable program"""
-    for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
-        name = os.path.join(p, program)
-        if os.name == 'nt' or os.access(name, os.X_OK):
-            return name
-    return None
-
-def checktools():
-    # Before we go any further, check for pre-requisite tools
-    # stuff from coreutils (cat, rm, etc) are not tested
-    for p in requiredtools:
-        if os.name == 'nt':
-            p += '.exe'
-        found = findprogram(p)
-        if found:
-            vlog("# Found prerequisite", p, "at", found)
-        else:
-            print "WARNING: Did not find prerequisite tool: "+p
-
-def terminate(proc):
-    """Terminate subprocess (with fallback for Python versions < 2.6)"""
-    vlog('# Terminating process %d' % proc.pid)
-    try:
-        getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
-    except OSError:
-        pass
-
-def killdaemons():
-    # Kill off any leftover daemon processes
-    try:
-        fp = open(DAEMON_PIDS)
-        for line in fp:
-            try:
-                pid = int(line)
-            except ValueError:
-                continue
-            try:
-                os.kill(pid, 0)
-                vlog('# Killing daemon process %d' % pid)
-                os.kill(pid, signal.SIGTERM)
-                time.sleep(0.25)
-                os.kill(pid, 0)
-                vlog('# Daemon process %d is stuck - really killing it' % pid)
-                os.kill(pid, signal.SIGKILL)
-            except OSError, err:
-                if err.errno != errno.ESRCH:
-                    raise
-        fp.close()
-        os.unlink(DAEMON_PIDS)
-    except IOError:
-        pass
-
-def cleanup(options):
-    if not options.keep_tmpdir:
-        vlog("# Cleaning up HGTMP", HGTMP)
-        shutil.rmtree(HGTMP, True)
-
-def usecorrectpython():
-    # some tests run python interpreter. they must use same
-    # interpreter we use or bad things will happen.
-    exedir, exename = os.path.split(sys.executable)
-    if exename in ('python', 'python.exe'):
-        path = findprogram(exename)
-        if os.path.dirname(path) == exedir:
-            return
-    else:
-        exename = 'python'
-    vlog('# Making python executable in test path use correct Python')
-    mypython = os.path.join(BINDIR, exename)
-    try:
-        os.symlink(sys.executable, mypython)
-    except AttributeError:
-        # windows fallback
-        shutil.copyfile(sys.executable, mypython)
-        shutil.copymode(sys.executable, mypython)
-
-def installhg(options):
-    vlog("# Performing temporary installation of HG")
-    installerrs = os.path.join("tests", "install.err")
-    pure = options.pure and "--pure" or ""
-
-    # Run installer in hg root
-    script = os.path.realpath(sys.argv[0])
-    hgroot = os.path.dirname(os.path.dirname(script))
-    os.chdir(hgroot)
-    nohome = '--home=""'
-    if os.name == 'nt':
-        # The --home="" trick works only on OS where os.sep == '/'
-        # because of a distutils convert_path() fast-path. Avoid it at
-        # least on Windows for now, deal with .pydistutils.cfg bugs
-        # when they happen.
-        nohome = ''
-    cmd = ('%s setup.py %s clean --all'
-           ' build --build-base="%s"'
-           ' install --force --prefix="%s" --install-lib="%s"'
-           ' --install-scripts="%s" %s >%s 2>&1'
-           % (sys.executable, pure, os.path.join(HGTMP, "build"),
-              INST, PYTHONDIR, BINDIR, nohome, installerrs))
-    vlog("# Running", cmd)
-    if os.system(cmd) == 0:
-        if not options.verbose:
-            os.remove(installerrs)
-    else:
-        f = open(installerrs)
-        for line in f:
-            print line,
-        f.close()
-        sys.exit(1)
-    os.chdir(TESTDIR)
-
-    usecorrectpython()
-
-    vlog("# Installing dummy diffstat")
-    f = open(os.path.join(BINDIR, 'diffstat'), 'w')
-    f.write('#!' + sys.executable + '\n'
-            'import sys\n'
-            'files = 0\n'
-            'for line in sys.stdin:\n'
-            '    if line.startswith("diff "):\n'
-            '        files += 1\n'
-            'sys.stdout.write("files patched: %d\\n" % files)\n')
-    f.close()
-    os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
-
-    if options.py3k_warnings and not options.anycoverage:
-        vlog("# Updating hg command to enable Py3k Warnings switch")
-        f = open(os.path.join(BINDIR, 'hg'), 'r')
-        lines = [line.rstrip() for line in f]
-        lines[0] += ' -3'
-        f.close()
-        f = open(os.path.join(BINDIR, 'hg'), 'w')
-        for line in lines:
-            f.write(line + '\n')
-        f.close()
-
-    hgbat = os.path.join(BINDIR, 'hg.bat')
-    if os.path.isfile(hgbat):
-        # hg.bat expects to be put in bin/scripts while run-tests.py
-        # installation layout put it in bin/ directly. Fix it
-        f = open(hgbat, 'rb')
-        data = f.read()
-        f.close()
-        if '"%~dp0..\python" "%~dp0hg" %*' in data:
-            data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
-                                '"%~dp0python" "%~dp0hg" %*')
-            f = open(hgbat, 'wb')
-            f.write(data)
-            f.close()
-        else:
-            print 'WARNING: cannot fix hg.bat reference to python.exe'
-
-    if options.anycoverage:
-        custom = os.path.join(TESTDIR, 'sitecustomize.py')
-        target = os.path.join(PYTHONDIR, 'sitecustomize.py')
-        vlog('# Installing coverage trigger to %s' % target)
-        shutil.copyfile(custom, target)
-        rc = os.path.join(TESTDIR, '.coveragerc')
-        vlog('# Installing coverage rc to %s' % rc)
-        os.environ['COVERAGE_PROCESS_START'] = rc
-        fn = os.path.join(INST, '..', '.coverage')
-        os.environ['COVERAGE_FILE'] = fn
-
-def outputcoverage(options):
-
-    vlog('# Producing coverage report')
-    os.chdir(PYTHONDIR)
-
-    def covrun(*args):
-        cmd = 'coverage %s' % ' '.join(args)
-        vlog('# Running: %s' % cmd)
-        os.system(cmd)
-
-    if options.child:
-        return
-
-    covrun('-c')
-    omit = ','.join([BINDIR, TESTDIR])
-    covrun('-i', '-r', '"--omit=%s"' % omit) # report
-    if options.annotate:
-        adir = os.path.join(TESTDIR, 'annotated')
-        if not os.path.isdir(adir):
-            os.mkdir(adir)
-        covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
-
-def pytest(test, wd, options, replacements):
-    py3kswitch = options.py3k_warnings and ' -3' or ''
-    cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test)
-    vlog("# Running", cmd)
-    return run(cmd, wd, options, replacements)
-
-def shtest(test, wd, options, replacements):
-    cmd = '"%s"' % test
-    vlog("# Running", cmd)
-    return run(cmd, wd, options, replacements)
-
-needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
-escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
-escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
-escapemap.update({'\\': '\\\\', '\r': r'\r'})
-def escapef(m):
-    return escapemap[m.group(0)]
-def stringescape(s):
-    return escapesub(escapef, s)
-
-def tsttest(test, wd, options, replacements):
-    t = open(test)
-    out = []
-    script = []
-    salt = "SALT" + str(time.time())
-
-    pos = prepos = -1
-    after = {}
-    expected = {}
-    for n, l in enumerate(t):
-        if not l.endswith('\n'):
-            l += '\n'
-        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)
-
-    t.close()
-
-    script.append('echo %s %s $?\n' % (salt, n + 1))
-
-    fd, name = tempfile.mkstemp(suffix='hg-tst')
-
-    try:
-        for l in script:
-            os.write(fd, l)
-        os.close(fd)
-
-        cmd = '"%s" "%s"' % (options.shell, name)
-        vlog("# Running", cmd)
-        exitcode, output = run(cmd, wd, options, replacements)
-        # do not merge output if skipped, return hghave message instead
-        # similarly, with --debug, output is None
-        if exitcode == SKIPPED_STATUS or output is None:
-            return exitcode, output
-    finally:
-        os.remove(name)
-
-    def rematch(el, l):
-        try:
-            # ensure that the regex matches to the end of the string
-            return re.match(el + r'\Z', l)
-        except re.error:
-            # el is an invalid regex
-            return False
-
-    def globmatch(el, l):
-        # The only supported special characters are * and ?. Escaping is
-        # supported.
-        i, n = 0, len(el)
-        res = ''
-        while i < n:
-            c = el[i]
-            i += 1
-            if c == '\\' and el[i] in '*?\\':
-                res += el[i - 1:i + 1]
-                i += 1
-            elif c == '*':
-                res += '.*'
-            elif c == '?':
-                res += '.'
-            else:
-                res += re.escape(c)
-        return rematch(res, l)
-
-    pos = -1
-    postout = []
-    ret = 0
-    for n, l in enumerate(output):
-        lout, lcmd = l, None
-        if salt in l:
-            lout, lcmd = l.split(salt, 1)
-
-        if lout:
-            if lcmd:
-                lout += ' (no-eol)\n'
-
-            el = None
-            if pos in expected and expected[pos]:
-                el = expected[pos].pop(0)
-
-            if el == lout: # perfect match (fast)
-                postout.append("  " + lout)
-            elif (el and
-                  (el.endswith(" (re)\n") and rematch(el[:-6] + '\n', lout) or
-                   el.endswith(" (glob)\n") and globmatch(el[:-8] + '\n', lout)
-                   or el.endswith(" (esc)\n") and
-                      el.decode('string-escape') == l)):
-                postout.append("  " + el) # fallback regex/glob/esc match
-            else:
-                if needescape(lout):
-                    lout = stringescape(lout.rstrip('\n')) + " (esc)\n"
-                postout.append("  " + lout) # let diff deal with it
-
-        if lcmd:
-            # add on last return code
-            ret = int(lcmd.split()[1])
-            if ret != 0:
-                postout.append("  [%s]\n" % ret)
-            if pos in after:
-                postout += after.pop(pos)
-            pos = int(lcmd.split()[0])
-
-    if pos in after:
-        postout += after.pop(pos)
-
-    return exitcode, postout
-
-wifexited = getattr(os, "WIFEXITED", lambda x: False)
-def run(cmd, wd, options, replacements):
-    """Run command in a sub-process, capturing the output (stdout and stderr).
-    Return a tuple (exitcode, output).  output is None in debug mode."""
-    # TODO: Use subprocess.Popen if we're running on Python 2.4
-    if options.debug:
-        proc = subprocess.Popen(cmd, shell=True, cwd=wd)
-        ret = proc.wait()
-        return (ret, None)
-
-    proc = Popen4(cmd, wd, options.timeout)
-    def cleanup():
-        terminate(proc)
-        ret = proc.wait()
-        if ret == 0:
-            ret = signal.SIGTERM << 8
-        killdaemons()
-        return ret
-
-    output = ''
-    proc.tochild.close()
-
-    try:
-        output = proc.fromchild.read()
-    except KeyboardInterrupt:
-        vlog('# Handling keyboard interrupt')
-        cleanup()
-        raise
-
-    ret = proc.wait()
-    if wifexited(ret):
-        ret = os.WEXITSTATUS(ret)
-
-    if proc.timeout:
-        ret = 'timeout'
-
-    if ret:
-        killdaemons()
-
-    for s, r in replacements:
-        output = re.sub(s, r, output)
-    return ret, splitnewlines(output)
-
-def runone(options, test):
-    '''tristate output:
-    None -> skipped
-    True -> passed
-    False -> failed'''
-
-    global results, resultslock, iolock
-
-    testpath = os.path.join(TESTDIR, test)
-
-    def result(l, e):
-        resultslock.acquire()
-        results[l].append(e)
-        resultslock.release()
-
-    def skip(msg):
-        if not options.verbose:
-            result('s', (test, msg))
-        else:
-            iolock.acquire()
-            print "\nSkipping %s: %s" % (testpath, msg)
-            iolock.release()
-        return None
-
-    def fail(msg, ret):
-        if not options.nodiff:
-            iolock.acquire()
-            print "\nERROR: %s %s" % (testpath, msg)
-            iolock.release()
-        if (not ret and options.interactive
-            and os.path.exists(testpath + ".err")):
-            iolock.acquire()
-            print "Accept this change? [n] ",
-            answer = sys.stdin.readline().strip()
-            iolock.release()
-            if answer.lower() in "y yes".split():
-                if test.endswith(".t"):
-                    rename(testpath + ".err", testpath)
-                else:
-                    rename(testpath + ".err", testpath + ".out")
-                result('p', test)
-                return
-        result('f', (test, msg))
-
-    def success():
-        result('p', test)
-
-    def ignore(msg):
-        result('i', (test, msg))
-
-    if (os.path.basename(test).startswith("test-") and '~' not in test and
-        ('.' not in test or test.endswith('.py') or
-         test.endswith('.bat') or test.endswith('.t'))):
-        if not os.path.exists(test):
-            skip("doesn't exist")
-            return None
-    else:
-        vlog('# Test file', test, 'not supported, ignoring')
-        return None # not a supported test, don't record
-
-    if not (options.whitelisted and test in options.whitelisted):
-        if options.blacklist and test in options.blacklist:
-            skip("blacklisted")
-            return None
-
-        if options.retest and not os.path.exists(test + ".err"):
-            ignore("not retesting")
-            return None
-
-        if options.keywords:
-            fp = open(test)
-            t = fp.read().lower() + test.lower()
-            fp.close()
-            for k in options.keywords.lower().split():
-                if k in t:
-                    break
-                else:
-                    ignore("doesn't match keyword")
-                    return None
-
-    vlog("# Test", test)
-
-    # create a fresh hgrc
-    hgrc = open(HGRCPATH, 'w+')
-    hgrc.write('[ui]\n')
-    hgrc.write('slash = True\n')
-    hgrc.write('[defaults]\n')
-    hgrc.write('backout = -d "0 0"\n')
-    hgrc.write('commit = -d "0 0"\n')
-    hgrc.write('tag = -d "0 0"\n')
-    if options.inotify:
-        hgrc.write('[extensions]\n')
-        hgrc.write('inotify=\n')
-        hgrc.write('[inotify]\n')
-        hgrc.write('pidfile=%s\n' % DAEMON_PIDS)
-        hgrc.write('appendpid=True\n')
-    if options.extra_config_opt:
-        for opt in options.extra_config_opt:
-            section, key = opt.split('.', 1)
-            assert '=' in key, ('extra config opt %s must '
-                                'have an = for assignment' % opt)
-            hgrc.write('[%s]\n%s\n' % (section, key))
-    hgrc.close()
-
-    ref = os.path.join(TESTDIR, test+".out")
-    err = os.path.join(TESTDIR, test+".err")
-    if os.path.exists(err):
-        os.remove(err)       # Remove any previous output files
-    try:
-        tf = open(testpath)
-        firstline = tf.readline().rstrip()
-        tf.close()
-    except:
-        firstline = ''
-    lctest = test.lower()
-
-    if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
-        runner = pytest
-    elif lctest.endswith('.t'):
-        runner = tsttest
-        ref = testpath
-    else:
-        # do not try to run non-executable programs
-        if not os.access(testpath, os.X_OK):
-            return skip("not executable")
-        runner = shtest
-
-    # Make a tmp subdirectory to work in
-    testtmp = os.environ["TESTTMP"] = os.environ["HOME"] = \
-        os.path.join(HGTMP, os.path.basename(test))
-
-    os.mkdir(testtmp)
-    ret, out = runner(testpath, testtmp, options, [
-        (re.escape(testtmp), '$TESTTMP'),
-        (r':%s\b' % options.port, ':$HGPORT'),
-        (r':%s\b' % (options.port + 1), ':$HGPORT1'),
-        (r':%s\b' % (options.port + 2), ':$HGPORT2'),
-        ])
-    vlog("# Ret was:", ret)
-
-    mark = '.'
-
-    skipped = (ret == SKIPPED_STATUS)
-
-    # If we're not in --debug mode and reference output file exists,
-    # check test output against it.
-    if options.debug:
-        refout = None                   # to match "out is None"
-    elif os.path.exists(ref):
-        f = open(ref, "r")
-        refout = splitnewlines(f.read())
-        f.close()
-    else:
-        refout = []
-
-    if (ret != 0 or out != refout) and not skipped and not options.debug:
-        # Save errors to a file for diagnosis
-        f = open(err, "wb")
-        for line in out:
-            f.write(line)
-        f.close()
-
-    if skipped:
-        mark = 's'
-        if out is None:                 # debug mode: nothing to parse
-            missing = ['unknown']
-            failed = None
-        else:
-            missing, failed = parsehghaveoutput(out)
-        if not missing:
-            missing = ['irrelevant']
-        if failed:
-            fail("hghave failed checking for %s" % failed[-1], ret)
-            skipped = False
-        else:
-            skip(missing[-1])
-    elif ret == 'timeout':
-        mark = 't'
-        fail("timed out", ret)
-    elif out != refout:
-        mark = '!'
-        if not options.nodiff:
-            iolock.acquire()
-            if options.view:
-                os.system("%s %s %s" % (options.view, ref, err))
-            else:
-                showdiff(refout, out, ref, err)
-            iolock.release()
-        if ret:
-            fail("output changed and returned error code %d" % ret, ret)
-        else:
-            fail("output changed", ret)
-        ret = 1
-    elif ret:
-        mark = '!'
-        fail("returned error code %d" % ret, ret)
-    else:
-        success()
-
-    if not options.verbose:
-        iolock.acquire()
-        sys.stdout.write(mark)
-        sys.stdout.flush()
-        iolock.release()
-
-    killdaemons()
-
-    if not options.keep_tmpdir:
-        shutil.rmtree(testtmp, True)
-    if skipped:
-        return None
-    return ret == 0
-
-_hgpath = None
-
-def _gethgpath():
-    """Return the path to the mercurial package that is actually found by
-    the current Python interpreter."""
-    global _hgpath
-    if _hgpath is not None:
-        return _hgpath
-
-    cmd = '%s -c "import mercurial; print mercurial.__path__[0]"'
-    pipe = os.popen(cmd % PYTHON)
-    try:
-        _hgpath = pipe.read().strip()
-    finally:
-        pipe.close()
-    return _hgpath
-
-def _checkhglib(verb):
-    """Ensure that the 'mercurial' package imported by python is
-    the one we expect it to be.  If not, print a warning to stderr."""
-    expecthg = os.path.join(PYTHONDIR, 'mercurial')
-    actualhg = _gethgpath()
-    if os.path.abspath(actualhg) != os.path.abspath(expecthg):
-        sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
-                         '         (expected %s)\n'
-                         % (verb, actualhg, expecthg))
-
-def runchildren(options, tests):
-    if INST:
-        installhg(options)
-        _checkhglib("Testing")
-
-    optcopy = dict(options.__dict__)
-    optcopy['jobs'] = 1
-
-    # Because whitelist has to override keyword matches, we have to
-    # actually load the whitelist in the children as well, so we allow
-    # the list of whitelist files to pass through and be parsed in the
-    # children, but not the dict of whitelisted tests resulting from
-    # the parse, used here to override blacklisted tests.
-    whitelist = optcopy['whitelisted'] or []
-    del optcopy['whitelisted']
-
-    blacklist = optcopy['blacklist'] or []
-    del optcopy['blacklist']
-    blacklisted = []
-
-    if optcopy['with_hg'] is None:
-        optcopy['with_hg'] = os.path.join(BINDIR, "hg")
-    optcopy.pop('anycoverage', None)
-
-    opts = []
-    for opt, value in optcopy.iteritems():
-        name = '--' + opt.replace('_', '-')
-        if value is True:
-            opts.append(name)
-        elif isinstance(value, list):
-            for v in value:
-                opts.append(name + '=' + str(v))
-        elif value is not None:
-            opts.append(name + '=' + str(value))
-
-    tests.reverse()
-    jobs = [[] for j in xrange(options.jobs)]
-    while tests:
-        for job in jobs:
-            if not tests:
-                break
-            test = tests.pop()
-            if test not in whitelist and test in blacklist:
-                blacklisted.append(test)
-            else:
-                job.append(test)
-    fps = {}
-
-    for j, job in enumerate(jobs):
-        if not job:
-            continue
-        rfd, wfd = os.pipe()
-        childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
-        childtmp = os.path.join(HGTMP, 'child%d' % j)
-        childopts += ['--tmpdir', childtmp]
-        cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job
-        vlog(' '.join(cmdline))
-        fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
-        os.close(wfd)
-    signal.signal(signal.SIGINT, signal.SIG_IGN)
-    failures = 0
-    tested, skipped, failed = 0, 0, 0
-    skips = []
-    fails = []
-    while fps:
-        pid, status = os.wait()
-        fp = fps.pop(pid)
-        l = fp.read().splitlines()
-        try:
-            test, skip, fail = map(int, l[:3])
-        except ValueError:
-            test, skip, fail = 0, 0, 0
-        split = -fail or len(l)
-        for s in l[3:split]:
-            skips.append(s.split(" ", 1))
-        for s in l[split:]:
-            fails.append(s.split(" ", 1))
-        tested += test
-        skipped += skip
-        failed += fail
-        vlog('pid %d exited, status %d' % (pid, status))
-        failures |= status
-    print
-    skipped += len(blacklisted)
-    if not options.noskips:
-        for s in skips:
-            print "Skipped %s: %s" % (s[0], s[1])
-        for s in blacklisted:
-            print "Skipped %s: blacklisted" % s
-    for s in fails:
-        print "Failed %s: %s" % (s[0], s[1])
-
-    _checkhglib("Tested")
-    print "# Ran %d tests, %d skipped, %d failed." % (
-        tested, skipped, failed)
-
-    if options.anycoverage:
-        outputcoverage(options)
-    sys.exit(failures != 0)
-
-results = dict(p=[], f=[], s=[], i=[])
-resultslock = threading.Lock()
-iolock = threading.Lock()
-
-def runqueue(options, tests, results):
-    for test in tests:
-        ret = runone(options, test)
-        if options.first and ret is not None and not ret:
-            break
-
-def runtests(options, tests):
-    global DAEMON_PIDS, HGRCPATH
-    DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
-    HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
-
-    try:
-        if INST:
-            installhg(options)
-            _checkhglib("Testing")
-
-        if options.restart:
-            orig = list(tests)
-            while tests:
-                if os.path.exists(tests[0] + ".err"):
-                    break
-                tests.pop(0)
-            if not tests:
-                print "running all tests"
-                tests = orig
-
-        runqueue(options, tests, results)
-
-        failed = len(results['f'])
-        tested = len(results['p']) + failed
-        skipped = len(results['s'])
-        ignored = len(results['i'])
-
-        if options.child:
-            fp = os.fdopen(options.child, 'w')
-            fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
-            for s in results['s']:
-                fp.write("%s %s\n" % s)
-            for s in results['f']:
-                fp.write("%s %s\n" % s)
-            fp.close()
-        else:
-            print
-            for s in results['s']:
-                print "Skipped %s: %s" % s
-            for s in results['f']:
-                print "Failed %s: %s" % s
-            _checkhglib("Tested")
-            print "# Ran %d tests, %d skipped, %d failed." % (
-                tested, skipped + ignored, failed)
-
-        if options.anycoverage:
-            outputcoverage(options)
-    except KeyboardInterrupt:
-        failed = True
-        print "\ninterrupted!"
-
-    if failed:
-        sys.exit(1)
-
-def main():
-    (options, args) = parseargs()
-    if not options.child:
-        os.umask(022)
-
-        checktools()
-
-    if len(args) == 0:
-        args = os.listdir(".")
-    args.sort()
-
-    tests = args
-
-    # Reset some environment variables to well-known values so that
-    # the tests produce repeatable output.
-    os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C'
-    os.environ['TZ'] = 'GMT'
-    os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>"
-    os.environ['CDPATH'] = ''
-    os.environ['COLUMNS'] = '80'
-    os.environ['GREP_OPTIONS'] = ''
-    os.environ['http_proxy'] = ''
-
-    # unset env related to hooks
-    for k in os.environ.keys():
-        if k.startswith('HG_'):
-            # can't remove on solaris
-            os.environ[k] = ''
-            del os.environ[k]
-
-    global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
-    TESTDIR = os.environ["TESTDIR"] = os.getcwd()
-    if options.tmpdir:
-        options.keep_tmpdir = True
-        tmpdir = options.tmpdir
-        if os.path.exists(tmpdir):
-            # Meaning of tmpdir has changed since 1.3: we used to create
-            # HGTMP inside tmpdir; now HGTMP is tmpdir.  So fail if
-            # tmpdir already exists.
-            sys.exit("error: temp dir %r already exists" % tmpdir)
-
-            # Automatically removing tmpdir sounds convenient, but could
-            # really annoy anyone in the habit of using "--tmpdir=/tmp"
-            # or "--tmpdir=$HOME".
-            #vlog("# Removing temp dir", tmpdir)
-            #shutil.rmtree(tmpdir)
-        os.makedirs(tmpdir)
-    else:
-        tmpdir = tempfile.mkdtemp('', 'hgtests.')
-    HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
-    DAEMON_PIDS = None
-    HGRCPATH = None
-
-    os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
-    os.environ["HGMERGE"] = "internal:merge"
-    os.environ["HGUSER"]   = "test"
-    os.environ["HGENCODING"] = "ascii"
-    os.environ["HGENCODINGMODE"] = "strict"
-    os.environ["HGPORT"] = str(options.port)
-    os.environ["HGPORT1"] = str(options.port + 1)
-    os.environ["HGPORT2"] = str(options.port + 2)
-
-    if options.with_hg:
-        INST = None
-        BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
-
-        # This looks redundant with how Python initializes sys.path from
-        # the location of the script being executed.  Needed because the
-        # "hg" specified by --with-hg is not the only Python script
-        # executed in the test suite that needs to import 'mercurial'
-        # ... which means it's not really redundant at all.
-        PYTHONDIR = BINDIR
-    else:
-        INST = os.path.join(HGTMP, "install")
-        BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
-        PYTHONDIR = os.path.join(INST, "lib", "python")
-
-    os.environ["BINDIR"] = BINDIR
-    os.environ["PYTHON"] = PYTHON
-
-    if not options.child:
-        path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
-        os.environ["PATH"] = os.pathsep.join(path)
-
-        # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
-        # can run .../tests/run-tests.py test-foo where test-foo
-        # adds an extension to HGRC
-        pypath = [PYTHONDIR, TESTDIR]
-        # We have to augment PYTHONPATH, rather than simply replacing
-        # it, in case external libraries are only available via current
-        # PYTHONPATH.  (In particular, the Subversion bindings on OS X
-        # are in /opt/subversion.)
-        oldpypath = os.environ.get(IMPL_PATH)
-        if oldpypath:
-            pypath.append(oldpypath)
-        os.environ[IMPL_PATH] = os.pathsep.join(pypath)
-
-    COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
-
-    vlog("# Using TESTDIR", TESTDIR)
-    vlog("# Using HGTMP", HGTMP)
-    vlog("# Using PATH", os.environ["PATH"])
-    vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
-
-    try:
-        if len(tests) > 1 and options.jobs > 1:
-            runchildren(options, tests)
-        else:
-            runtests(options, tests)
-    finally:
-        time.sleep(1)
-        cleanup(options)
-
-if __name__ == '__main__':
-    main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-keyword	Wed Sep 14 16:19:33 2011 +0100
@@ -0,0 +1,334 @@
+#!/bin/sh
+
+cat <<EOF >> $HGRCPATH
+[extensions]
+keyword =
+mq =
+notify =
+EOF
+
+# demo before [keyword] files are set up
+# would succeed without uisetup otherwise
+echo % hg kwdemo
+hg --quiet kwdemo \
+| sed -e 's![^ ][^ ]*demo.txt,v!/TMP/demo.txt,v!' \
+ -e 's/,v [a-z0-9][a-z0-9]* /,v xxxxxxxxxxxx /' \
+ -e '/[$]Revision/ s/: [a-z0-9][a-z0-9]* /: xxxxxxxxxxxx /' \
+ -e 's! 20[0-9][0-9]/[01][0-9]/[0-3][0-9] [0-2][0-9]:[0-6][0-9]:[0-6][0-9]! 2000/00/00 00:00:00!'
+
+hg --quiet kwdemo "Branch = {branches}"
+
+cat <<EOF >> $HGRCPATH
+[keyword]
+* =
+b = ignore
+[hooks]
+commit=
+commit.test=cp a hooktest
+EOF
+
+hg init Test-bndl
+cd Test-bndl
+
+echo % kwshrink should exit silently in empty/invalid repo
+hg kwshrink
+
+# Symlinks cannot be created on Windows. The bundle was made with:
+#
+# hg init t
+# cd t
+# echo a > a
+# ln -s a sym
+# hg add sym
+# hg ci -m addsym -u mercurial
+# hg bundle --base null ../test-keyword.hg
+#
+hg pull -u "$TESTDIR/test-keyword.hg" \
+    | sed 's/pulling from.*test-keyword.hg/pulling from test-keyword.hg/'
+
+echo 'expand $Id$' > a
+echo 'do not process $Id:' >> a
+echo 'xxx $' >> a
+echo 'ignore $Id$' > b
+echo % cat
+cat a b
+
+echo % addremove
+hg addremove
+echo % status
+hg status
+
+echo % default keyword expansion including commit hook
+#echo % interrupted commit should not change state or run commit hook
+#hg --debug commit
+#echo % status
+#hg status
+
+echo % commit
+hg --debug commit -mab -d '0 0' -u 'User Name <user@example.com>' \
+    | grep -v '^committed changeset'
+echo % status
+hg status
+echo % identify
+hg debugrebuildstate
+hg --quiet identify
+echo % cat
+cat a b
+echo % hg cat
+hg cat a b
+
+echo
+echo % diff a hooktest
+diff a hooktest
+
+echo % removing commit hook from config
+sed -e '/\[hooks\]/,$ d' "$HGRCPATH" > $HGRCPATH.nohook
+mv "$HGRCPATH".nohook "$HGRCPATH"
+rm hooktest
+
+echo % bundle
+hg --quiet bundle --base null ../kw.hg
+
+cd ..
+hg init Test
+cd Test
+
+echo % pull from bundle
+hg pull -u ../kw.hg
+
+echo % touch
+touch a b
+echo % status
+hg status
+
+rm a b
+echo % update
+hg update
+echo % cat
+cat a b
+
+echo % check whether expansion is filewise and filemode is preserved
+echo '$Id$' > c
+echo 'tests for different changenodes' >> c
+chmod 600 c
+ls -l c | cut -b 1-10
+echo % commit c
+hg commit -A -mcndiff -d '1 0' -u 'User Name <user@example.com>'
+ls -l c | cut -b 1-10
+echo % force expansion
+hg -v kwexpand
+echo % compare changenodes in a c
+cat a c
+
+echo % qinit -c
+hg qinit -c
+echo % qimport
+hg qimport -r tip -n mqtest.diff
+echo % qcommit
+hg qcommit -mqtest
+echo % keywords should not be expanded in patch
+cat .hg/patches/mqtest.diff
+echo % qpop
+hg qpop
+echo % qpush
+hg qpush
+echo % cat
+cat c
+echo % qpop and move on
+hg qpop
+
+echo % copy
+hg cp a c
+
+echo % kwfiles added
+hg kwfiles
+
+echo % commit
+hg --debug commit -ma2c -d '1 0' -u 'User Name <user@example.com>' \
+    | grep -v '^committed changeset'
+echo % cat a c
+cat a c
+echo % touch copied c after 1 second
+sleep 1
+touch c
+echo % status
+hg status
+
+echo % kwfiles
+hg kwfiles
+
+echo % diff --rev
+hg diff --rev 1 | grep -v 'b/c'
+
+echo % rollback
+hg rollback
+echo % status
+hg status
+echo % update -C
+hg update --clean
+
+echo % custom keyword expansion
+echo % try with kwdemo
+hg --quiet kwdemo "Xinfo = {author}: {desc}"
+
+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 % interrupted commit should not change state
+#hg commit
+#echo % status
+#hg status
+
+echo % commit
+hg --debug commit -l log -d '2 0' -u 'User Name <user@example.com>' \
+    | grep -v '^committed changeset'
+rm log
+echo % status
+hg status
+echo % verify
+hg verify
+
+echo % cat
+cat a b
+echo % hg cat
+hg cat sym a b
+echo
+echo % annotate
+hg annotate a
+
+echo % remove
+hg debugrebuildstate
+hg remove a
+hg --debug commit -m rma | grep -v '^committed changeset'
+echo % status
+hg status
+echo % rollback
+hg rollback
+echo % status
+hg status
+echo % revert a
+hg revert --no-backup --rev tip a
+echo % cat a
+cat a
+
+echo % clone to test incoming
+cd ..
+# remove updating status for backwards compatibility
+hg clone -r1 Test Test-a | grep -v 'working directory'
+cd Test-a
+cat <<EOF >> .hg/hgrc
+[paths]
+default = ../Test
+EOF
+echo % incoming
+# remove path to temp dir for backwards compatibility
+hg incoming | grep -v '^comparing with'
+
+#sed -e 's/Id.*/& rejecttest/' a > a.new
+#mv a.new a
+#echo % commit rejecttest
+#hg --debug commit -m'rejects?' -d '3 0' -u 'User Name <user@example.com>'
+#echo % export
+#hg export -o ../rejecttest.diff tip
+
+cd ../Test
+#echo % import
+#hg import ../rejecttest.diff
+#echo % cat
+#cat a b
+#echo % rollback
+#hg rollback
+#echo % clean update
+#hg update --clean
+
+echo % kwexpand/kwshrink on selected files
+mkdir x
+echo % copy a x/a
+hg copy a x/a
+echo % kwexpand a
+hg --verbose kwexpand a
+echo % kwexpand x/a should abort
+hg --verbose kwexpand x/a
+cd x
+hg --debug commit -m xa -d '3 0' -u 'User Name <user@example.com>' \
+    | grep -v '^committed changeset'
+echo % cat a
+cat a
+echo % kwshrink a inside directory x
+hg --verbose kwshrink a
+echo % cat a
+cat a
+cd ..
+
+echo % kwexpand nonexistent
+hg kwexpand nonexistent 2>&1 | sed 's/nonexistent:.*/nonexistent:/'
+
+#echo % hg serve
+#hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
+#cat hg.pid >> $DAEMON_PIDS
+#echo % expansion
+#echo % hgweb file
+#("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/file/tip/a/?style=raw')
+#echo % no expansion
+#echo % hgweb annotate
+#("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/annotate/tip/a/?style=raw')
+#echo % hgweb changeset
+#("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/rev/tip/?style=raw')
+#echo % hgweb filediff
+#("$TESTDIR/get-with-headers.py" localhost:$HGPORT '/diff/bb948857c743/a?style=raw')
+#echo % errors encountered
+#cat errors.log
+
+#echo % merge/resolve
+#echo '$Id$' > m
+#hg add m
+#hg commit -m 4kw 
+#echo foo >> m
+#hg commit -m 5foo
+#echo % simplemerge
+#hg update 4
+#echo foo >> m
+#hg commit -m 6foo
+#hg merge
+#hg commit -m simplemerge
+#cat m
+#echo % conflict
+#hg update 4
+#echo bar >> m
+#hg commit -m 8bar
+#hg merge
+#echo % keyword stays outside conflict zone
+#cat m
+#echo % resolve to local
+#HGMERGE=internal:local hg resolve
+#hg commit -m localresolve
+#cat m
+
+echo % switch off expansion
+echo % kwshrink with unknown file u
+cp a u
+hg --verbose kwshrink
+echo % cat
+cat a b
+echo % hg cat
+hg cat a b
+
+rm "$HGRCPATH"
+echo % cat
+cat a b
+echo % hg cat
+hg cat a b
Binary file tests/test-keyword.hg has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-keyword.out	Wed Sep 14 16:19:33 2011 +0100
@@ -0,0 +1,290 @@
+% hg kwdemo
+[extensions]
+keyword = 
+[keyword]
+demo.txt = 
+[keywordmaps]
+Author = {author|user}
+Date = {date|utcdate}
+Header = {root}/{file},v {node|short} {date|utcdate} {author|user}
+Id = {file|basename},v {node|short} {date|utcdate} {author|user}
+RCSFile = {file|basename},v
+RCSfile = {file|basename},v
+Revision = {node|short}
+Source = {root}/{file},v
+$Author: test $
+$Date: 2000/00/00 00:00:00 $
+$Header: /TMP/demo.txt,v xxxxxxxxxxxx 2000/00/00 00:00:00 test $
+$Id: demo.txt,v xxxxxxxxxxxx 2000/00/00 00:00:00 test $
+$RCSFile: demo.txt,v $
+$RCSfile: demo.txt,v $
+$Revision: xxxxxxxxxxxx $
+$Source: /TMP/demo.txt,v $
+[extensions]
+keyword = 
+[keyword]
+demo.txt = 
+[keywordmaps]
+Branch = {branches}
+$Branch: demobranch $
+% kwshrink should exit silently in empty/invalid repo
+pulling from test-keyword.hg
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 1 changesets with 1 changes to 1 files
+1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% cat
+expand $Id$
+do not process $Id:
+xxx $
+ignore $Id$
+% addremove
+adding a
+adding b
+% status
+A a
+A b
+% default keyword expansion including commit hook
+% commit
+a
+b
+overwriting a expanding keywords
+running hook commit.test: cp a hooktest
+% status
+? hooktest
+% identify
+475643d17242
+% cat
+expand $Id: a,v 475643d17242 1970/01/01 00:00:00 user $
+do not process $Id:
+xxx $
+ignore $Id$
+% hg cat
+expand $Id: a,v 475643d17242 1970/01/01 00:00:00 user $
+do not process $Id:
+xxx $
+ignore $Id$
+
+% diff a hooktest
+% removing commit hook from config
+% bundle
+% pull from bundle
+pulling from ../kw.hg
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 2 changesets with 3 changes to 3 files
+3 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% touch
+% status
+% update
+2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% cat
+expand $Id: a,v 475643d17242 1970/01/01 00:00:00 user $
+do not process $Id:
+xxx $
+ignore $Id$
+% check whether expansion is filewise and filemode is preserved
+-rw-------
+% commit c
+adding c
+-rw-------
+% force expansion
+overwriting a expanding keywords
+overwriting c expanding keywords
+% compare changenodes in a c
+expand $Id: a,v 475643d17242 1970/01/01 00:00:00 user $
+do not process $Id:
+xxx $
+$Id: c,v c08ff8b9d54e 1970/01/01 00:00:01 user $
+tests for different changenodes
+% qinit -c
+% qimport
+% qcommit
+% keywords should not be expanded in patch
+# HG changeset patch
+# User User Name <user@example.com>
+# Date 1 0
+# Node ID c08ff8b9d54ebf87b428631bd34b3d8b69cbf72f
+# Parent  475643d17242703d20c30af0fc52b30fd231c6f6
+cndiff
+
+diff -r 475643d17242 -r c08ff8b9d54e c
+--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
++++ b/c	Thu Jan 01 00:00:01 1970 +0000
+@@ -0,0 +1,2 @@
++$Id$
++tests for different changenodes
+% qpop
+Patch queue now empty
+% qpush
+applying mqtest.diff
+Now at: mqtest.diff
+% cat
+$Id: c,v c08ff8b9d54e 1970/01/01 00:00:01 user $
+tests for different changenodes
+% qpop and move on
+Patch queue now empty
+% copy
+% kwfiles added
+a
+c
+% commit
+c
+ c: copy a:0045e12f6c5791aac80ca6cbfd97709a88307292
+overwriting c expanding keywords
+% cat a c
+expand $Id: a,v 475643d17242 1970/01/01 00:00:00 user $
+do not process $Id:
+xxx $
+expand $Id: c,v 1acf8f3ab611 1970/01/01 00:00:01 user $
+do not process $Id:
+xxx $
+% touch copied c after 1 second
+% status
+% kwfiles
+a
+c
+% diff --rev
+diff -r 475643d17242 c
+--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+@@ -0,0 +1,3 @@
++expand $Id$
++do not process $Id:
++xxx $
+% rollback
+rolling back last transaction
+% status
+A c
+% update -C
+0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+% custom keyword expansion
+% try with kwdemo
+[extensions]
+keyword = 
+[keyword]
+* = 
+b = ignore
+demo.txt = 
+[keywordmaps]
+Xinfo = {author}: {desc}
+$Xinfo: test: hg keyword configuration and expansion example $
+% cat
+expand $Id: a,v 475643d17242 1970/01/01 00:00:00 user $
+do not process $Id:
+xxx $
+ignore $Id$
+% hg cat
+expand $Id: a 475643d17242 Thu, 01 Jan 1970 00:00:00 +0000 user $
+do not process $Id:
+xxx $
+ignore $Id$
+% commit
+a
+overwriting a expanding keywords
+% status
+% verify
+checking changesets
+checking manifests
+crosschecking files in changesets and manifests
+checking files
+3 files, 3 changesets, 4 total revisions
+% cat
+expand $Id: a 9ea11ce3ca13 Thu, 01 Jan 1970 00:00:02 +0000 user $
+do not process $Id:
+xxx $
+$Xinfo: User Name <user@example.com>: firstline $
+ignore $Id$
+% hg cat
+expand $Id: a 9ea11ce3ca13 Thu, 01 Jan 1970 00:00:02 +0000 user $
+do not process $Id:
+xxx $
+$Xinfo: User Name <user@example.com>: firstline $
+ignore $Id$
+a
+% annotate
+1: expand $Id$
+1: do not process $Id:
+1: xxx $
+2: $Xinfo$
+% remove
+% status
+% rollback
+rolling back last transaction
+% status
+R a
+% revert a
+% cat a
+expand $Id: a 9ea11ce3ca13 Thu, 01 Jan 1970 00:00:02 +0000 user $
+do not process $Id:
+xxx $
+$Xinfo: User Name <user@example.com>: firstline $
+% clone to test incoming
+requesting all changes
+adding changesets
+adding manifests
+adding file changes
+added 2 changesets with 3 changes to 3 files
+3 files updated, 0 files merged, 0 files removed, 0 files unresolved
+% incoming
+searching for changes
+changeset:   2:9ea11ce3ca13
+tag:         tip
+user:        User Name <user@example.com>
+date:        Thu Jan 01 00:00:02 1970 +0000
+summary:     firstline
+
+% kwexpand/kwshrink on selected files
+% copy a x/a
+% kwexpand a
+overwriting a expanding keywords
+% kwexpand x/a should abort
+abort: outstanding uncommitted changes
+x/a
+ x/a: copy a:779c764182ce5d43e2b1eb66ce06d7b47bfe342e
+overwriting x/a expanding keywords
+% cat a
+expand $Id: x/a 93c8b61c99cd Thu, 01 Jan 1970 00:00:03 +0000 user $
+do not process $Id:
+xxx $
+$Xinfo: User Name <user@example.com>: xa $
+% kwshrink a inside directory x
+overwriting x/a shrinking keywords
+% cat a
+expand $Id$
+do not process $Id:
+xxx $
+$Xinfo$
+% kwexpand nonexistent
+nonexistent:
+% switch off expansion
+% kwshrink with unknown file u
+overwriting a shrinking keywords
+overwriting x/a shrinking keywords
+% cat
+expand $Id$
+do not process $Id:
+xxx $
+$Xinfo$
+ignore $Id$
+% hg cat
+expand $Id: a 9ea11ce3ca13 Thu, 01 Jan 1970 00:00:02 +0000 user $
+do not process $Id:
+xxx $
+$Xinfo: User Name <user@example.com>: firstline $
+ignore $Id$
+% cat
+expand $Id$
+do not process $Id:
+xxx $
+$Xinfo$
+ignore $Id$
+% hg cat
+expand $Id$
+do not process $Id:
+xxx $
+$Xinfo$
+ignore $Id$
--- a/tests/test-keyword.t	Sat Sep 10 13:23:41 2011 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1083 +0,0 @@
-  $ cat <<EOF >> $HGRCPATH
-  > [extensions]
-  > keyword =
-  > mq =
-  > notify =
-  > record =
-  > transplant =
-  > [ui]
-  > interactive = true
-  > EOF
-
-Run kwdemo before [keyword] files are set up
-as it would succeed without uisetup otherwise
-
-  $ hg --quiet kwdemo
-  [extensions]
-  keyword =
-  [keyword]
-  demo.txt = 
-  [keywordset]
-  svn = False
-  [keywordmaps]
-  Author = {author|user}
-  Date = {date|utcdate}
-  Header = {root}/{file},v {node|short} {date|utcdate} {author|user}
-  Id = {file|basename},v {node|short} {date|utcdate} {author|user}
-  RCSFile = {file|basename},v
-  RCSfile = {file|basename},v
-  Revision = {node|short}
-  Source = {root}/{file},v
-  $Author: test $
-  $Date: ????/??/?? ??:??:?? $ (glob)
-  $Header: */demo.txt,v ???????????? ????/??/?? ??:??:?? test $ (glob)
-  $Id: demo.txt,v ???????????? ????/??/?? ??:??:?? test $ (glob)
-  $RCSFile: demo.txt,v $
-  $RCSfile: demo.txt,v $
-  $Revision: ???????????? $ (glob)
-  $Source: */demo.txt,v $ (glob)
-
-  $ hg --quiet kwdemo "Branch = {branches}"
-  [extensions]
-  keyword =
-  [keyword]
-  demo.txt = 
-  [keywordset]
-  svn = False
-  [keywordmaps]
-  Branch = {branches}
-  $Branch: demobranch $
-
-  $ cat <<EOF >> $HGRCPATH
-  > [keyword]
-  > ** =
-  > b = ignore
-  > i = ignore
-  > [hooks]
-  > EOF
-  $ cp $HGRCPATH $HGRCPATH.nohooks
-  > cat <<EOF >> $HGRCPATH
-  > commit=
-  > commit.test=cp a hooktest
-  > EOF
-
-  $ hg init Test-bndl
-  $ cd Test-bndl
-
-kwshrink should exit silently in empty/invalid repo
-
-  $ hg kwshrink
-
-Symlinks cannot be created on Windows.
-A bundle to test this was made with:
- hg init t
- cd t
- echo a > a
- ln -s a sym
- hg add sym
- hg ci -m addsym -u mercurial
- hg bundle --base null ../test-keyword.hg
-
-  $ hg pull -u "$TESTDIR"/bundles/test-keyword.hg
-  pulling from *test-keyword.hg (glob)
-  requesting all changes
-  adding changesets
-  adding manifests
-  adding file changes
-  added 1 changesets with 1 changes to 1 files
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-
-  $ echo 'expand $Id$' > a
-  $ echo 'do not process $Id:' >> a
-  $ echo 'xxx $' >> a
-  $ echo 'ignore $Id$' > b
-
-Output files as they were created
-
-  $ cat a b
-  expand $Id$
-  do not process $Id:
-  xxx $
-  ignore $Id$
-
-no kwfiles
-
-  $ hg kwfiles
-
-untracked candidates
-
-  $ hg -v kwfiles --unknown
-  k a
-
-Add files and check status
-
-  $ hg addremove
-  adding a
-  adding b
-  $ hg status
-  A a
-  A b
-
-
-Default keyword expansion including commit hook
-Interrupted commit should not change state or run commit hook
-
-  $ hg --debug commit
-  abort: empty commit message
-  [255]
-  $ hg status
-  A a
-  A b
-
-Commit with several checks
-
-  $ hg --debug commit -mabsym -u 'User Name <user@example.com>'
-  a
-  b
-  overwriting a expanding keywords
-  running hook commit.test: cp a hooktest
-  committed changeset 1:ef63ca68695bc9495032c6fda1350c71e6d256e9
-  $ hg status
-  ? hooktest
-  $ hg debugrebuildstate
-  $ hg --quiet identify
-  ef63ca68695b
-
-cat files in working directory with keywords expanded
-
-  $ cat a b
-  expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
-  do not process $Id:
-  xxx $
-  ignore $Id$
-
-hg cat files and symlink, no expansion
-
-  $ hg cat sym a b && echo
-  expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
-  do not process $Id:
-  xxx $
-  ignore $Id$
-  a
-
-Test hook execution
-
-  $ diff a hooktest
-
-  $ cp $HGRCPATH.nohooks $HGRCPATH
-  $ rm hooktest
-
-bundle
-
-  $ hg bundle --base null ../kw.hg
-  2 changesets found
-  $ cd ..
-  $ hg init Test
-  $ cd Test
-
-Notify on pull to check whether keywords stay as is in email
-ie. if patch.diff wrapper acts as it should
-
-  $ cat <<EOF >> $HGRCPATH
-  > [hooks]
-  > incoming.notify = python:hgext.notify.hook
-  > [notify]
-  > sources = pull
-  > diffstat = False
-  > maxsubject = 15
-  > [reposubs]
-  > * = Test
-  > EOF
-
-Pull from bundle and trigger notify
-
-  $ hg pull -u ../kw.hg
-  pulling from ../kw.hg
-  requesting all changes
-  adding changesets
-  adding manifests
-  adding file changes
-  added 2 changesets with 3 changes to 3 files
-  Content-Type: text/plain; charset="us-ascii"
-  MIME-Version: 1.0
-  Content-Transfer-Encoding: 7bit
-  Date: * (glob)
-  Subject: changeset in...
-  From: mercurial
-  X-Hg-Notification: changeset a2392c293916
-  Message-Id: <hg.a2392c293916*> (glob)
-  To: Test
-  
-  changeset a2392c293916 in $TESTTMP/Test
-  details: $TESTTMP/Test?cmd=changeset;node=a2392c293916
-  description:
-  	addsym
-  
-  diffs (6 lines):
-  
-  diff -r 000000000000 -r a2392c293916 sym
-  --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
-  +++ b/sym	Sat Feb 09 20:25:47 2008 +0100
-  @@ -0,0 +1,1 @@
-  +a
-  \ No newline at end of file
-  Content-Type: text/plain; charset="us-ascii"
-  MIME-Version: 1.0
-  Content-Transfer-Encoding: 7bit
-  Date:* (glob)
-  Subject: changeset in...
-  From: User Name <user@example.com>
-  X-Hg-Notification: changeset ef63ca68695b
-  Message-Id: <hg.ef63ca68695b*> (glob)
-  To: Test
-  
-  changeset ef63ca68695b in $TESTTMP/Test
-  details: $TESTTMP/Test?cmd=changeset;node=ef63ca68695b
-  description:
-  	absym
-  
-  diffs (12 lines):
-  
-  diff -r a2392c293916 -r ef63ca68695b a
-  --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
-  +++ b/a	Thu Jan 01 00:00:00 1970 +0000
-  @@ -0,0 +1,3 @@
-  +expand $Id$
-  +do not process $Id:
-  +xxx $
-  diff -r a2392c293916 -r ef63ca68695b b
-  --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
-  +++ b/b	Thu Jan 01 00:00:00 1970 +0000
-  @@ -0,0 +1,1 @@
-  +ignore $Id$
-  3 files updated, 0 files merged, 0 files removed, 0 files unresolved
-
-  $ cp $HGRCPATH.nohooks $HGRCPATH
-
-Touch files and check with status
-
-  $ touch a b
-  $ hg status
-
-Update and expand
-
-  $ rm sym a b
-  $ hg update -C
-  3 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ cat a b
-  expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
-  do not process $Id:
-  xxx $
-  ignore $Id$
-
-Check whether expansion is filewise and file mode is preserved
-
-  $ echo '$Id$' > c
-  $ echo 'tests for different changenodes' >> c
-  $ chmod 600 c
-  $ ls -l c | cut -b 1-10
-  -rw-------
-
-commit file c
-
-  $ hg commit -A -mcndiff -d '1 0' -u 'User Name <user@example.com>'
-  adding c
-  $ ls -l c | cut -b 1-10
-  -rw-------
-
-force expansion
-
-  $ hg -v kwexpand
-  overwriting a expanding keywords
-  overwriting c expanding keywords
-
-compare changenodes in a and c
-
-  $ cat a c
-  expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
-  do not process $Id:
-  xxx $
-  $Id: c,v 40a904bbbe4c 1970/01/01 00:00:01 user $
-  tests for different changenodes
-
-record
-
-  $ echo '$Id$' > r
-  $ hg add r
-
-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 a<<EOF
-  > y
-  > y
-  > n
-  > EOF
-  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?] 
-
-  $ hg identify
-  d17e03c92c97+ tip
-  $ hg status
-  M a
-  A r
-
-Cat modified file a
-
-  $ cat a
-  expand $Id: a,v d17e03c92c97 1970/01/01 00:00:01 test $
-  foo
-  do not process $Id:
-  xxx $
-  bar
-
-Diff remaining chunk
-
-  $ hg diff a
-  diff -r d17e03c92c97 a
-  --- a/a	Wed Dec 31 23:59:51 1969 -0000
-  +++ b/a	* (glob)
-  @@ -2,3 +2,4 @@
-   foo
-   do not process $Id:
-   xxx $
-  +bar
-
-  $ hg rollback
-  repository tip rolled back to revision 2 (undo commit)
-  working directory now based on revision 2
-
-Record all chunks in file a
-
-  $ echo foo > msg
-
- - do not use "hg record -m" here!
-
-  $ hg record -l msg -d '1 11' a<<EOF
-  > y
-  > y
-  > y
-  > EOF
-  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?] 
-
-File a should be clean
-
-  $ hg status -A a
-  C a
-
-rollback and revert expansion
-
-  $ cat a
-  expand $Id: a,v 59f969a3b52c 1970/01/01 00:00:01 test $
-  foo
-  do not process $Id:
-  xxx $
-  bar
-  $ hg --verbose rollback
-  repository tip rolled back to revision 2 (undo commit)
-  working directory now based on revision 2
-  overwriting a expanding keywords
-  $ hg status a
-  M a
-  $ cat a
-  expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
-  foo
-  do not process $Id:
-  xxx $
-  bar
-  $ echo '$Id$' > y
-  $ echo '$Id$' > z
-  $ hg add y
-  $ hg commit -Am "rollback only" z
-  $ cat z
-  $Id: z,v 45a5d3adce53 1970/01/01 00:00:00 test $
-  $ hg --verbose rollback
-  repository tip rolled back to revision 2 (undo commit)
-  working directory now based on revision 2
-  overwriting z shrinking keywords
-
-Only z should be overwritten
-
-  $ hg status a y z
-  M a
-  A y
-  A z
-  $ cat z
-  $Id$
-  $ hg forget y z
-  $ rm y z
-
-record added file alone
-
-  $ hg -v record -l msg -d '1 12' r<<EOF
-  > y
-  > EOF
-  diff --git a/r b/r
-  new file mode 100644
-  examine changes to 'r'? [Ynsfdaq?] 
-  r
-  committed changeset 3:899491280810
-  overwriting r expanding keywords
- - status call required for dirstate.normallookup() check
-  $ hg status r
-  $ hg --verbose rollback
-  repository tip rolled back to revision 2 (undo commit)
-  working directory now based on revision 2
-  overwriting r shrinking keywords
-  $ hg forget r
-  $ rm msg r
-  $ hg update -C
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-
-record added keyword ignored file
-
-  $ echo '$Id$' > i
-  $ hg add i
-  $ hg --verbose record -d '1 13' -m recignored<<EOF
-  > y
-  > EOF
-  diff --git a/i b/i
-  new file mode 100644
-  examine changes to 'i'? [Ynsfdaq?] 
-  i
-  committed changeset 3:5f40fe93bbdc
-  $ cat i
-  $Id$
-  $ hg -q rollback
-  $ hg forget i
-  $ rm i
-
-Test patch queue repo
-
-  $ hg init --mq
-  $ hg qimport -r tip -n mqtest.diff
-  $ hg commit --mq -m mqtest
-
-Keywords should not be expanded in patch
-
-  $ cat .hg/patches/mqtest.diff
-  # HG changeset patch
-  # User User Name <user@example.com>
-  # Date 1 0
-  # Node ID 40a904bbbe4cd4ab0a1f28411e35db26341a40ad
-  # Parent  ef63ca68695bc9495032c6fda1350c71e6d256e9
-  cndiff
-  
-  diff -r ef63ca68695b -r 40a904bbbe4c c
-  --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
-  +++ b/c	Thu Jan 01 00:00:01 1970 +0000
-  @@ -0,0 +1,2 @@
-  +$Id$
-  +tests for different changenodes
-
-  $ hg qpop
-  popping mqtest.diff
-  patch queue now empty
-
-qgoto, implying qpush, should expand
-
-  $ hg qgoto mqtest.diff
-  applying mqtest.diff
-  now at: mqtest.diff
-  $ cat c
-  $Id: c,v 40a904bbbe4c 1970/01/01 00:00:01 user $
-  tests for different changenodes
-  $ hg cat c
-  $Id: c,v 40a904bbbe4c 1970/01/01 00:00:01 user $
-  tests for different changenodes
-
-Keywords should not be expanded in filelog
-
-  $ hg --config 'extensions.keyword=!' cat c
-  $Id$
-  tests for different changenodes
-
-qpop and move on
-
-  $ hg qpop
-  popping mqtest.diff
-  patch queue now empty
-
-Copy and show added kwfiles
-
-  $ hg cp a c
-  $ hg kwfiles
-  a
-  c
-
-Commit and show expansion in original and copy
-
-  $ hg --debug commit -ma2c -d '1 0' -u 'User Name <user@example.com>'
-  c
-   c: copy a:0045e12f6c5791aac80ca6cbfd97709a88307292
-  overwriting c expanding keywords
-  committed changeset 2:25736cf2f5cbe41f6be4e6784ef6ecf9f3bbcc7d
-  $ cat a c
-  expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
-  do not process $Id:
-  xxx $
-  expand $Id: c,v 25736cf2f5cb 1970/01/01 00:00:01 user $
-  do not process $Id:
-  xxx $
-
-Touch copied c and check its status
-
-  $ touch c
-  $ hg status
-
-Copy kwfile to keyword ignored file unexpanding keywords
-
-  $ hg --verbose copy a i
-  copying a to i
-  overwriting i shrinking keywords
-  $ head -n 1 i
-  expand $Id$
-  $ hg forget i
-  $ rm i
-
-Copy ignored file to ignored file: no overwriting
-
-  $ hg --verbose copy b i
-  copying b to i
-  $ hg forget i
-  $ rm i
-
-cp symlink file; hg cp -A symlink file (part1)
-- copied symlink points to kwfile: overwrite
-
-  $ cp sym i
-  $ ls -l i
-  -rw-r--r--* (glob)
-  $ head -1 i
-  expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
-  $ hg copy --after --verbose sym i
-  copying sym to i
-  overwriting i shrinking keywords
-  $ head -1 i
-  expand $Id$
-  $ hg forget i
-  $ rm i
-
-Test different options of hg kwfiles
-
-  $ hg kwfiles
-  a
-  c
-  $ hg -v kwfiles --ignore
-  I b
-  I sym
-  $ hg kwfiles --all
-  K a
-  K c
-  I b
-  I sym
-
-Diff specific revision
-
-  $ hg diff --rev 1
-  diff -r ef63ca68695b c
-  --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
-  +++ b/c	* (glob)
-  @@ -0,0 +1,3 @@
-  +expand $Id$
-  +do not process $Id:
-  +xxx $
-
-Status after rollback:
-
-  $ hg rollback
-  repository tip rolled back to revision 1 (undo commit)
-  working directory now based on revision 1
-  $ hg status
-  A c
-  $ hg update --clean
-  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
-
-cp symlink file; hg cp -A symlink file (part2)
-- copied symlink points to kw ignored file: do not overwrite
-
-  $ cat a > i
-  $ ln -s i symignored
-  $ hg commit -Am 'fake expansion in ignored and symlink' i symignored
-  $ cp symignored x
-  $ hg copy --after --verbose symignored x
-  copying symignored to x
-  $ head -n 1 x
-  expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
-  $ hg forget x
-  $ rm x
-
-  $ hg rollback
-  repository tip rolled back to revision 1 (undo commit)
-  working directory now based on revision 1
-  $ hg update --clean
-  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ rm i symignored
-
-Custom keywordmaps as argument to kwdemo
-
-  $ hg --quiet kwdemo "Xinfo = {author}: {desc}"
-  [extensions]
-  keyword =
-  [keyword]
-  ** = 
-  b = ignore
-  demo.txt = 
-  i = ignore
-  [keywordset]
-  svn = False
-  [keywordmaps]
-  Xinfo = {author}: {desc}
-  $Xinfo: test: hg keyword configuration and expansion example $
-
-Configure custom keywordmaps
-
-  $ cat <<EOF >>$HGRCPATH
-  > [keywordmaps]
-  > Id = {file} {node|short} {date|rfc822date} {author|user}
-  > Xinfo = {author}: {desc}
-  > EOF
-
-Cat and hg cat files before custom expansion
-
-  $ cat a b
-  expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
-  do not process $Id:
-  xxx $
-  ignore $Id$
-  $ hg cat sym a b && echo
-  expand $Id: a ef63ca68695b Thu, 01 Jan 1970 00:00:00 +0000 user $
-  do not process $Id:
-  xxx $
-  ignore $Id$
-  a
-
-Write custom keyword and prepare multiline commit message
-
-  $ echo '$Xinfo$' >> a
-  $ cat <<EOF >> log
-  > firstline
-  > secondline
-  > EOF
-
-Interrupted commit should not change state
-
-  $ hg commit
-  abort: empty commit message
-  [255]
-  $ hg status
-  M a
-  ? c
-  ? log
-
-Commit with multiline message and custom expansion
-
-  $ hg --debug commit -l log -d '2 0' -u 'User Name <user@example.com>'
-  a
-  overwriting a expanding keywords
-  committed changeset 2:bb948857c743469b22bbf51f7ec8112279ca5d83
-  $ rm log
-
-Stat, verify and show custom expansion (firstline)
-
-  $ hg status
-  ? c
-  $ hg verify
-  checking changesets
-  checking manifests
-  crosschecking files in changesets and manifests
-  checking files
-  3 files, 3 changesets, 4 total revisions
-  $ cat a b
-  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 $
-  ignore $Id$
-  $ hg cat sym a b && echo
-  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 $
-  ignore $Id$
-  a
-
-annotate
-
-  $ hg annotate a
-  1: expand $Id$
-  1: do not process $Id:
-  1: xxx $
-  2: $Xinfo$
-
-remove with status checks
-
-  $ hg debugrebuildstate
-  $ hg remove a
-  $ hg --debug commit -m rma
-  committed changeset 3:d14c712653769de926994cf7fbb06c8fbd68f012
-  $ hg status
-  ? c
-
-Rollback, revert, and check expansion
-
-  $ hg rollback
-  repository tip rolled back to revision 2 (undo commit)
-  working directory now based on revision 2
-  $ hg status
-  R a
-  ? c
-  $ hg revert --no-backup --rev tip a
-  $ cat a
-  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 $
-
-Clone to test global and local configurations
-
-  $ cd ..
-
-Expansion in destinaton with global configuration
-
-  $ hg --quiet clone Test globalconf
-  $ cat globalconf/a
-  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 destination with local configuration in origin only
-
-  $ hg --quiet --config 'keyword.**=ignore' clone Test localconf
-  $ cat localconf/a
-  expand $Id$
-  do not process $Id:
-  xxx $
-  $Xinfo$
-
-Clone to test incoming
-
-  $ hg clone -r1 Test Test-a
-  adding changesets
-  adding manifests
-  adding file changes
-  added 2 changesets with 3 changes to 3 files
-  updating to branch default
-  3 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ cd Test-a
-  $ cat <<EOF >> .hg/hgrc
-  > [paths]
-  > default = ../Test
-  > EOF
-  $ hg incoming
-  comparing with $TESTTMP/Test
-  searching for changes
-  changeset:   2:bb948857c743
-  tag:         tip
-  user:        User Name <user@example.com>
-  date:        Thu Jan 01 00:00:02 1970 +0000
-  summary:     firstline
-  
-Imported patch should not be rejected
-
-  $ python -c \
-  > 'import re; s=re.sub("(Id.*)","\\1 rejecttest",open("a").read()); open("a","wb").write(s);'
-  $ hg --debug commit -m'rejects?' -d '3 0' -u 'User Name <user@example.com>'
-  a
-  overwriting a expanding keywords
-  committed changeset 2:85e279d709ffc28c9fdd1b868570985fc3d87082
-  $ hg export -o ../rejecttest.diff tip
-  $ cd ../Test
-  $ hg import ../rejecttest.diff
-  applying ../rejecttest.diff
-  $ cat a b
-  expand $Id: a 4e0994474d25 Thu, 01 Jan 1970 00:00:03 +0000 user $ rejecttest
-  do not process $Id: rejecttest
-  xxx $
-  $Xinfo: User Name <user@example.com>: rejects? $
-  ignore $Id$
-
-  $ hg rollback
-  repository tip rolled back to revision 2 (undo commit)
-  working directory now based on revision 2
-  $ hg update --clean
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-
-kwexpand/kwshrink on selected files
-
-  $ mkdir x
-  $ hg copy a x/a
-  $ hg --verbose kwshrink a
-  overwriting a shrinking keywords
- - sleep required for dirstate.normal() check
-  $ sleep 1
-  $ hg status a
-  $ hg --verbose kwexpand a
-  overwriting a expanding keywords
-  $ hg status a
-
-kwexpand x/a should abort
-
-  $ hg --verbose kwexpand x/a
-  abort: outstanding uncommitted changes
-  [255]
-  $ cd x
-  $ hg --debug commit -m xa -d '3 0' -u 'User Name <user@example.com>'
-  x/a
-   x/a: copy a:779c764182ce5d43e2b1eb66ce06d7b47bfe342e
-  overwriting x/a expanding keywords
-  committed changeset 3:b4560182a3f9a358179fd2d835c15e9da379c1e4
-  $ cat a
-  expand $Id: x/a b4560182a3f9 Thu, 01 Jan 1970 00:00:03 +0000 user $
-  do not process $Id:
-  xxx $
-  $Xinfo: User Name <user@example.com>: xa $
-
-kwshrink a inside directory x
-
-  $ hg --verbose kwshrink a
-  overwriting x/a shrinking keywords
-  $ cat a
-  expand $Id$
-  do not process $Id:
-  xxx $
-  $Xinfo$
-  $ cd ..
-
-kwexpand nonexistent
-
-  $ hg kwexpand nonexistent
-  nonexistent:* (glob)
-
-
-hg serve
- - expand with hgweb file
- - no expansion with hgweb annotate/changeset/filediff
- - check errors
-
-  $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
-  $ cat hg.pid >> $DAEMON_PIDS
-  $ $TESTDIR/get-with-headers.py localhost:$HGPORT '/file/tip/a/?style=raw'
-  200 Script output follows
-  
-  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 $
-  $ $TESTDIR/get-with-headers.py localhost:$HGPORT '/annotate/tip/a/?style=raw'
-  200 Script output follows
-  
-  
-  user@1: expand $Id$
-  user@1: do not process $Id:
-  user@1: xxx $
-  user@2: $Xinfo$
-  
-  
-  
-  
-  $ $TESTDIR/get-with-headers.py localhost:$HGPORT '/rev/tip/?style=raw'
-  200 Script output follows
-  
-  
-  # HG changeset patch
-  # User User Name <user@example.com>
-  # Date 3 0
-  # Node ID b4560182a3f9a358179fd2d835c15e9da379c1e4
-  # Parent  bb948857c743469b22bbf51f7ec8112279ca5d83
-  xa
-  
-  diff -r bb948857c743 -r b4560182a3f9 x/a
-  --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
-  +++ b/x/a	Thu Jan 01 00:00:03 1970 +0000
-  @@ -0,0 +1,4 @@
-  +expand $Id$
-  +do not process $Id:
-  +xxx $
-  +$Xinfo$
-  
-  $ $TESTDIR/get-with-headers.py localhost:$HGPORT '/diff/bb948857c743/a?style=raw'
-  200 Script output follows
-  
-  
-  diff -r ef63ca68695b -r bb948857c743 a
-  --- a/a	Thu Jan 01 00:00:00 1970 +0000
-  +++ b/a	Thu Jan 01 00:00:02 1970 +0000
-  @@ -1,3 +1,4 @@
-   expand $Id$
-   do not process $Id:
-   xxx $
-  +$Xinfo$
-  
-  
-  
-  
-  $ cat errors.log
-
-Prepare merge and resolve tests
-
-  $ echo '$Id$' > m
-  $ hg add m
-  $ hg commit -m 4kw 
-  $ echo foo >> m
-  $ hg commit -m 5foo
-
-simplemerge
-
-  $ hg update 4
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ echo foo >> m
-  $ hg commit -m 6foo
-  created new head
-  $ hg merge
-  0 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  (branch merge, don't forget to commit)
-  $ hg commit -m simplemerge
-  $ cat m
-  $Id: m 27d48ee14f67 Thu, 01 Jan 1970 00:00:00 +0000 test $
-  foo
-
-conflict: keyword should stay outside conflict zone
-
-  $ hg update 4
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ echo bar >> m
-  $ hg commit -m 8bar
-  created new head
-  $ hg merge
-  merging m
-  warning: conflicts during merge.
-  merging m failed!
-  0 files updated, 0 files merged, 0 files removed, 1 files unresolved
-  use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
-  [1]
-  $ cat m
-  $Id$
-  <<<<<<< local
-  bar
-  =======
-  foo
-  >>>>>>> other
-
-resolve to local
-
-  $ HGMERGE=internal:local hg resolve -a
-  $ hg commit -m localresolve
-  $ cat m
-  $Id: m 800511b3a22d Thu, 01 Jan 1970 00:00:00 +0000 test $
-  bar
-
-Test restricted mode with transplant -b
-
-  $ hg update 6
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ hg branch foo
-  marked working directory as branch foo
-  $ mv a a.bak
-  $ echo foobranch > a
-  $ cat a.bak >> a
-  $ rm a.bak
-  $ hg commit -m 9foobranch
-  $ hg update default
-  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
-  $ hg -y transplant -b foo tip
-  applying 4aa30d025d50
-  4aa30d025d50 transplanted to e00abbf63521
-
-Expansion in changeset but not in file
-
-  $ hg tip -p
-  changeset:   11:e00abbf63521
-  tag:         tip
-  parent:      9:800511b3a22d
-  user:        test
-  date:        Thu Jan 01 00:00:00 1970 +0000
-  summary:     9foobranch
-  
-  diff -r 800511b3a22d -r e00abbf63521 a
-  --- a/a	Thu Jan 01 00:00:00 1970 +0000
-  +++ b/a	Thu Jan 01 00:00:00 1970 +0000
-  @@ -1,3 +1,4 @@
-  +foobranch
-   expand $Id$
-   do not process $Id:
-   xxx $
-  
-  $ head -n 2 a
-  foobranch
-  expand $Id: a e00abbf63521 Thu, 01 Jan 1970 00:00:00 +0000 test $
-
-Turn off expansion
-
-  $ hg -q rollback
-  $ hg -q update -C
-
-kwshrink with unknown file u
-
-  $ cp a u
-  $ hg --verbose kwshrink
-  overwriting a shrinking keywords
-  overwriting m shrinking keywords
-  overwriting x/a shrinking keywords
-
-Keywords shrunk in working directory, but not yet disabled
- - cat shows unexpanded keywords
- - hg cat shows expanded keywords
-
-  $ cat a b
-  expand $Id$
-  do not process $Id:
-  xxx $
-  $Xinfo$
-  ignore $Id$
-  $ hg cat sym a b && echo
-  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 $
-  ignore $Id$
-  a
-
-Now disable keyword expansion
-
-  $ rm "$HGRCPATH"
-  $ cat a b
-  expand $Id$
-  do not process $Id:
-  xxx $
-  $Xinfo$
-  ignore $Id$
-  $ hg cat sym a b && echo
-  expand $Id$
-  do not process $Id:
-  xxx $
-  $Xinfo$
-  ignore $Id$
-  a