(0.9.2compat) discard wread/wwrite approach 0.9.2compat
authorChristian Ebert <blacktrash@gmx.net>
Wed, 06 Feb 2008 18:38:14 +0100
branch0.9.2compat
changeset 380 0ed26effe190
parent 379 290d023e8306 (diff)
parent 370 a8983fed9a9e (current diff)
child 387 ecb2935f9bb8
(0.9.2compat) discard wread/wwrite approach
hgkw/keyword.py
tests/test-keyword
tests/test-keyword.out
--- a/hgkw/keyword.py	Tue Feb 05 17:23:24 2008 +0100
+++ b/hgkw/keyword.py	Wed Feb 06 18:38:14 2008 +0100
@@ -85,20 +85,46 @@
         Or, better, use bundle/unbundle to share changes.
 '''
 
-from mercurial import commands, cmdutil, context
-from mercurial import localrepo, revlog, templater, util
+from mercurial import commands, cmdutil, context, fancyopts
+from mercurial import filelog, localrepo, revlog, templater, util
 from mercurial.node import *
 from mercurial.i18n import gettext as _
-import mimetypes, os.path, re, shutil, tempfile, time
+import getopt, os.path, re, shutil, sys, tempfile, time
+
+commands.optionalrepo += ' kwdemo'
+
+# hg commands that do not act on keywords
+nokwcommands = ('add addremove bundle copy export grep identify incoming init'
+                ' log outgoing push remove rename rollback tip convert')
+
+# hg commands that trigger expansion only when writing to working dir,
+# not when reading filelog, and unexpand when reading from working dir
+restricted = 'diff1 record qfold qimport qnew qpush qrefresh qrecord'
+
+def utcdate(date):
+    '''Returns hgdate in cvs-like UTC format.'''
+    return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
+
+
+_kwtemplater = None
+
+# backwards compatibility hacks
 
 try:
     # avoid spurious rejects if patchfile is available
     from mercurial.patch import patchfile
     _patchfile_init = patchfile.__init__
+
+    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.'''
+        _patchfile_init(self, ui, fname, missing=missing)
+        if _kwtemplater.matcher(self.fname):
+            # shrink keywords read from working dir
+            kwshrunk = _kwtemplater.shrink(''.join(self.lines))
+            self.lines = kwshrunk.splitlines(True)
 except ImportError:
-    _patchfile_init = None
-
-# backwards compatibility hacks
+    pass
 
 try:
     # templatefilters module introduced in 9f1e6ab76069
@@ -110,13 +136,24 @@
     template_firstline = templater.firstline
 
 try:
-    # webcommands module introduced in 08887121a652
-    from mercurial.hgweb import webcommands
-    _webcommands = True
-    kwweb_func = webcommands.rawfile
+    # cmdutil.parse moves to dispatch._parse in 18a9fbb5cd78
+    # also avoid name conflict with other dispatch package(s)
+    from mercurial.dispatch import _parse
 except ImportError:
-    from mercurial.hgweb import hgweb_mod
-    _webcommands = False
+    try:
+        # commands.parse moves to cmdutil.parse in 0c61124ad877
+        _parse = cmdutil.parse
+    except AttributeError:
+        _parse = commands.parse
+
+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.'''
@@ -134,12 +171,64 @@
     except AttributeError:
         return f
 
+# 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 = {}
 
-commands.optionalrepo += ' kwdemo'
+    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
 
-def utcdate(date):
-    '''Returns hgdate in cvs-like UTC format.'''
-    return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
+        # 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):
     '''
@@ -156,11 +245,13 @@
         'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
     }
 
-    def __init__(self, ui, repo, inc, exc):
+    def __init__(self, ui, repo, inc, exc, restricted):
         self.ui = ui
         self.repo = repo
         self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
-        self.ctx = None
+        self.restricted = restricted
+        self.commitnode = None
+        self.path = ''
 
         kwmaps = self.ui.configitems('keywordmaps')
         if kwmaps: # override default templates
@@ -184,62 +275,86 @@
             return cmdutil.changeset_templater(self.ui, self.repo,
                                                False, None, '', False)
 
