hgkw/keyword.py
changeset 378 0683aaf458d8
parent 377 ce5eb725d0c6
parent 375 ed160b097bad
child 381 17e94f77de6a
equal deleted inserted replaced
377:ce5eb725d0c6 378:0683aaf458d8
    78 Expansions spanning more than one line and incremental expansions,
    78 Expansions spanning more than one line and incremental expansions,
    79 like CVS' $Log$, are not supported. A keyword template map
    79 like CVS' $Log$, are not supported. A keyword template map
    80 "Log = {desc}" expands to the first line of the changeset description.
    80 "Log = {desc}" expands to the first line of the changeset description.
    81 '''
    81 '''
    82 
    82 
    83 from mercurial import commands, cmdutil, context, localrepo
    83 from mercurial import commands, cmdutil, context, dispatch, filelog, revlog
    84 from mercurial import patch, revlog, templater, templatefilters, util
    84 from mercurial import patch, localrepo, templater, templatefilters, util
    85 from mercurial.node import *
    85 from mercurial.node import *
    86 from mercurial.hgweb import webcommands
       
    87 from mercurial.i18n import _
    86 from mercurial.i18n import _
    88 import mimetypes, re, shutil, tempfile, time
    87 import re, shutil, sys, tempfile, time
    89 
    88 
    90 commands.optionalrepo += ' kwdemo'
    89 commands.optionalrepo += ' kwdemo'
       
    90 
       
    91 # hg commands that do not act on keywords
       
    92 nokwcommands = ('add addremove bundle copy export grep identify incoming init'
       
    93                 ' log outgoing push remove rename rollback tip convert')
       
    94 
       
    95 # hg commands that trigger expansion only when writing to working dir,
       
    96 # not when reading filelog, and unexpand when reading from working dir
       
    97 restricted = 'diff1 record qfold qimport qnew qpush qrefresh qrecord'
    91 
    98 
    92 def utcdate(date):
    99 def utcdate(date):
    93     '''Returns hgdate in cvs-like UTC format.'''
   100     '''Returns hgdate in cvs-like UTC format.'''
    94     return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
   101     return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
       
   102 
       
   103 
       
   104 _kwtemplater = None
    95 
   105 
    96 class kwtemplater(object):
   106 class kwtemplater(object):
    97     '''
   107     '''
    98     Sets up keyword templates, corresponding keyword regex, and
   108     Sets up keyword templates, corresponding keyword regex, and
    99     provides keyword substitution functions.
   109     provides keyword substitution functions.
   106         'Source': '{root}/{file},v',
   116         'Source': '{root}/{file},v',
   107         'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
   117         'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
   108         'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
   118         'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
   109     }
   119     }
   110 
   120 
   111     def __init__(self, ui, repo, inc, exc):
   121     def __init__(self, ui, repo, inc, exc, restricted):
   112         self.ui = ui
   122         self.ui = ui
   113         self.repo = repo
   123         self.repo = repo
   114         self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
   124         self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
   115         self.ctx = None
   125         self.restricted = restricted
       
   126         self.commitnode = None
       
   127         self.path = ''
   116 
   128 
   117         kwmaps = self.ui.configitems('keywordmaps')
   129         kwmaps = self.ui.configitems('keywordmaps')
   118         if kwmaps: # override default templates
   130         if kwmaps: # override default templates
   119             kwmaps = [(k, templater.parsestring(v, quoted=False))
   131             kwmaps = [(k, templater.parsestring(v, quoted=False))
   120                       for (k, v) in kwmaps]
   132                       for (k, v) in kwmaps]
   125 
   137 
   126         templatefilters.filters['utcdate'] = utcdate
   138         templatefilters.filters['utcdate'] = utcdate
   127         self.ct = cmdutil.changeset_templater(self.ui, self.repo,
   139         self.ct = cmdutil.changeset_templater(self.ui, self.repo,
   128                                               False, '', False)
   140                                               False, '', False)
   129 
   141 
   130     def substitute(self, path, data, node, subfunc):
   142     def substitute(self, node, data, subfunc):
   131         '''Obtains file's changenode if node not given,
   143         '''Obtains file's changenode if commit node not given,
   132         and calls given substitution function.'''
   144         and calls given substitution function.'''
   133         if node is None:
   145         if self.commitnode:
   134             # kwrepo.wwrite except when overwriting on commit
   146             fnode = self.commitnode
   135             try:
   147         else:
   136                 fnode = self.ctx.filenode(path)
   148             c = context.filectx(self.repo, self.path, fileid=node)
   137                 fl = self.repo.file(path)
   149             fnode = c.node()
   138                 c = context.filectx(self.repo, path, fileid=fnode, filelog=fl)
       
   139                 node = c.node()
       
   140             except revlog.LookupError:
       
   141                 # eg: convert
       
   142                 return subfunc == self.re_kw.sub and data or (data, None)
       
   143 
   150 
   144         def kwsub(mobj):
   151         def kwsub(mobj):
   145             '''Substitutes keyword using corresponding template.'''
   152             '''Substitutes keyword using corresponding template.'''
   146             kw = mobj.group(1)
   153             kw = mobj.group(1)
   147             self.ct.use_template(self.templates[kw])
   154             self.ct.use_template(self.templates[kw])
   148             self.ui.pushbuffer()
   155             self.ui.pushbuffer()
   149             self.ct.show(changenode=node, root=self.repo.root, file=path)
   156             self.ct.show(changenode=fnode, root=self.repo.root, file=self.path)
   150             ekw = templatefilters.firstline(self.ui.popbuffer())
   157             ekw = templatefilters.firstline(self.ui.popbuffer())
   151             return '$%s: %s $' % (kw, ekw)
   158             return '$%s: %s $' % (kw, ekw)
   152 
   159 
   153         return subfunc(kwsub, data)
   160         return subfunc(kwsub, data)
   154 
   161 
   155     def expand(self, path, data, ctx):
   162     def expand(self, node, data):
   156         '''Returns data with keywords expanded.'''
   163         '''Returns data with keywords expanded.'''
   157         if util.binary(data):
   164         if self.restricted or util.binary(data):
   158             return data
   165             return data
   159         if self.ctx is None:
   166         return self.substitute(node, data, self.re_kw.sub)
   160             self.ctx = ctx or self.repo.changectx()
   167 
   161         return self.substitute(path, data, None, self.re_kw.sub)
   168     def process(self, node, data, expand):
   162 
       
   163     def process(self, path, data, expand, ctx, node):
       
   164         '''Returns a tuple: data, count.
   169         '''Returns a tuple: data, count.
   165         Count is number of keywords/keyword substitutions,
   170         Count is number of keywords/keyword substitutions,
   166         telling caller whether to act on file containing data.'''
   171         telling caller whether to act on file containing data.'''
   167         if util.binary(data):
   172         if util.binary(data):
   168             return data, None
   173             return data, None
   169         if expand:
   174         if expand:
   170             self.ctx = ctx
   175             return self.substitute(node, data, self.re_kw.subn)
   171             return self.substitute(path, data, node, self.re_kw.subn)
   176         return data, self.re_kw.search(data)
   172         return self.re_kw.subn(r'$\1$', data)
   177 
   173 
   178     def shrink(self, text):
   174     def shrink(self, data):
       
   175         '''Returns text with all keyword substitutions removed.'''
   179         '''Returns text with all keyword substitutions removed.'''
   176         if util.binary(data):
   180         if util.binary(text):
   177             return data
   181             return text
   178         return self.re_kw.sub(r'$\1$', data)
   182         return self.re_kw.sub(r'$\1$', text)
       
   183 
       
   184 class kwfilelog(filelog.filelog):
       
   185     '''
       
   186     Subclass of filelog to hook into its read, add, cmp methods.
       
   187     Keywords are "stored" unexpanded, and processed on reading.
       
   188     '''
       
   189     def __init__(self, opener, path):
       
   190         super(kwfilelog, self).__init__(opener, path)
       
   191         _kwtemplater.path = path
       
   192 
       
   193     def kwctread(self, node, expand):
       
   194         '''Reads expanding and counting keywords, called from _overwrite.'''
       
   195         data = super(kwfilelog, self).read(node)
       
   196         return _kwtemplater.process(node, data, expand)
       
   197 
       
   198     def read(self, node):
       
   199         '''Expands keywords when reading filelog.'''
       
   200         data = super(kwfilelog, self).read(node)
       
   201         return _kwtemplater.expand(node, data)
       
   202 
       
   203     def add(self, text, meta, tr, link, p1=None, p2=None):
       
   204         '''Removes keyword substitutions when adding to filelog.'''
       
   205         text = _kwtemplater.shrink(text)
       
   206         return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2)
       
   207 
       
   208     def cmp(self, node, text):
       
   209         '''Removes keyword substitutions for comparison.'''
       
   210         text = _kwtemplater.shrink(text)
       
   211         if self.renamed(node):
       
   212             t2 = super(kwfilelog, self).read(node)
       
   213             return t2 != text
       
   214         return revlog.revlog.cmp(self, node, text)
       
   215 
   179 
   216 
   180 # store original patch.patchfile.__init__
   217 # store original patch.patchfile.__init__
   181 _patchfile_init = patch.patchfile.__init__
   218 _patchfile_init = patch.patchfile.__init__
   182 
   219 
   183 
   220 def _kwpatchfile_init(self, ui, fname, missing=False):
   184 def _iskwfile(f, link, kwt):
   221     '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
   185     return not link(f) and kwt.matcher(f)
   222     rejects or conflicts due to expanded keywords in working dir.'''
       
   223     _patchfile_init(self, ui, fname, missing=missing)
       
   224 
       
   225     if _kwtemplater.matcher(self.fname):
       
   226         # shrink keywords read from working dir
       
   227         kwshrunk = _kwtemplater.shrink(''.join(self.lines))
       
   228         self.lines = kwshrunk.splitlines(True)
       
   229 
       
   230 
       
   231 def _iskwfile(f, link):
       
   232     return not link(f) and _kwtemplater.matcher(f)
   186 
   233 
   187 def _status(ui, repo, *pats, **opts):
   234 def _status(ui, repo, *pats, **opts):
   188     '''Bails out if [keyword] configuration is not active.
   235     '''Bails out if [keyword] configuration is not active.
   189     Returns status of working directory.'''
   236     Returns status of working directory.'''
   190     if hasattr(repo, '_kwt'):
   237     if _kwtemplater:
   191         files, match, anypats = cmdutil.matchpats(repo, pats, opts)
   238         files, match, anypats = cmdutil.matchpats(repo, pats, opts)
   192         return repo.status(files=files, match=match, list_clean=True)
   239         return repo.status(files=files, match=match, list_clean=True)
   193     if ui.configitems('keyword'):
   240     if ui.configitems('keyword'):
   194         raise util.Abort(_('[keyword] patterns cannot match'))
   241         raise util.Abort(_('[keyword] patterns cannot match'))
   195     raise util.Abort(_('no [keyword] patterns configured'))
   242     raise util.Abort(_('no [keyword] patterns configured'))
   196 
   243 
   197 def _overwrite(ui, repo, node=None, expand=True, files=None):
   244 def _overwrite(ui, repo, node=None, expand=True, files=None):
   198     '''Overwrites selected files expanding/shrinking keywords.'''
   245     '''Overwrites selected files expanding/shrinking keywords.'''
   199     ctx = repo.changectx(node)
   246     ctx = repo.changectx(node)
   200     mf = ctx.manifest()
   247     mf = ctx.manifest()
   201     if node is not None:
   248     if node is not None:   # commit
   202         # commit
   249         _kwtemplater.commitnode = node
   203         files = [f for f in ctx.files() if f in mf]
   250         files = [f for f in ctx.files() if f in mf]
   204         notify = ui.debug
   251         notify = ui.debug
   205     else:
   252     else:                  # kwexpand/kwshrink
   206         # kwexpand/kwshrink
       
   207         notify = ui.note
   253         notify = ui.note
   208     candidates = [f for f in files if _iskwfile(f, mf.linkf, repo._kwt)]
   254     candidates = [f for f in files if _iskwfile(f, mf.linkf)]
   209     if candidates:
   255     if candidates:
   210         candidates.sort()
   256         candidates.sort()
   211         action = expand and 'expanding' or 'shrinking'
   257         action = expand and 'expanding' or 'shrinking'
   212         for f in candidates:
   258         for f in candidates:
   213             data, kwfound = repo._wreadkwct(f, expand, ctx, node)
   259             fp = repo.file(f, kwmatch=True)
       
   260             data, kwfound = fp.kwctread(mf[f], expand)
   214             if kwfound:
   261             if kwfound:
   215                 notify(_('overwriting %s %s keywords\n') % (f, action))
   262                 notify(_('overwriting %s %s keywords\n') % (f, action))
   216                 repo.wwrite(f, data, mf.flags(f), overwrite=True)
   263                 repo.wwrite(f, data, mf.flags(f))
   217                 repo.dirstate.normal(f)
   264                 repo.dirstate.normal(f)
   218 
   265 
   219 def _kwfwrite(ui, repo, expand, *pats, **opts):
   266 def _kwfwrite(ui, repo, expand, *pats, **opts):
   220     '''Selects files and passes them to _overwrite.'''
   267     '''Selects files and passes them to _overwrite.'''
   221     status = _status(ui, repo, *pats, **opts)
   268     status = _status(ui, repo, *pats, **opts)
   228         lock = repo.lock()
   275         lock = repo.lock()
   229         _overwrite(ui, repo, expand=expand, files=clean)
   276         _overwrite(ui, repo, expand=expand, files=clean)
   230     finally:
   277     finally:
   231         del wlock, lock
   278         del wlock, lock
   232 
   279 
   233 def cat(ui, repo, file1, *pats, **opts):
       
   234     '''output the current or given revision of files expanding keywords
       
   235 
       
   236     Print the specified files as they were at the given revision.
       
   237     If no revision is given, the parent of the working directory is used,
       
   238     or tip if no revision is checked out.
       
   239 
       
   240     Output may be to a file, in which case the name of the file is
       
   241     given using a format string.  The formatting rules are the same as
       
   242     for the export command, with the following additions:
       
   243 
       
   244     %s   basename of file being printed
       
   245     %d   dirname of file being printed, or '.' if in repo root
       
   246     %p   root-relative path name of file being printed
       
   247     '''
       
   248     ctx = repo.changectx(opts['rev'])
       
   249     err = 1
       
   250     for src, abs, rel, exact in cmdutil.walk(repo, (file1,) + pats, opts,
       
   251                                              ctx.node()):
       
   252         fp = cmdutil.make_file(repo, opts['output'], ctx.node(), pathname=abs)
       
   253         data = ctx.filectx(abs).data()
       
   254         try:
       
   255             if repo._kwt.matcher(abs):
       
   256                 data = repo._kwt.expand(abs, data, ctx)
       
   257         except AttributeError:
       
   258             pass
       
   259         fp.write(data)
       
   260         err = 0
       
   261     return err
       
   262 
   280 
   263 def demo(ui, repo, *args, **opts):
   281 def demo(ui, repo, *args, **opts):
   264     '''print [keywordmaps] configuration and an expansion example
   282     '''print [keywordmaps] configuration and an expansion example
   265 
   283 
   266     Show current, custom, or default keyword template maps
   284     Show current, custom, or default keyword template maps
   334     ui.note(_('unhooked all commit hooks\n'))
   352     ui.note(_('unhooked all commit hooks\n'))
   335     ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
   353     ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
   336     repo.commit(text=msg)
   354     repo.commit(text=msg)
   337     format = ui.verbose and ' in %s' % path or ''
   355     format = ui.verbose and ' in %s' % path or ''
   338     demostatus('%s keywords expanded%s' % (kwstatus, format))
   356     demostatus('%s keywords expanded%s' % (kwstatus, format))
   339     ui.write(repo.wopener(fn).read())
   357     ui.write(repo.wread(fn))
   340     ui.debug(_('\nremoving temporary repo %s\n') % tmpdir)
   358     ui.debug(_('\nremoving temporary repo %s\n') % tmpdir)
   341     shutil.rmtree(tmpdir, ignore_errors=True)
   359     shutil.rmtree(tmpdir, ignore_errors=True)
   342 
   360 
   343 def expand(ui, repo, *pats, **opts):
   361 def expand(ui, repo, *pats, **opts):
   344     '''expand keywords in working directory
   362     '''expand keywords in working directory
   361     modified, added, removed, deleted, unknown, ignored, clean = status
   379     modified, added, removed, deleted, unknown, ignored, clean = status
   362     files = modified + added + clean
   380     files = modified + added + clean
   363     if opts.get('untracked'):
   381     if opts.get('untracked'):
   364         files += unknown
   382         files += unknown
   365     files.sort()
   383     files.sort()
   366     kwfiles = [f for f in files if _iskwfile(f, repo._link, repo._kwt)]
   384     kwfiles = [f for f in files if _iskwfile(f, repo._link)]
   367     cwd = pats and repo.getcwd() or ''
   385     cwd = pats and repo.getcwd() or ''
   368     kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
   386     kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
   369     if opts.get('all') or opts.get('ignore'):
   387     if opts.get('all') or opts.get('ignore'):
   370         kwfstats += (('I', [f for f in files if f not in kwfiles]),)
   388         kwfstats += (('I', [f for f in files if f not in kwfiles]),)
   371     for char, filenames in kwfstats:
   389     for char, filenames in kwfstats:
   384     # 3rd argument sets expansion to False
   402     # 3rd argument sets expansion to False
   385     _kwfwrite(ui, repo, False, *pats, **opts)
   403     _kwfwrite(ui, repo, False, *pats, **opts)
   386 
   404 
   387 
   405 
   388 def reposetup(ui, repo):
   406 def reposetup(ui, repo):
   389     if not repo.local() or repo.root.endswith('/.hg/patches'):
   407     '''Sets up repo as kwrepo for keyword substitution.
       
   408     Overrides file method to return kwfilelog instead of filelog
       
   409     if file matches user configuration.
       
   410     Wraps commit to overwrite configured files with updated
       
   411     keyword substitutions.
       
   412     This is done for local repos only, and only if there are
       
   413     files configured at all for keyword substitution.'''
       
   414 
       
   415     if not repo.local():
   390         return
   416         return
   391 
   417 
   392     inc, exc = [], ['.hgtags', '.hg_archival.txt']
   418     hgcmd, func, args, opts, cmdopts = dispatch._parse(ui, sys.argv[1:])
       
   419     if hgcmd in nokwcommands.split():
       
   420         return
       
   421 
       
   422     if hgcmd == 'diff':
       
   423         # only expand if comparing against working dir
       
   424         node1, node2 = cmdutil.revpair(repo, cmdopts.get('rev'))
       
   425         if node2 is not None:
       
   426             return
       
   427         # shrink if rev is not current node
       
   428         if node1 is not None and node1 != repo.changectx().node():
       
   429             hgcmd = 'diff1'
       
   430 
       
   431     inc, exc = [], ['.hgtags']
   393     for pat, opt in ui.configitems('keyword'):
   432     for pat, opt in ui.configitems('keyword'):
   394         if opt != 'ignore':
   433         if opt != 'ignore':
   395             inc.append(pat)
   434             inc.append(pat)
   396         else:
   435         else:
   397             exc.append(pat)
   436             exc.append(pat)
   398     if not inc:
   437     if not inc:
   399         return
   438         return
   400 
   439 
       
   440     global _kwtemplater
       
   441     _restricted = hgcmd in restricted.split()
       
   442     _kwtemplater = kwtemplater(ui, repo, inc, exc, _restricted)
       
   443 
   401     class kwrepo(repo.__class__):
   444     class kwrepo(repo.__class__):
   402         def _wreadkwct(self, filename, expand, ctx, node):
   445         def file(self, f, kwmatch=False):
   403             '''Reads filename and returns tuple of data with keywords
   446             if f[0] == '/':
   404             expanded/shrunk and count of keywords (for _overwrite).'''
   447                 f = f[1:]
   405             data = super(kwrepo, self).wread(filename)
   448             if kwmatch or _kwtemplater.matcher(f):
   406             return self._kwt.process(filename, data, expand, ctx, node)
   449                 return kwfilelog(self.sopener, f)
       
   450             return filelog.filelog(self.sopener, f)
   407 
   451 
   408         def wread(self, filename):
   452         def wread(self, filename):
   409             data = super(kwrepo, self).wread(filename)
   453             data = super(kwrepo, self).wread(filename)
   410             if self._kwt.matcher(filename):
   454             if _restricted and _kwtemplater.matcher(filename):
   411                 return self._kwt.shrink(data)
   455                 return _kwtemplater.shrink(data)
   412             return data
   456             return data
   413 
       
   414         def wwrite(self, filename, data, flags, overwrite=False):
       
   415             if not overwrite and self._kwt.matcher(filename):
       
   416                 data = self._kwt.expand(filename, data, None)
       
   417             super(kwrepo, self).wwrite(filename, data, flags)
       
   418 
       
   419         def wwritedata(self, filename, data):
       
   420             if self._kwt.matcher(filename):
       
   421                 data = self._kwt.expand(filename, data, None)
       
   422             return super(kwrepo, self).wwritedata(filename, data)
       
   423 
   457 
   424         def commit(self, files=None, text='', user=None, date=None,
   458         def commit(self, files=None, text='', user=None, date=None,
   425                    match=util.always, force=False, force_editor=False,
   459                    match=util.always, force=False, force_editor=False,
   426                    p1=None, p2=None, extra={}, empty_ok=False):
   460                    p1=None, p2=None, extra={}, empty_ok=False):
   427             wlock = lock = None
   461             wlock = lock = None
   462                     repo.hook('commit', node=node, parent1=_p1, parent2=_p2)
   496                     repo.hook('commit', node=node, parent1=_p1, parent2=_p2)
   463                 return node
   497                 return node
   464             finally:
   498             finally:
   465                 del wlock, lock
   499                 del wlock, lock
   466 
   500 
   467     kwt = kwrepo._kwt = kwtemplater(ui, repo, inc, exc)
       
   468 
       
   469     def kwpatchfile_init(self, ui, fname, missing=False):
       
   470         '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
       
   471         rejects or conflicts due to expanded keywords in working dir.'''
       
   472         _patchfile_init(self, ui, fname, missing=missing)
       
   473 
       
   474         if kwt.matcher(self.fname):
       
   475             # shrink keywords read from working dir
       
   476             kwshrunk = kwt.shrink(''.join(self.lines))
       
   477             self.lines = kwshrunk.splitlines(True)
       
   478 
       
   479     def kwweb_rawfile(web, req, tmpl):
       
   480         '''Monkeypatch webcommands.rawfile so it expands keywords.'''
       
   481         path = web.cleanpath(req.form.get('file', [''])[0])
       
   482         if not path:
       
   483             content = web.manifest(tmpl, web.changectx(req), path)
       
   484             req.respond(webcommands.HTTP_OK, web.ctype)
       
   485             return content
       
   486         try:
       
   487             fctx = web.filectx(req)
       
   488         except revlog.LookupError:
       
   489             content = web.manifest(tmpl, web.changectx(req), path)
       
   490             req.respond(webcommands.HTTP_OK, web.ctype)
       
   491             return content
       
   492         path = fctx.path()
       
   493         text = fctx.data()
       
   494         if kwt.matcher(path):
       
   495             text = kwt.expand(path, text, web.changectx(req))
       
   496         mt = mimetypes.guess_type(path)[0]
       
   497         if mt is None or util.binary(text):
       
   498             mt = mt or 'application/octet-stream'
       
   499         req.respond(webcommands.HTTP_OK, mt, path, len(text))
       
   500         return [text]
       
   501 
       
   502     repo.__class__ = kwrepo
   501     repo.__class__ = kwrepo
   503     patch.patchfile.__init__ = kwpatchfile_init
   502     patch.patchfile.__init__ = _kwpatchfile_init
   504     webcommands.rawfile = kwweb_rawfile
       
   505 
   503 
   506 
   504 
   507 cmdtable = {
   505 cmdtable = {
   508     'kwcat':
       
   509         (cat, commands.table['cat'][1],
       
   510          _('hg kwcat [OPTION]... FILE...')),
       
   511     'kwdemo':
   506     'kwdemo':
   512         (demo,
   507         (demo,
   513          [('d', 'default', None, _('show default keyword template maps')),
   508          [('d', 'default', None, _('show default keyword template maps')),
   514           ('f', 'rcfile', [], _('read maps from rcfile'))],
   509           ('f', 'rcfile', [], _('read maps from rcfile'))],
   515          _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
   510          _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),