hgkw/keyword.py
changeset 353 159bf80a4301
parent 351 19d5f328a979
child 354 8e3364294d0c
equal deleted inserted replaced
351:19d5f328a979 353:159bf80a4301
    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, dispatch, filelog
    83 from mercurial import commands, cmdutil, context, localrepo
    84 from mercurial import patch, localrepo, revlog, templater, util
    84 from mercurial import patch, revlog, templater, util
    85 from mercurial.node import *
    85 from mercurial.node import *
    86 from mercurial.i18n import _
    86 from mercurial.i18n import _
    87 import re, shutil, sys, tempfile, time
    87 import re, shutil, tempfile, time
    88 
    88 
    89 commands.optionalrepo += ' kwdemo'
    89 commands.optionalrepo += ' kwdemo'
    90 
       
    91 # handle for external callers
       
    92 externalcall = None, None, {}
       
    93 
       
    94 def externalcmdhook(hgcmd, *args, **opts):
       
    95     '''Hook for external callers to pass hg commands to keyword.
       
    96 
       
    97     Caveat: hgcmd, args, opts are not checked for validity.
       
    98     This is the responsibility of the caller.
       
    99 
       
   100     hgmcd can be either the hg function object, eg diff or patch,
       
   101     or its string represenation, eg 'diff' or 'patch'.'''
       
   102     global externalcall
       
   103     if not isinstance(hgcmd, str):
       
   104         hgcmd = hgcmd.__name__.split('.')[-1]
       
   105     externalcall = hgcmd, args, opts
       
   106 
       
   107 # hg commands that trigger expansion only when writing to working dir,
       
   108 # not when reading filelog, and unexpand when reading from working dir
       
   109 restricted = ('diff1', 'record',
       
   110               'qfold', 'qimport', 'qnew', 'qpush', 'qrefresh', 'qrecord')
       
   111 
    90 
   112 def utcdate(date):
    91 def utcdate(date):
   113     '''Returns hgdate in cvs-like UTC format.'''
    92     '''Returns hgdate in cvs-like UTC format.'''
   114     return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
    93     return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
   115 
       
   116 
    94 
   117 _kwtemplater = None
    95 _kwtemplater = None
   118 
    96 
   119 class kwtemplater(object):
    97 class kwtemplater(object):
   120     '''
    98     '''
   129         'Source': '{root}/{file},v',
   107         'Source': '{root}/{file},v',
   130         'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
   108         'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
   131         'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
   109         'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
   132     }
   110     }
   133 
   111 
   134     def __init__(self, ui, repo, inc, exc, hgcmd):
   112     def __init__(self, ui, repo, inc, exc):
   135         self.ui = ui
   113         self.ui = ui
   136         self.repo = repo
   114         self.repo = repo
   137         self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
   115         self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
   138         self.hgcmd = hgcmd
   116         self.ctx = None
   139         self.commitnode = None
       
   140         self.path = ''
       
   141 
   117 
   142         kwmaps = self.ui.configitems('keywordmaps')
   118         kwmaps = self.ui.configitems('keywordmaps')
   143         if kwmaps: # override default templates
   119         if kwmaps: # override default templates
   144             kwmaps = [(k, templater.parsestring(v, quoted=False))
   120             kwmaps = [(k, templater.parsestring(v, quoted=False))
   145                       for (k, v) in kwmaps]
   121                       for (k, v) in kwmaps]
   150 
   126 
   151         templater.common_filters['utcdate'] = utcdate
   127         templater.common_filters['utcdate'] = utcdate
   152         self.ct = cmdutil.changeset_templater(self.ui, self.repo,
   128         self.ct = cmdutil.changeset_templater(self.ui, self.repo,
   153                                               False, '', False)
   129                                               False, '', False)
   154 
   130 
   155     def substitute(self, node, data, subfunc):
   131     def substitute(self, path, data, node, subfunc):
   156         '''Obtains file's changenode if commit node not given,
   132         '''Obtains file's changenode if node not given,
   157         and calls given substitution function.'''
   133         and calls given substitution function.'''
   158         if self.commitnode:
   134         if node is None:
   159             fnode = self.commitnode
   135             # kwrepo.wwrite except when overwriting on commit
   160         else:
   136             if self.ctx is None:
   161             c = context.filectx(self.repo, self.path, fileid=node)
   137                 self.ctx = self.repo.changectx()
   162             fnode = c.node()
   138             fnode = self.ctx.filenode(path)
       
   139             fl = self.repo.file(path)
       
   140             c = context.filectx(self.repo, path, fileid=fnode, filelog=fl)
       
   141             node = c.node()
   163 
   142 
   164         def kwsub(mobj):
   143         def kwsub(mobj):
   165             '''Substitutes keyword using corresponding template.'''
   144             '''Substitutes keyword using corresponding template.'''
   166             kw = mobj.group(1)
   145             kw = mobj.group(1)
   167             self.ct.use_template(self.templates[kw])
   146             self.ct.use_template(self.templates[kw])
   168             self.ui.pushbuffer()
   147             self.ui.pushbuffer()
   169             self.ct.show(changenode=fnode, root=self.repo.root, file=self.path)
   148             self.ct.show(changenode=node, root=self.repo.root, file=path)
   170             return '$%s: %s $' % (kw, templater.firstline(self.ui.popbuffer()))
   149             return '$%s: %s $' % (kw, templater.firstline(self.ui.popbuffer()))
   171 
   150 
   172         return subfunc(kwsub, data)
   151         return subfunc(kwsub, data)
   173 
   152 
   174     def expand(self, node, data):
   153     def expand(self, path, data):
   175         '''Returns data with keywords expanded.'''
   154         '''Returns data with keywords expanded.'''
   176         if util.binary(data) or self.hgcmd in restricted:
   155         if util.binary(data):
   177             return data
   156             return data
   178         return self.substitute(node, data, self.re_kw.sub)
   157         return self.substitute(path, data, None, self.re_kw.sub)
   179 
   158 
   180     def process(self, node, data, expand):
   159     def process(self, path, data, expand, ctx, node):
   181         '''Returns a tuple: data, count.
   160         '''Returns a tuple: data, count.
   182         Count is number of keywords/keyword substitutions,
   161         Count is number of keywords/keyword substitutions,
   183         telling caller whether to act on file containing data.'''
   162         telling caller whether to act on file containing data.'''
   184         if util.binary(data):
   163         if util.binary(data):
   185             return data, None
   164             return data, None
   186         if expand:
   165         if expand:
   187             return self.substitute(node, data, self.re_kw.subn)
   166             self.ctx = ctx
   188         return data, self.re_kw.search(data)
   167             return self.substitute(path, data, node, self.re_kw.subn)
   189 
   168         return self.re_kw.subn(r'$\1$', data)
   190     def shrink(self, text):
   169 
       
   170     def shrink(self, data):
   191         '''Returns text with all keyword substitutions removed.'''
   171         '''Returns text with all keyword substitutions removed.'''
   192         if util.binary(text):
   172         if util.binary(data):
   193             return text
   173             return data
   194         return self.re_kw.sub(r'$\1$', text)
   174         return self.re_kw.sub(r'$\1$', data)
   195 
       
   196 class kwfilelog(filelog.filelog):
       
   197     '''
       
   198     Subclass of filelog to hook into its read, add, cmp methods.
       
   199     Keywords are "stored" unexpanded, and processed on reading.
       
   200     '''
       
   201     def __init__(self, opener, path):
       
   202         super(kwfilelog, self).__init__(opener, path)
       
   203         _kwtemplater.path = path
       
   204 
       
   205     def kwctread(self, node, expand):
       
   206         '''Reads expanding and counting keywords, called from _overwrite.'''
       
   207         data = super(kwfilelog, self).read(node)
       
   208         return _kwtemplater.process(node, data, expand)
       
   209 
       
   210     def read(self, node):
       
   211         '''Expands keywords when reading filelog.'''
       
   212         data = super(kwfilelog, self).read(node)
       
   213         return _kwtemplater.expand(node, data)
       
   214 
       
   215     def add(self, text, meta, tr, link, p1=None, p2=None):
       
   216         '''Removes keyword substitutions when adding to filelog.'''
       
   217         text = _kwtemplater.shrink(text)
       
   218         return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2)
       
   219 
       
   220     def cmp(self, node, text):
       
   221         '''Removes keyword substitutions for comparison.'''
       
   222         text = _kwtemplater.shrink(text)
       
   223         if self.renamed(node):
       
   224             t2 = super(kwfilelog, self).read(node)
       
   225             return t2 != text
       
   226         return revlog.revlog.cmp(self, node, text)
       
   227 
   175 
   228 
   176 
   229 # store original patch.patchfile.__init__
   177 # store original patch.patchfile.__init__
   230 _patchfile_init = patch.patchfile.__init__
   178 _patchfile_init = patch.patchfile.__init__
   231 
   179 
   255 
   203 
   256 def _overwrite(ui, repo, node=None, expand=True, files=None):
   204 def _overwrite(ui, repo, node=None, expand=True, files=None):
   257     '''Overwrites selected files expanding/shrinking keywords.'''
   205     '''Overwrites selected files expanding/shrinking keywords.'''
   258     ctx = repo.changectx(node)
   206     ctx = repo.changectx(node)
   259     mf = ctx.manifest()
   207     mf = ctx.manifest()
   260     if node is not None:   # commit
   208     if node is not None:
   261         _kwtemplater.commitnode = node
   209         # commit
   262         files = [f for f in ctx.files() if f in mf]
   210         files = [f for f in ctx.files() if f in mf]
   263         notify = ui.debug
   211         notify = ui.debug
   264     else:                  # kwexpand/kwshrink
   212     else:
       
   213         # kwexpand/kwshrink
   265         notify = ui.note
   214         notify = ui.note
   266     candidates = [f for f in files if _iskwfile(f, mf.linkf)]
   215     candidates = [f for f in files if _iskwfile(f, mf.linkf)]
   267     if candidates:
   216     if candidates:
   268         candidates.sort()
   217         candidates.sort()
   269         action = expand and 'expanding' or 'shrinking'
   218         action = expand and 'expanding' or 'shrinking'
   270         for f in candidates:
   219         for f in candidates:
   271             fp = repo.file(f, kwmatch=True)
   220             data, kwfound = repo._wreadkwct(f, expand, ctx, node)
   272             data, kwfound = fp.kwctread(mf[f], expand)
       
   273             if kwfound:
   221             if kwfound:
   274                 notify(_('overwriting %s %s keywords\n') % (f, action))
   222                 notify(_('overwriting %s %s keywords\n') % (f, action))
   275                 repo.wwrite(f, data, mf.flags(f))
   223                 repo.wwrite(f, data, mf.flags(f), overwrite=True)
   276                 repo.dirstate.normal(f)
   224                 repo.dirstate.normal(f)
   277 
   225 
   278 def _kwfwrite(ui, repo, expand, *pats, **opts):
   226 def _kwfwrite(ui, repo, expand, *pats, **opts):
   279     '''Selects files and passes them to _overwrite.'''
   227     '''Selects files and passes them to _overwrite.'''
   280     status = _status(ui, repo, *pats, **opts)
   228     status = _status(ui, repo, *pats, **opts)
   364     ui.note(_('unhooked all commit hooks\n'))
   312     ui.note(_('unhooked all commit hooks\n'))
   365     ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
   313     ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
   366     repo.commit(text=msg)
   314     repo.commit(text=msg)
   367     format = ui.verbose and ' in %s' % path or ''
   315     format = ui.verbose and ' in %s' % path or ''
   368     demostatus('%s keywords expanded%s' % (kwstatus, format))
   316     demostatus('%s keywords expanded%s' % (kwstatus, format))
   369     ui.write(repo.wread(fn))
   317     ui.write(repo.wopener(fn).read())
   370     ui.debug(_('\nremoving temporary repo %s\n') % tmpdir)
   318     ui.debug(_('\nremoving temporary repo %s\n') % tmpdir)
   371     shutil.rmtree(tmpdir, ignore_errors=True)
   319     shutil.rmtree(tmpdir, ignore_errors=True)
   372 
   320 
   373 def expand(ui, repo, *pats, **opts):
   321 def expand(ui, repo, *pats, **opts):
   374     '''expand keywords in working directory
   322     '''expand keywords in working directory
   414     # 3rd argument sets expansion to False
   362     # 3rd argument sets expansion to False
   415     _kwfwrite(ui, repo, False, *pats, **opts)
   363     _kwfwrite(ui, repo, False, *pats, **opts)
   416 
   364 
   417 
   365 
   418 def reposetup(ui, repo):
   366 def reposetup(ui, repo):
   419     '''Sets up repo as kwrepo for keyword substitution.
       
   420     Overrides file method to return kwfilelog instead of filelog
       
   421     if file matches user configuration.
       
   422     Wraps commit to overwrite configured files with updated
       
   423     keyword substitutions.
       
   424     This is done for local repos only, and only if there are
       
   425     files configured at all for keyword substitution.'''
       
   426 
       
   427     if not repo.local():
   367     if not repo.local():
   428         return
   368         return
   429 
   369 
   430     nokwcommands = ('add', 'addremove', 'bundle', 'clone', 'copy',
   370     inc, exc = [], ['.hgtags', '.hg_archival.txt']
   431                     'export', 'grep', 'identify', 'incoming', 'init',
       
   432                     'log', 'outgoing', 'push', 'remove', 'rename',
       
   433                     'rollback', 'tip',
       
   434                     'convert')
       
   435     try:
       
   436         hgcmd, func, args, opts, cmdopts = dispatch._parse(ui, sys.argv[1:])
       
   437     except (cmdutil.UnknownCommand, dispatch.ParseError):
       
   438         # must be an external caller, otherwise Exception would have been
       
   439         # raised at core command line parsing
       
   440         hgcmd, args, cmdopts = externalcall
       
   441         if hgcmd is None:
       
   442             # not an "official" hg command as from command line
       
   443             return
       
   444     if hgcmd in nokwcommands:
       
   445         return
       
   446 
       
   447     if hgcmd == 'diff':
       
   448         # only expand if comparing against working dir
       
   449         node1, node2 = cmdutil.revpair(repo, cmdopts.get('rev'))
       
   450         if node2 is not None:
       
   451             return
       
   452         # shrink if rev is not current node
       
   453         if node1 is not None and node1 != repo.changectx().node():
       
   454             hgcmd = 'diff1'
       
   455 
       
   456     inc, exc = [], ['.hgtags']
       
   457     for pat, opt in ui.configitems('keyword'):
   371     for pat, opt in ui.configitems('keyword'):
   458         if opt != 'ignore':
   372         if opt != 'ignore':
   459             inc.append(pat)
   373             inc.append(pat)
   460         else:
   374         else:
   461             exc.append(pat)
   375             exc.append(pat)
   462     if not inc:
   376     if not inc:
   463         return
   377         return
   464 
   378 
   465     global _kwtemplater
   379     global _kwtemplater
   466     _kwtemplater = kwtemplater(ui, repo, inc, exc, hgcmd)
   380     _kwtemplater = kwtemplater(ui, repo, inc, exc)
   467 
   381 
   468     class kwrepo(repo.__class__):
   382     class kwrepo(repo.__class__):
   469         def file(self, f, kwmatch=False):
   383         def _wreadkwct(self, filename, expand, ctx, node):
   470             if f[0] == '/':
   384             '''Reads filename and returns tuple of data with keywords
   471                 f = f[1:]
   385             expanded/shrunk and count of keywords (for _overwrite).'''
   472             if kwmatch or _kwtemplater.matcher(f):
   386             data = super(kwrepo, self).wread(filename)
   473                 return kwfilelog(self.sopener, f)
   387             return _kwtemplater.process(filename, data, expand, ctx, node)
   474             return filelog.filelog(self.sopener, f)
       
   475 
   388 
   476         def wread(self, filename):
   389         def wread(self, filename):
   477             data = super(kwrepo, self).wread(filename)
   390             data = super(kwrepo, self).wread(filename)
   478             if hgcmd in restricted and _kwtemplater.matcher(filename):
   391             if _kwtemplater.matcher(filename):
   479                 return _kwtemplater.shrink(data)
   392                 return _kwtemplater.shrink(data)
   480             return data
   393             return data
       
   394 
       
   395         def wwrite(self, filename, data, flags, overwrite=False):
       
   396             if not overwrite and _kwtemplater.matcher(filename):
       
   397                 data = _kwtemplater.expand(filename, data)
       
   398             super(kwrepo, self).wwrite(filename, data, flags)
       
   399 
       
   400         def wwritedata(self, filename, data):
       
   401             if _kwtemplater.matcher(filename):
       
   402                 data = _kwtemplater.expand(filename, data)
       
   403             return super(kwrepo, self).wwritedata(filename, data)
   481 
   404 
   482         def commit(self, files=None, text='', user=None, date=None,
   405         def commit(self, files=None, text='', user=None, date=None,
   483                    match=util.always, force=False, force_editor=False,
   406                    match=util.always, force=False, force_editor=False,
   484                    p1=None, p2=None, extra={}):
   407                    p1=None, p2=None, extra={}):
   485             wlock = lock = None
   408             wlock = lock = None