-    def substitute(self, path, data, node, subfunc):
-        '''Obtains file's changenode if node not given,
+    def substitute(self, node, data, subfunc):
+        '''Obtains file's changenode if commit node not given,
         and calls given substitution function.'''
-        if node is None:
-            # kwrepo.wwrite except when overwriting on commit
-            if self.ctx is None:
-                self.ctx = self.repo.changectx()
-            try:
-                fnode = self.ctx.filenode(path)
-                fl = self.repo.file(path)
-                c = context.filectx(self.repo, path, fileid=fnode, filelog=fl)
-                node = c.node()
-            except revlog.LookupError:
-                # eg: convert
-                return subfunc == self.re_kw.sub and data or (data, None)
+        if self.commitnode:
+            fnode = self.commitnode
+        else:
+            c = context.filectx(self.repo, self.path, fileid=node)
+            fnode = c.node()
 
         def kwsub(mobj):
             '''Substitutes keyword using corresponding template.'''
             kw = mobj.group(1)
             self.ct.use_template(self.templates[kw])
             self.ui.pushbuffer()
-            self.ct.show(changenode=node, root=self.repo.root, file=path)
-            ekw = template_firstline(self.ui.popbuffer())
-            return '$%s: %s $' % (kw, ekw)
+            self.ct.show(changenode=fnode, root=self.repo.root, file=self.path)
+            return '$%s: %s $' % (kw, template_firstline(self.ui.popbuffer()))
 
         return subfunc(kwsub, data)
 
-    def expand(self, path, data, node):
+    def expand(self, node, data):
         '''Returns data with keywords expanded.'''
-        if util.binary(data):
+        if self.restricted or util.binary(data):
             return data
-        return self.substitute(path, data, node, self.re_kw.sub)
+        return self.substitute(node, data, self.re_kw.sub)
 
-    def process(self, path, data, expand, ctx, node):
+    def process(self, node, data, expand):
         '''Returns a tuple: data, count.
         Count is number of keywords/keyword substitutions,
         telling caller whether to act on file containing data.'''
         if util.binary(data):
             return data, None
         if expand:
-            self.ctx = ctx
-            return self.substitute(path, data, node, self.re_kw.subn)
-        return self.re_kw.subn(r'$\1$', data)
+            return self.substitute(node, data, self.re_kw.subn)
+        return data, self.re_kw.search(data)
 
-    def shrink(self, data):
+    def shrink(self, text):
         '''Returns text with all keyword substitutions removed.'''
-        if util.binary(data):
-            return data
-        return self.re_kw.sub(r'$\1$', data)
+        if util.binary(text):
+            return text
+        return self.re_kw.sub(r'$\1$', text)
+
+class kwfilelog(filelog.filelog):
+    '''
+    Subclass of filelog to hook into its read, add, cmp methods.
+    Keywords are "stored" unexpanded, and processed on reading.
+    '''
+    def __init__(self, opener, path):
+        super(kwfilelog, self).__init__(opener, path)
+        _kwtemplater.path = path
+
+    def kwctread(self, node, expand):
+        '''Reads expanding and counting keywords, called from _overwrite.'''
+        data = super(kwfilelog, self).read(node)
+        return _kwtemplater.process(node, data, expand)
 
+    def read(self, node):
+        '''Expands keywords when reading filelog.'''
+        data = super(kwfilelog, self).read(node)
+        return _kwtemplater.expand(node, data)
 
+    def add(self, text, meta, tr, link, p1=None, p2=None):
+        '''Removes keyword substitutions when adding to filelog.'''
+        text = _kwtemplater.shrink(text)
+        return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2)
+
+    def cmp(self, node, text):
+        '''Removes keyword substitutions for comparison.'''
+        text = _kwtemplater.shrink(text)
+        if self.renamed(node):
+            t2 = super(kwfilelog, self).read(node)
+            return t2 != text
+        return revlog.revlog.cmp(self, node, text)
+
+def _iskwfile(f, link):
+    return not link(f) and _kwtemplater.matcher(f)
 
 def _status(ui, repo, *pats, **opts):
     '''Bails out if [keyword] configuration is not active.
     Returns status of working directory.'''
-    if hasattr(repo, '_kwt'):
+    if _kwtemplater:
         files, match, anypats = cmdutil.matchpats(repo, pats, opts)
         return repo.status(files=files, match=match, list_clean=True)
     if ui.configitems('keyword'):
@@ -250,23 +365,23 @@
     '''Overwrites selected files expanding/shrinking keywords.'''
     ctx = repo.changectx(node)
     mf = ctx.manifest()
-    if node is not None:
-        # commit
+    if node is not None:   # commit
+        _kwtemplater.commitnode = node
         files = [f for f in ctx.files() if f in mf]
         notify = ui.debug
-    else:
-        # kwexpand/kwshrink
+    else:                  # kwexpand/kwshrink
         notify = ui.note
-    candidates = [f for f in files if not mf.linkf(f) and repo._kwt.matcher(f)]
+    candidates = [f for f in files if _iskwfile(f, mf.linkf)]
     if candidates:
         overwritten = []
         candidates.sort()
         action = expand and 'expanding' or 'shrinking'
         for f in candidates:
-            data, kwfound = repo._wreadkwct(f, expand, ctx, node)
+            fp = repo.file(f, kwmatch=True)
+            data, kwfound = fp.kwctread(mf[f], expand)
             if kwfound:
                 notify(_('overwriting %s %s keywords\n') % (f, action))
-                repo.wwrite(f, data, mf.flags(f), overwrite=True)
+                _wwrite(repo, f, data, mf)
                 overwritten.append(f)
         _normal(repo, overwritten)
 
@@ -284,37 +399,6 @@
     finally:
         del wlock, lock
 
-def cat(ui, repo, file1, *pats, **opts):
-    '''output the current or given revision of files expanding keywords
-
-    Print the specified files as they were at the given revision.
-    If no revision is given, the parent of the working directory is used,
-    or tip if no revision is checked out.
-
-    Output may be to a file, in which case the name of the file is
-    given using a format string.  The formatting rules are the same as
-    for the export command, with the following additions:
-
-    %s   basename of file being printed
-    %d   dirname of file being printed, or '.' if in repo root
-    %p   root-relative path name of file being printed
-    '''
-    ctx = repo.changectx(opts['rev'])
-    try:
-        repo._kwt.ctx = ctx
-        kw = True
-    except AttributeError:
-        kw = False
-    err = 1
-    for src, abs, rel, exact in cmdutil.walk(repo, (file1,) + pats, opts,
-                                             ctx.node()):
-        fp = cmdutil.make_file(repo, opts['output'], ctx.node(), pathname=abs)
-        data = ctx.filectx(abs).data()
-        if kw and repo._kwt.matcher(abs):
-            data = repo._kwt.expand(abs, data, None)
-        fp.write(data)
-        err = 0
-    return err
 
 def demo(ui, repo, *args, **opts):
     '''print [keywordmaps] configuration and an expansion example
