hgkw/keyword.py
branchstable
changeset 820 67c17a447d99
parent 819 8f2c2cc51789
child 836 53a9228f13de
equal deleted inserted replaced
810:0ae62443e644 820:67c17a447d99
    80 Expansions spanning more than one line and incremental expansions,
    80 Expansions spanning more than one line and incremental expansions,
    81 like CVS' $Log$, are not supported. A keyword template map "Log =
    81 like CVS' $Log$, are not supported. A keyword template map "Log =
    82 {desc}" expands to the first line of the changeset description.
    82 {desc}" expands to the first line of the changeset description.
    83 '''
    83 '''
    84 
    84 
    85 from mercurial import commands, cmdutil, dispatch, filelog, revlog, extensions
    85 from mercurial import commands, cmdutil, dispatch, filelog, extensions
    86 from mercurial import patch, localrepo, templater, templatefilters, util, match
    86 from mercurial import localrepo, match, patch, templatefilters, templater, util
    87 from mercurial.hgweb import webcommands
    87 from mercurial.hgweb import webcommands
    88 from mercurial.i18n import _
    88 from mercurial.i18n import _
    89 import re, shutil, tempfile
    89 import re, shutil, tempfile
    90 
    90 
    91 commands.optionalrepo += ' kwdemo'
    91 commands.optionalrepo += ' kwdemo'
    92 
    92 
    93 # hg commands that do not act on keywords
    93 # hg commands that do not act on keywords
    94 nokwcommands = ('add addremove annotate bundle copy export grep incoming init'
    94 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
    95                 ' log outgoing push rename tip verify convert email glog')
    95                 ' outgoing push tip verify convert email glog')
    96 
    96 
    97 # hg commands that trigger expansion only when writing to working dir,
    97 # hg commands that trigger expansion only when writing to working dir,
    98 # not when reading filelog, and unexpand when reading from working dir
    98 # not when reading filelog, and unexpand when reading from working dir
    99 restricted = 'merge record qrecord resolve transplant'
    99 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
   100 
   100 
   101 # commands using dorecord
       
   102 recordcommands = 'record qrecord'
       
   103 # names of extensions using dorecord
   101 # names of extensions using dorecord
   104 recordextensions = 'record'
   102 recordextensions = 'record'
   105 
   103 
   106 # date like in cvs' $Date
   104 # date like in cvs' $Date
   107 utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S')
   105 utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S')
   136         'LastChangedDate': '{date|svnisodate}',
   134         'LastChangedDate': '{date|svnisodate}',
   137     })
   135     })
   138     templates.update(kwsets[ui.configbool('keywordset', 'svn')])
   136     templates.update(kwsets[ui.configbool('keywordset', 'svn')])
   139     return templates
   137     return templates
   140 
   138 
       
   139 def _shrinktext(text, subfunc):
       
   140     '''Helper for keyword expansion removal in text.
       
   141     Depending on subfunc also returns number of substitutions.'''
       
   142     return subfunc(r'$\1$', text)
       
   143 
       
   144 
   141 class kwtemplater(object):
   145 class kwtemplater(object):
   142     '''
   146     '''
   143     Sets up keyword templates, corresponding keyword regex, and
   147     Sets up keyword templates, corresponding keyword regex, and
   144     provides keyword substitution functions.
   148     provides keyword substitution functions.
   145     '''
   149     '''
   147     def __init__(self, ui, repo, inc, exc):
   151     def __init__(self, ui, repo, inc, exc):
   148         self.ui = ui
   152         self.ui = ui
   149         self.repo = repo
   153         self.repo = repo
   150         self.match = match.match(repo.root, '', [], inc, exc)
   154         self.match = match.match(repo.root, '', [], inc, exc)
   151         self.restrict = kwtools['hgcmd'] in restricted.split()
   155         self.restrict = kwtools['hgcmd'] in restricted.split()
   152         self.record = kwtools['hgcmd'] in recordcommands.split()
   156         self.record = False
   153 
   157 
   154         kwmaps = self.ui.configitems('keywordmaps')
   158         kwmaps = self.ui.configitems('keywordmaps')
   155         if kwmaps: # override default templates
   159         if kwmaps: # override default templates
   156             self.templates = dict((k, templater.parsestring(v, False))
   160             self.templates = dict((k, templater.parsestring(v, False))
   157                                   for k, v in kwmaps)
   161                                   for k, v in kwmaps)
   158         else:
   162         else:
   159             self.templates = _defaultkwmaps(self.ui)
   163             self.templates = _defaultkwmaps(self.ui)
   160         escaped = map(re.escape, self.templates.keys())
   164         escaped = '|'.join(map(re.escape, self.templates.keys()))
   161         kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
   165         self.re_kw = re.compile(r'\$(%s)\$' % escaped)
   162         self.re_kw = re.compile(kwpat)
   166         self.re_kwexp = re.compile(r'\$(%s): [^$\n\r]*? \$' % escaped)
   163 
   167 
   164         templatefilters.filters.update({'utcdate': utcdate,
   168         templatefilters.filters.update({'utcdate': utcdate,
   165                                         'svnisodate': svnisodate,
   169                                         'svnisodate': svnisodate,
   166                                         'svnutcdate': svnutcdate})
   170                                         'svnutcdate': svnutcdate})
   167 
   171 
   183         if not self.restrict and self.match(path) and not util.binary(data):
   187         if not self.restrict and self.match(path) and not util.binary(data):
   184             ctx = self.repo.filectx(path, fileid=node).changectx()
   188             ctx = self.repo.filectx(path, fileid=node).changectx()
   185             return self.substitute(data, path, ctx, self.re_kw.sub)
   189             return self.substitute(data, path, ctx, self.re_kw.sub)
   186         return data
   190         return data
   187 
   191 
   188     def iskwfile(self, path, flagfunc):
   192     def iskwfile(self, cand, ctx):
   189         '''Returns true if path matches [keyword] pattern
   193         '''Returns subset of candidates which are configured for keyword
   190         and is not a symbolic link.
   194         expansion are not symbolic links.'''
   191         Caveat: localrepository._link fails on Windows.'''
   195         return [f for f in cand if self.match(f) and not 'l' in ctx.flags(f)]
   192         return self.match(path) and not 'l' in flagfunc(path)
   196 
   193 
   197     def overwrite(self, ctx, candidates, lookup, expand, recsubn=None):
   194     def overwrite(self, ctx, candidates, iswctx, expand, cfiles):
       
   195         '''Overwrites selected files expanding/shrinking keywords.'''
   198         '''Overwrites selected files expanding/shrinking keywords.'''
   196         if cfiles is not None:
   199         if self.restrict or lookup: # exclude kw_copy
   197             candidates = [f for f in candidates if f in cfiles]
   200             candidates = self.iskwfile(candidates, ctx)
   198         candidates = [f for f in candidates if self.iskwfile(f, ctx.flags)]
   201         if not candidates:
   199         if candidates:
   202             return
   200             restrict = self.restrict
   203         commit = self.restrict and not lookup
   201             self.restrict = True        # do not expand when reading
   204         if self.restrict or expand and lookup:
   202             rollback = kwtools['hgcmd'] == 'rollback'
       
   203             mf = ctx.manifest()
   205             mf = ctx.manifest()
   204             msg = (expand and _('overwriting %s expanding keywords\n')
   206         fctx = ctx
   205                    or _('overwriting %s shrinking keywords\n'))
   207         subn = (self.restrict and self.re_kw.subn or
   206             for f in candidates:
   208                 recsubn or self.re_kwexp.subn)
   207                 if not self.record and not rollback:
   209         msg = (expand and _('overwriting %s expanding keywords\n')
   208                     data = self.repo.file(f).read(mf[f])
   210                or _('overwriting %s shrinking keywords\n'))
   209                 else:
   211         for f in candidates:
   210                     data = self.repo.wread(f)
   212             if self.restrict:
   211                 if util.binary(data):
   213                 data = self.repo.file(f).read(mf[f])
   212                     continue
   214             else:
   213                 if expand:
   215                 data = self.repo.wread(f)
   214                     if iswctx:
   216             if util.binary(data):
   215                         ctx = self.repo.filectx(f, fileid=mf[f]).changectx()
   217                 continue
   216                     data, found = self.substitute(data, f, ctx,
   218             if expand:
   217                                                   self.re_kw.subn)
   219                 if lookup:
   218                 else:
   220                     fctx = self.repo.filectx(f, fileid=mf[f]).changectx()
   219                     found = self.re_kw.search(data)
   221                 data, found = self.substitute(data, f, fctx, subn)
   220                 if found:
   222             elif self.restrict:
   221                     self.ui.note(msg % f)
   223                 found = self.re_kw.search(data)
   222                     self.repo.wwrite(f, data, mf.flags(f))
   224             else:
   223                     if iswctx and not rollback:
   225                 data, found = _shrinktext(data, subn)
   224                         self.repo.dirstate.normal(f)
   226             if found:
   225                     elif self.record:
   227                 self.ui.note(msg % f)
   226                         self.repo.dirstate.normallookup(f)
   228                 self.repo.wwrite(f, data, ctx.flags(f))
   227             self.restrict = restrict
   229                 if commit:
   228 
   230                     self.repo.dirstate.normal(f)
   229     def shrinktext(self, text):
   231                 elif self.record:
   230         '''Unconditionally removes all keyword substitutions from text.'''
   232                     self.repo.dirstate.normallookup(f)
   231         return self.re_kw.sub(r'$\1$', text)
       
   232 
   233 
   233     def shrink(self, fname, text):
   234     def shrink(self, fname, text):
   234         '''Returns text with all keyword substitutions removed.'''
   235         '''Returns text with all keyword substitutions removed.'''
   235         if self.match(fname) and not util.binary(text):
   236         if self.match(fname) and not util.binary(text):
   236             return self.shrinktext(text)
   237             return _shrinktext(text, self.re_kwexp.sub)
   237         return text
   238         return text
   238 
   239 
   239     def shrinklines(self, fname, lines):
   240     def shrinklines(self, fname, lines):
   240         '''Returns lines with keyword substitutions removed.'''
   241         '''Returns lines with keyword substitutions removed.'''
   241         if self.match(fname):
   242         if self.match(fname):
   242             text = ''.join(lines)
   243             text = ''.join(lines)
   243             if not util.binary(text):
   244             if not util.binary(text):
   244                 return self.shrinktext(text).splitlines(True)
   245                 return _shrinktext(text, self.re_kwexp.sub).splitlines(True)
   245         return lines
   246         return lines
   246 
   247 
   247     def wread(self, fname, data):
   248     def wread(self, fname, data):
   248         '''If in restricted mode returns data read from wdir with
   249         '''If in restricted mode returns data read from wdir with
   249         keyword substitutions removed.'''
   250         keyword substitutions removed.'''
   260         self.path = path
   261         self.path = path
   261 
   262 
   262     def read(self, node):
   263     def read(self, node):
   263         '''Expands keywords when reading filelog.'''
   264         '''Expands keywords when reading filelog.'''
   264         data = super(kwfilelog, self).read(node)
   265         data = super(kwfilelog, self).read(node)
       
   266         if self.renamed(node):
       
   267             return data
   265         return self.kwt.expand(self.path, node, data)
   268         return self.kwt.expand(self.path, node, data)
   266 
   269 
   267     def add(self, text, meta, tr, link, p1=None, p2=None):
   270     def add(self, text, meta, tr, link, p1=None, p2=None):
   268         '''Removes keyword substitutions when adding to filelog.'''
   271         '''Removes keyword substitutions when adding to filelog.'''
   269         text = self.kwt.shrink(self.path, text)
   272         text = self.kwt.shrink(self.path, text)
   270         return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
   273         return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
   271 
   274 
   272     def cmp(self, node, text):
   275     def cmp(self, node, text):
   273         '''Removes keyword substitutions for comparison.'''
   276         '''Removes keyword substitutions for comparison.'''
   274         text = self.kwt.shrink(self.path, text)
   277         text = self.kwt.shrink(self.path, text)
   275         if self.renamed(node):
   278         return super(kwfilelog, self).cmp(node, text)
   276             t2 = super(kwfilelog, self).read(node)
       
   277             return t2 != text
       
   278         return revlog.revlog.cmp(self, node, text)
       
   279 
   279 
   280 def _status(ui, repo, kwt, *pats, **opts):
   280 def _status(ui, repo, kwt, *pats, **opts):
   281     '''Bails out if [keyword] configuration is not active.
   281     '''Bails out if [keyword] configuration is not active.
   282     Returns status of working directory.'''
   282     Returns status of working directory.'''
   283     if kwt:
   283     if kwt:
   297     try:
   297     try:
   298         status = _status(ui, repo, kwt, *pats, **opts)
   298         status = _status(ui, repo, kwt, *pats, **opts)
   299         modified, added, removed, deleted, unknown, ignored, clean = status
   299         modified, added, removed, deleted, unknown, ignored, clean = status
   300         if modified or added or removed or deleted:
   300         if modified or added or removed or deleted:
   301             raise util.Abort(_('outstanding uncommitted changes'))
   301             raise util.Abort(_('outstanding uncommitted changes'))
   302         kwt.overwrite(wctx, clean, True, expand, None)
   302         kwt.overwrite(wctx, clean, True, expand)
   303     finally:
   303     finally:
   304         wlock.release()
   304         wlock.release()
   305 
   305 
   306 def demo(ui, repo, *args, **opts):
   306 def demo(ui, repo, *args, **opts):
   307     '''print [keywordmaps] configuration and an expansion example
   307     '''print [keywordmaps] configuration and an expansion example
   413     modified, added, removed, deleted, unknown, ignored, clean = status
   413     modified, added, removed, deleted, unknown, ignored, clean = status
   414     files = []
   414     files = []
   415     if not opts.get('unknown') or opts.get('all'):
   415     if not opts.get('unknown') or opts.get('all'):
   416         files = sorted(modified + added + clean)
   416         files = sorted(modified + added + clean)
   417     wctx = repo[None]
   417     wctx = repo[None]
   418     kwfiles = [f for f in files if kwt.iskwfile(f, wctx.flags)]
   418     kwfiles = kwt.iskwfile(files, wctx)
   419     kwunknown = [f for f in unknown if kwt.iskwfile(f, wctx.flags)]
   419     kwunknown = kwt.iskwfile(unknown, wctx)
   420     if not opts.get('ignore') or opts.get('all'):
   420     if not opts.get('ignore') or opts.get('all'):
   421         showfiles = kwfiles, kwunknown
   421         showfiles = kwfiles, kwunknown
   422     else:
   422     else:
   423         showfiles = [], []
   423         showfiles = [], []
   424     if opts.get('all') or opts.get('ignore'):
   424     if opts.get('all') or opts.get('ignore'):
   500 
   500 
   501         def kwcommitctx(self, ctx, error=False):
   501         def kwcommitctx(self, ctx, error=False):
   502             n = super(kwrepo, self).commitctx(ctx, error)
   502             n = super(kwrepo, self).commitctx(ctx, error)
   503             # no lock needed, only called from repo.commit() which already locks
   503             # no lock needed, only called from repo.commit() which already locks
   504             if not kwt.record:
   504             if not kwt.record:
       
   505                 restrict = kwt.restrict
       
   506                 kwt.restrict = True
   505                 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
   507                 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
   506                               False, True, None)
   508                               False, True)
       
   509                 kwt.restrict = restrict
   507             return n
   510             return n
   508 
   511 
   509         def rollback(self, dryrun=False):
   512         def rollback(self, dryrun=False):
   510             wlock = repo.wlock()
   513             wlock = repo.wlock()
   511             try:
   514             try:
   512                 if not dryrun:
   515                 if not dryrun:
   513                     cfiles = self['.'].files()
   516                     changed = self['.'].files()
   514                 ret = super(kwrepo, self).rollback(dryrun)
   517                 ret = super(kwrepo, self).rollback(dryrun)
   515                 if not dryrun:
   518                 if not dryrun:
   516                     ctx = self['.']
   519                     ctx = self['.']
   517                     modified, added = super(kwrepo, self).status()[:2]
   520                     modified, added = self[None].status()[:2]
   518                     kwt.overwrite(ctx, added, True, False, cfiles)
   521                     modified = [f for f in modified if f in changed]
   519                     kwt.overwrite(ctx, modified, True, True, cfiles)
   522                     added = [f for f in added if f in changed]
       
   523                     kwt.overwrite(ctx, added, True, False)
       
   524                     kwt.overwrite(ctx, modified, True, True)
   520                 return ret
   525                 return ret
   521             finally:
   526             finally:
   522                 wlock.release()
   527                 wlock.release()
   523 
   528 
   524     # monkeypatches
   529     # monkeypatches
   539     def kwweb_skip(orig, web, req, tmpl):
   544     def kwweb_skip(orig, web, req, tmpl):
   540         '''Wraps webcommands.x turning off keyword expansion.'''
   545         '''Wraps webcommands.x turning off keyword expansion.'''
   541         kwt.match = util.never
   546         kwt.match = util.never
   542         return orig(web, req, tmpl)
   547         return orig(web, req, tmpl)
   543 
   548 
       
   549     def kw_copy(orig, ui, repo, pats, opts, rename=False):
       
   550         '''Wraps cmdutil.copy so that copy/rename destinations do not
       
   551         contain expanded keywords.
       
   552         Note that the source may also be a symlink as:
       
   553         hg cp sym x                -> x is symlink
       
   554         cp sym x; hg cp -A sym x   -> x is file (maybe expanded keywords)
       
   555         '''
       
   556         orig(ui, repo, pats, opts, rename)
       
   557         if opts.get('dry_run'):
       
   558             return
       
   559         wctx = repo[None]
       
   560         candidates = [f for f in repo.dirstate.copies() if
       
   561                       kwt.match(repo.dirstate.copied(f)) and
       
   562                       not 'l' in wctx.flags(f)]
       
   563         kwt.overwrite(wctx, candidates, False, False)
       
   564 
   544     def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
   565     def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
   545         '''Wraps record.dorecord expanding keywords after recording.'''
   566         '''Wraps record.dorecord expanding keywords after recording.'''
   546         wlock = repo.wlock()
   567         wlock = repo.wlock()
   547         try:
   568         try:
   548             # record returns 0 even when nothing has changed
   569             # record returns 0 even when nothing has changed
   549             # therefore compare nodes before and after
   570             # therefore compare nodes before and after
       
   571             kwt.record = True
   550             ctx = repo['.']
   572             ctx = repo['.']
       
   573             modified, added = repo[None].status()[:2]
   551             ret = orig(ui, repo, commitfunc, *pats, **opts)
   574             ret = orig(ui, repo, commitfunc, *pats, **opts)
   552             recordctx = repo['.']
   575             recctx = repo['.']
   553             if ctx != recordctx:
   576             if ctx != recctx:
   554                 kwt.overwrite(recordctx, recordctx.files(),
   577                 modified = [f for f in modified if f in recctx]
   555                               False, True, recordctx)
   578                 added = [f for f in added if f in recctx]
       
   579                 kwt.restrict = False
       
   580                 kwt.overwrite(recctx, modified, False, True, kwt.re_kwexp.subn)
       
   581                 kwt.overwrite(recctx, added, False, True, kwt.re_kw.subn)
       
   582                 kwt.restrict = True
   556             return ret
   583             return ret
   557         finally:
   584         finally:
   558             wlock.release()
   585             wlock.release()
   559 
   586 
   560     repo.__class__ = kwrepo
   587     repo.__class__ = kwrepo
   561 
   588 
   562     extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
   589     extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
   563     extensions.wrapfunction(patch, 'diff', kw_diff)
   590     extensions.wrapfunction(patch, 'diff', kw_diff)
       
   591     extensions.wrapfunction(cmdutil, 'copy', kw_copy)
   564     for c in 'annotate changeset rev filediff diff'.split():
   592     for c in 'annotate changeset rev filediff diff'.split():
   565         extensions.wrapfunction(webcommands, c, kwweb_skip)
   593         extensions.wrapfunction(webcommands, c, kwweb_skip)
   566     for name in recordextensions.split():
   594     for name in recordextensions.split():
   567         try:
   595         try:
   568             record = extensions.find(name)
   596             record = extensions.find(name)