--- a/hgkw/keyword.py Wed Feb 06 11:32:34 2008 +0100
+++ b/hgkw/keyword.py Wed Feb 06 17:25:37 2008 +0100
@@ -80,19 +80,29 @@
"Log = {desc}" expands to the first line of the changeset description.
'''
-from mercurial import commands, cmdutil, context, localrepo
-from mercurial import patch, revlog, templater, templatefilters, util
+from mercurial import commands, cmdutil, context, dispatch, filelog, revlog
+from mercurial import patch, localrepo, templater, templatefilters, util
from mercurial.node import *
-from mercurial.hgweb import webcommands
from mercurial.i18n import _
-import mimetypes, re, shutil, tempfile, time
+import 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
+
class kwtemplater(object):
'''
Sets up keyword templates, corresponding keyword regex, and
@@ -108,11 +118,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
@@ -127,67 +139,102 @@
self.ct = cmdutil.changeset_templater(self.ui, self.repo,
False, '', 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
- 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)
+ self.ct.show(changenode=fnode, root=self.repo.root, file=self.path)
ekw = templatefilters.firstline(self.ui.popbuffer())
return '$%s: %s $' % (kw, ekw)
return subfunc(kwsub, data)
- def expand(self, path, data, ctx):
+ def expand(self, node, data):
'''Returns data with keywords expanded.'''
- if util.binary(data):
+ if self.restricted or util.binary(data):
return data
- if self.ctx is None:
- self.ctx = ctx or self.repo.changectx()
- return self.substitute(path, data, None, 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, text):
+ '''Returns text with all keyword substitutions removed.'''
+ 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 shrink(self, data):
- '''Returns text with all keyword substitutions removed.'''
- if util.binary(data):
- return data
- return self.re_kw.sub(r'$\1$', data)
+ 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)
+
# store original patch.patchfile.__init__
_patchfile_init = patch.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)
-def _iskwfile(f, link, kwt):
- return not link(f) and kwt.matcher(f)
+ if _kwtemplater.matcher(self.fname):
+ # shrink keywords read from working dir
+ kwshrunk = _kwtemplater.shrink(''.join(self.lines))
+ self.lines = kwshrunk.splitlines(True)
+
+
+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'):
@@ -198,22 +245,22 @@
'''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 _iskwfile(f, mf.linkf, repo._kwt)]
+ candidates = [f for f in files if _iskwfile(f, mf.linkf)]
if candidates:
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)
+ repo.wwrite(f, data, mf.flags(f))
repo.dirstate.normal(f)
def _kwfwrite(ui, repo, expand, *pats, **opts):
@@ -230,35 +277,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'])
- 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()
- try:
- if repo._kwt.matcher(abs):
- data = repo._kwt.expand(abs, data, ctx)
- except AttributeError:
- pass
- fp.write(data)
- err = 0
- return err
def demo(ui, repo, *args, **opts):
'''print [keywordmaps] configuration and an expansion example
@@ -336,7 +354,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)
@@ -363,7 +381,7 @@
if opts.get('untracked'):
files += unknown
files.sort()
- kwfiles = [f for f in files if _iskwfile(f, repo._link, repo._kwt)]
+ kwfiles = [f for f in files if _iskwfile(f, repo._link)]
cwd = pats and repo.getcwd() or ''
kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
if opts.get('all') or opts.get('ignore'):
@@ -386,10 +404,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 = dispatch._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)
@@ -398,29 +437,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, overwrite=False):
- if not overwrite and self._kwt.matcher(filename):
- data = self._kwt.expand(filename, data, None)
- super(kwrepo, self).wwrite(filename, data, flags)
-
- 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=None, text='', user=None, date=None,
match=util.always, force=False, force_editor=False,
p1=None, p2=None, extra={}, empty_ok=False):
@@ -464,50 +498,11 @@
finally:
del wlock, lock
- kwt = kwrepo._kwt = kwtemplater(ui, repo, inc, exc)
-
- 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)
-
- 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, web.changectx(req))
- 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]
-
repo.__class__ = kwrepo
- patch.patchfile.__init__ = kwpatchfile_init
- webcommands.rawfile = kwweb_rawfile
+ patch.patchfile.__init__ = _kwpatchfile_init
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.out Wed Feb 06 11:32:34 2008 +0100
+++ b/tests/test-keyword.out Wed Feb 06 17:25:37 2008 +0100
@@ -46,7 +46,6 @@
list of commands:
- 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
@@ -85,23 +84,6 @@
Branch = {branches}
$Branch: demobranch $
% kwshrink should exit silently in empty/invalid repo
-% symlink nonexisting file
-% commit hook must fail with missing file
-cp: a: No such file or directory
-adding sym
-sym
-running hook commit.test: cp a hooktest
-warning: commit.test hook exited with status 1
-% bundle null revision containing empty symlink
-1 changesets found
-% pull from bundle
-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:
@@ -113,38 +95,43 @@
% addremove
adding a
adding b
+adding sym
% status
A a
A b
+A sym
% default keyword expansion including commit hook
% interrupted commit should not change state or run commit hook
a
b
+sym
transaction abort!
rollback completed
abort: empty commit message
% status
A a
A b
+A sym
% commit
a
b
+sym
overwriting a expanding keywords
running hook commit.test: cp a hooktest
% status
? hooktest
% identify
-cecf1e2cc3d3
+f782df5f9602
% cat
-expand $Id: a,v cecf1e2cc3d3 1970/01/01 00:00:01 user $
+expand $Id: a,v f782df5f9602 1970/01/01 00:00:00 user $
do not process $Id:
xxx $
-expand $Id: a,v cecf1e2cc3d3 1970/01/01 00:00:01 user $
+expand $Id: a,v f782df5f9602 1970/01/01 00:00:00 user $
do not process $Id:
xxx $
ignore $Id$
-% hg kwcat
-expand $Id: a,v cecf1e2cc3d3 1970/01/01 00:00:01 user $
+% hg cat
+expand $Id: a,v f782df5f9602 1970/01/01 00:00:00 user $
do not process $Id:
xxx $
ignore $Id$
@@ -156,10 +143,10 @@
% update
3 files updated, 0 files merged, 0 files removed, 0 files unresolved
% cat
-expand $Id: a,v cecf1e2cc3d3 1970/01/01 00:00:01 user $
+expand $Id: a,v f782df5f9602 1970/01/01 00:00:00 user $
do not process $Id:
xxx $
-expand $Id: a,v cecf1e2cc3d3 1970/01/01 00:00:01 user $
+expand $Id: a,v f782df5f9602 1970/01/01 00:00:00 user $
do not process $Id:
xxx $
ignore $Id$
@@ -170,25 +157,23 @@
overwriting a expanding keywords
overwriting c expanding keywords
% compare changenodes in a c
-expand $Id: a,v cecf1e2cc3d3 1970/01/01 00:00:01 user $
+expand $Id: a,v f782df5f9602 1970/01/01 00:00:00 user $
do not process $Id:
xxx $
-$Id: c,v c033759cd8fd 1970/01/01 00:00:02 user $
+$Id: c,v ba4426d1938e 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 2 0
-# Node ID c033759cd8fd162b7863dc4e5d1eea433603880f
-# Parent cecf1e2cc3d3447fcc20dd4eac5c4faa8a615df3
+# Date 1 0
+# Node ID ba4426d1938ec9673e03ab274d88c44e24618f7f
+# Parent f782df5f9602483b4e51c31a12315f353bba380c
cndiff
-diff -r cecf1e2cc3d3 -r c033759cd8fd c
+diff -r f782df5f9602 -r ba4426d1938e c
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
-+++ b/c Thu Jan 01 00:00:02 1970 +0000
++++ b/c Thu Jan 01 00:00:01 1970 +0000
@@ -0,0 +1,2 @@
+$Id$
+tests for different changenodes
@@ -198,7 +183,7 @@
applying mqtest.diff
Now at: mqtest.diff
% cat
-$Id: c,v c033759cd8fd 1970/01/01 00:00:02 user $
+$Id: c,v ba4426d1938e 1970/01/01 00:00:01 user $
tests for different changenodes
% qpop and move on
Patch queue now empty
@@ -211,10 +196,10 @@
c: copy a:0045e12f6c5791aac80ca6cbfd97709a88307292
overwriting c expanding keywords
% cat a c
-expand $Id: a,v cecf1e2cc3d3 1970/01/01 00:00:01 user $
+expand $Id: a,v f782df5f9602 1970/01/01 00:00:00 user $
do not process $Id:
xxx $
-expand $Id: c,v 1fed52d26fd0 1970/01/01 00:00:02 user $
+expand $Id: c,v 0ba462c0f077 1970/01/01 00:00:01 user $
do not process $Id:
xxx $
% touch copied c after 1 second
@@ -223,7 +208,7 @@
a
c
% diff --rev
-diff -r cecf1e2cc3d3 c
+diff -r f782df5f9602 c
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -0,0 +1,3 @@
+expand $Id$
@@ -247,15 +232,15 @@
Xinfo = {author}: {desc}
$Xinfo: test: hg keyword config and expansion example $
% cat
-expand $Id: a,v cecf1e2cc3d3 1970/01/01 00:00:01 user $
+expand $Id: a,v f782df5f9602 1970/01/01 00:00:00 user $
do not process $Id:
xxx $
-expand $Id: a,v cecf1e2cc3d3 1970/01/01 00:00:01 user $
+expand $Id: a,v f782df5f9602 1970/01/01 00:00:00 user $
do not process $Id:
xxx $
ignore $Id$
-% hg kwcat
-expand $Id: a cecf1e2cc3d3 Thu, 01 Jan 1970 00:00:01 +0000 user $
+% hg cat
+expand $Id: a f782df5f9602 Thu, 01 Jan 1970 00:00:00 +0000 user $
do not process $Id:
xxx $
ignore $Id$
@@ -272,17 +257,17 @@
overwriting a expanding keywords
% status
% cat
-expand $Id: a a576e5647736 Thu, 01 Jan 1970 00:00:03 +0000 user $
+expand $Id: a 0729690beff6 Thu, 01 Jan 1970 00:00:02 +0000 user $
do not process $Id:
xxx $
$Xinfo: User Name <user@example.com>: firstline $
-expand $Id: a a576e5647736 Thu, 01 Jan 1970 00:00:03 +0000 user $
+expand $Id: a 0729690beff6 Thu, 01 Jan 1970 00:00:02 +0000 user $
do not process $Id:
xxx $
$Xinfo: User Name <user@example.com>: firstline $
ignore $Id$
-% hg kwcat
-expand $Id: a a576e5647736 Thu, 01 Jan 1970 00:00:03 +0000 user $
+% hg cat
+expand $Id: a 0729690beff6 Thu, 01 Jan 1970 00:00:02 +0000 user $
do not process $Id:
xxx $
$Xinfo: User Name <user@example.com>: firstline $
@@ -296,7 +281,7 @@
R a
% revert a
% cat a
-expand $Id: a a576e5647736 Thu, 01 Jan 1970 00:00:03 +0000 user $
+expand $Id: a 0729690beff6 Thu, 01 Jan 1970 00:00:02 +0000 user $
do not process $Id:
xxx $
$Xinfo: User Name <user@example.com>: firstline $
@@ -305,15 +290,15 @@
adding changesets
adding manifests
adding file changes
-added 2 changesets with 3 changes to 3 files
+added 1 changesets with 3 changes to 3 files
3 files updated, 0 files merged, 0 files removed, 0 files unresolved
% incoming
comparing with test-keyword/Test
searching for changes
-changeset: 2:a576e5647736
+changeset: 1:0729690beff6
tag: tip
user: User Name <user@example.com>
-date: Thu Jan 01 00:00:03 1970 +0000
+date: Thu Jan 01 00:00:02 1970 +0000
summary: firstline
% commit rejecttest
@@ -323,11 +308,11 @@
% import
applying ../rejecttest.diff
% cat
-expand $Id: a 97b8d4afd122 Thu, 01 Jan 1970 00:00:04 +0000 user $ rejecttest
+expand $Id: a 82983f13f138 Thu, 01 Jan 1970 00:00:03 +0000 user $ rejecttest
do not process $Id: rejecttest
xxx $
$Xinfo: User Name <user@example.com>: rejects? $
-expand $Id: a 97b8d4afd122 Thu, 01 Jan 1970 00:00:04 +0000 user $ rejecttest
+expand $Id: a 82983f13f138 Thu, 01 Jan 1970 00:00:03 +0000 user $ rejecttest
do not process $Id: rejecttest
xxx $
$Xinfo: User Name <user@example.com>: rejects? $
@@ -347,7 +332,7 @@
x/a: copy a:779c764182ce5d43e2b1eb66ce06d7b47bfe342e
overwriting x/a expanding keywords
% cat a
-expand $Id: x/a 6ae8e7fbf16c Thu, 01 Jan 1970 00:00:04 +0000 user $
+expand $Id: x/a f27c134d2d9b Thu, 01 Jan 1970 00:00:03 +0000 user $
do not process $Id:
xxx $
$Xinfo: User Name <user@example.com>: xa $
@@ -360,6 +345,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
@@ -373,10 +359,27 @@
xxx $
$Xinfo$
ignore $Id$
-% hg kwcat
-expand $Id: a a576e5647736 Thu, 01 Jan 1970 00:00:03 +0000 user $
+% hg cat
+expand $Id: a 0729690beff6 Thu, 01 Jan 1970 00:00:02 +0000 user $
do not process $Id:
xxx $
$Xinfo: User Name <user@example.com>: firstline $
ignore $Id$
a
+% cat
+expand $Id$
+do not process $Id:
+xxx $
+$Xinfo$
+expand $Id$
+do not process $Id:
+xxx $
+$Xinfo$
+ignore $Id$
+% hg cat
+expand $Id$
+do not process $Id:
+xxx $
+$Xinfo$
+ignore $Id$
+a