@@ -392,7 +476,7 @@
     repo.commit(text=msg)
     format = ui.verbose and ' in %s' % path or ''
     demostatus('%s keywords expanded%s' % (kwstatus, format))
-    ui.write(repo.wopener(fn).read())
+    ui.write(repo.wread(fn))
     ui.debug(_('\nremoving temporary repo %s\n') % tmpdir)
     shutil.rmtree(tmpdir, ignore_errors=True)
 
@@ -419,9 +503,9 @@
     if opts.get('untracked'):
         files += unknown
     files.sort()
-    # use full def of repo._link for backwards compatibility
-    kwfiles = [f for f in files if
-               not os.path.islink(repo.wjoin(f)) and repo._kwt.matcher(f)]
+    # use the full definition of repo._link for backwards compatibility
+    kwfiles = [f for f in files if _kwtemplater.matcher(f)
+               and not os.path.islink(repo.wjoin(f))]
     cwd = pats and repo.getcwd() or ''
     kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
     if opts.get('all') or opts.get('ignore'):
@@ -444,10 +528,31 @@
 
 
 def reposetup(ui, repo):
-    if not repo.local() or repo.root.endswith('/.hg/patches'):
+    '''Sets up repo as kwrepo for keyword substitution.
+    Overrides file method to return kwfilelog instead of filelog
+    if file matches user configuration.
+    Wraps commit to overwrite configured files with updated
+    keyword substitutions.
+    This is done for local repos only, and only if there are
+    files configured at all for keyword substitution.'''
+
+    if not repo.local():
         return
 
-    inc, exc = [], ['.hgtags', '.hg_archival.txt']
+    hgcmd, func, args, opts, cmdopts = _parse(ui, sys.argv[1:])
+    if hgcmd in nokwcommands.split():
+        return
+
+    if hgcmd == 'diff':
+        # only expand if comparing against working dir
+        node1, node2 = cmdutil.revpair(repo, cmdopts.get('rev'))
+        if node2 is not None:
+            return
+        # shrink if rev is not current node
+        if node1 is not None and node1 != repo.changectx().node():
+            hgcmd = 'diff1'
+
+    inc, exc = [], ['.hgtags']
     for pat, opt in ui.configitems('keyword'):
         if opt != 'ignore':
             inc.append(pat)
@@ -456,34 +561,24 @@
     if not inc:
         return
 
+    global _kwtemplater
+    _restricted = hgcmd in restricted.split()
+    _kwtemplater = kwtemplater(ui, repo, inc, exc, _restricted)
+
     class kwrepo(repo.__class__):
-        def _wreadkwct(self, filename, expand, ctx, node):
-            '''Reads filename and returns tuple of data with keywords
-            expanded/shrunk and count of keywords (for _overwrite).'''
-            data = super(kwrepo, self).wread(filename)
-            return self._kwt.process(filename, data, expand, ctx, node)
+        def file(self, f, kwmatch=False):
+            if f[0] == '/':
+                f = f[1:]
+            if kwmatch or _kwtemplater.matcher(f):
+                return kwfilelog(self.sopener, f)
+            return filelog.filelog(self.sopener, f)
 
         def wread(self, filename):
             data = super(kwrepo, self).wread(filename)
-            if self._kwt.matcher(filename):
-                return self._kwt.shrink(data)
+            if _restricted and _kwtemplater.matcher(filename):
+                return _kwtemplater.shrink(data)
             return data
 
-        def wwrite(self, filename, data, flags=None, overwrite=False):
-            if not overwrite and self._kwt.matcher(filename):
-                data = self._kwt.expand(filename, data, None)
-            try:
-                super(kwrepo, self).wwrite(filename, data, flags)
-            except (AttributeError, TypeError):
-                # 656e06eebda7 removed file descriptor argument
-                # 67982d3ee76c added flags argument
-                super(kwrepo, self).wwrite(filename, data)
-
-        def wwritedata(self, filename, data):
-            if self._kwt.matcher(filename):
-                data = self._kwt.expand(filename, data, None)
-            return super(kwrepo, self).wwritedata(filename, data)
-
         def _commit(self, files, text, user, date, match, force, lock, wlock,
                     force_editor, p1, p2, extra, empty_ok):
             '''Private commit wrapper for backwards compatibility.'''
@@ -558,94 +653,14 @@
             finally:
                 del _wlock, _lock
 
-    kwt = kwrepo._kwt = kwtemplater(ui, repo, inc, exc)
-
-    if _patchfile_init:
-
-        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.'''
-            _patchfile_init(self, ui, fname, missing=missing)
-            if kwt.matcher(self.fname):
-                # shrink keywords read from working dir
-                kwshrunk = kwt.shrink(''.join(self.lines))
-                self.lines = kwshrunk.splitlines(True)
-
-        patchfile.__init__ = kwpatchfile_init
-
-    if _webcommands:
-        def kwweb_rawfile(web, req, tmpl):
-            '''Monkeypatch webcommands.rawfile so it expands keywords.'''
-            path = web.cleanpath(req.form.get('file', [''])[0])
-            if not path:
-                content = web.manifest(tmpl, web.changectx(req), path)
-                req.respond(webcommands.HTTP_OK, web.ctype)
-                return content
-            try:
-                fctx = web.filectx(req)
-            except revlog.LookupError:
-                content = web.manifest(tmpl, web.changectx(req), path)
-                req.respond(webcommands.HTTP_OK, web.ctype)
-                return content
-            path = fctx.path()
-            text = fctx.data()
-            if kwt.matcher(path):
-                text = kwt.expand(path, text, fctx.node())
-            mt = mimetypes.guess_type(path)[0]
-            if mt is None or util.binary(text):
-                mt = mt or 'application/octet-stream'
-            req.respond(webcommands.HTTP_OK, mt, path, len(text))
-            return [text]
-
-        webcommands.rawfile = kwweb_rawfile
-
-    else:
-        def kwweb_filerevision(self, fctx):
-            '''Monkeypatch hgweb_mod.hgweb.filerevision so keywords are
-            expanded in raw file output.'''
-            f = fctx.path()
-            text = fctx.data()
-            fl = fctx.filelog()
-            n = fctx.filenode()
-            parity = hgweb_mod.paritygen(self.stripecount)
-            mt = mimetypes.guess_type(f)[0]
-            rawtext = text
-            if kwt.matcher(f):
-                rawtext = kwt.expand(f, text, fctx.node())
-            if util.binary(text):
-                mt = mt or 'application/octet-stream'
-                text = "(binary:%s)" % mt
-            mt = mt or 'text/plain'
-            def lines():
-                for l, t in enumerate(text.splitlines(1)):
-                    yield {"line": t,
-                           "linenumber": "% 6d" % (l + 1),
-                           "parity": parity.next()}
-            yield self.t("filerevision",
-                         file=f,
-                         path=hgweb_mod._up(f),
-                         text=lines(),
-                         raw=rawtext,
-                         mimetype=mt,
-                         rev=fctx.rev(),
-                         node=hex(fctx.node()),
-                         author=fctx.user(),
-                         date=fctx.date(),
-                         desc=fctx.description(),
-                         parent=self.siblings(fctx.parents()),
-                         child=self.siblings(fctx.children()),
-                         rename=self.renamelink(fl, n),
-                         permissions=fctx.manifest().flags(f))
-
-        hgweb_mod.hgweb.filerevision = kwweb_filerevision
-
     repo.__class__ = kwrepo
+    try:
+        patchfile.__init__ = _kwpatchfile_init
+    except NameError:
+        pass
 
 
 cmdtable = {
-    'kwcat':
-        (cat, commands.table['cat'][1],
-         _('hg kwcat [OPTION]... FILE...')),
     'kwdemo':
         (demo,
          [('d', 'default', None, _('show default keyword template maps')),
--- a/tests/test-keyword	Tue Feb 05 17:23:24 2008 +0100
+++ b/tests/test-keyword	Wed Feb 06 18:38:14 2008 +0100
@@ -58,8 +58,8 @@
 hg --quiet identify
 echo % cat
 cat a b
-echo % hg kwcat
-hg kwcat a b
+echo % hg cat
+hg cat a b
 
 echo
 echo % diff a hooktest
@@ -91,20 +91,16 @@
 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
+hg qpop
+echo % qpush
+hg qpush
+echo % cat
+cat c
 echo % qpop and move on
 hg qpop
 
@@ -149,8 +145,8 @@
 
 echo % cat
 cat a b
-echo % hg kwcat
-hg kwcat a b
+echo % hg cat
+hg cat a b
 
 echo '$Xinfo$' >> a
 cat <<EOF >> log
@@ -171,8 +167,8 @@
 
 echo % cat
 cat a b
-echo % hg kwcat
-hg kwcat a b
+echo % hg cat
+hg cat a b
 
 echo % remove
 hg remove a
@@ -238,10 +234,17 @@
 echo % kwexpand nonexistent
 hg kwexpand nonexistent
 
+echo % switch off expansion
 echo % kwshrink with unknown file u
 cp a u
 hg --verbose kwshrink
 echo % cat
 cat a b
-echo % hg kwcat
-hg kwcat a b
+echo % hg cat
+hg cat a b
+
+rm $HGRCPATH
+echo % cat
+cat a b
+echo % hg cat
+hg cat a b
--- a/tests/test-keyword.out	Tue Feb 05 17:23:24 2008 +0100
+++ b/tests/test-keyword.out	Wed Feb 06 18:38:14 2008 +0100
@@ -34,7 +34,6 @@
         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.
- kwcat      output the current or given revision of files expanding keywords
  kwdemo     print [keywordmaps] configuration and an expansion example
  kwexpand   expand keywords in working directory
  kwfiles    print files currently configured for keyword expansion
@@ -97,7 +96,7 @@
 do not process $Id:
 xxx $
 ignore $Id$
-% hg kwcat
+% hg cat
 expand $Id: a,v 7f0665a496fd 1970/01/01 00:00:00 user $
 do not process $Id:
 xxx $
@@ -126,9 +125,7 @@
 xxx $
 $Id: c,v 7fefeeacf359 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>
@@ -143,6 +140,14 @@
 @@ -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 7fefeeacf359 1970/01/01 00:00:01 user $
+tests for different changenodes
 % qpop and move on
 Patch queue now empty
 % copy
@@ -194,7 +199,7 @@
 do not process $Id:
 xxx $
 ignore $Id$
-% hg kwcat
+% hg cat
 expand $Id: a 7f0665a496fd Thu, 01 Jan 1970 00:00:00 +0000 user $
 do not process $Id:
 xxx $
@@ -209,7 +214,7 @@
 xxx $
 $Xinfo: User Name <user@example.com>: firstline $
 ignore $Id$
-% hg kwcat
+% hg cat
 expand $Id: a 576a35651b0a Thu, 01 Jan 1970 00:00:02 +0000 user $
 do not process $Id:
 xxx $
@@ -265,6 +270,7 @@
 $Xinfo$
 % kwexpand nonexistent
 nonexistent: No such file or directory
+% switch off expansion
 % kwshrink with unknown file u
 overwriting a shrinking keywords
 overwriting x/a shrinking keywords
@@ -274,9 +280,21 @@
 xxx $
 $Xinfo$
 ignore $Id$
-% hg kwcat
+% hg cat
 expand $Id: a 576a35651b0a 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$