hgkw/keyword.py
changeset 307 32061d23db14
parent 306 78b80b2511de
child 308 2a20fa267041
equal deleted inserted replaced
306:78b80b2511de 307:32061d23db14
    89 commands.optionalrepo += ' kwdemo'
    89 commands.optionalrepo += ' kwdemo'
    90 
    90 
    91 def utcdate(date):
    91 def utcdate(date):
    92     '''Returns hgdate in cvs-like UTC format.'''
    92     '''Returns hgdate in cvs-like UTC format.'''
    93     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]))
       
    94 
       
    95 _kwtemplater = None
    94 
    96 
    95 class kwtemplater(object):
    97 class kwtemplater(object):
    96     '''
    98     '''
    97     Sets up keyword templates, corresponding keyword regex, and
    99     Sets up keyword templates, corresponding keyword regex, and
    98     provides keyword substitution functions.
   100     provides keyword substitution functions.
   105         'Source': '{root}/{file},v',
   107         'Source': '{root}/{file},v',
   106         'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
   108         'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
   107         'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
   109         'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
   108     }
   110     }
   109 
   111 
   110     def __init__(self, ui, repo, expand, path='', node=None):
   112     def __init__(self, ui, repo, inc, exc):
   111         self.ui = ui
   113         self.ui = ui
   112         self.repo = repo
   114         self.repo = repo
   113         self.ct = expand or None
   115         self.match = util.matcher(repo.root, inc=inc, exc=exc)[1]
   114         self.path = path
   116         self.node = None
   115         self.node = node
   117         self.path = ''
   116 
   118 
   117         kwmaps = self.ui.configitems('keywordmaps')
   119         kwmaps = self.ui.configitems('keywordmaps')
   118         if kwmaps: # override default templates
   120         if kwmaps: # override default templates
   119             kwmaps = [(k, templater.parsestring(v, quoted=False))
   121             kwmaps = [(k, templater.parsestring(v, quoted=False))
   120                       for (k, v) in kwmaps]
   122                       for (k, v) in kwmaps]
   121             self.templates = dict(kwmaps)
   123             self.templates = dict(kwmaps)
   122         escaped = map(re.escape, self.templates.keys())
   124         escaped = map(re.escape, self.templates.keys())
   123         kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
   125         kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
   124         self.re_kw = re.compile(kwpat)
   126         self.re_kw = re.compile(kwpat)
   125         if self.ct:
   127 
   126             templater.common_filters['utcdate'] = utcdate
   128         templater.common_filters['utcdate'] = utcdate
   127             self.ct = cmdutil.changeset_templater(self.ui, self.repo,
   129         self.ct = cmdutil.changeset_templater(self.ui, self.repo,
   128                                                   False, '', False)
   130                                               False, '', False)
   129 
   131 
   130     def substitute(self, node, data, subfunc):
   132     def substitute(self, node, data, subfunc):
   131         '''Obtains node if missing, and calls given substitution function.'''
   133         '''Obtains node if missing, and calls given substitution function.'''
   132         if not self.node:
   134         if not self.node:
   133             c = context.filectx(self.repo, self.path, fileid=node)
   135             c = context.filectx(self.repo, self.path, fileid=node)
   148         '''Returns data with keywords expanded.'''
   150         '''Returns data with keywords expanded.'''
   149         if util.binary(data):
   151         if util.binary(data):
   150             return data
   152             return data
   151         return self.substitute(node, data, self.re_kw.sub)
   153         return self.substitute(node, data, self.re_kw.sub)
   152 
   154 
   153     def process(self, node, data):
   155     def process(self, node, data, expand):
   154         '''Returns a tuple: data, count.
   156         '''Returns a tuple: data, count.
   155         Count is number of keywords/keyword substitutions, indicates
   157         Count is number of keywords/keyword substitutions, indicates
   156         to caller whether to act on file containing data.
   158         to caller whether to act on file containing data.
   157         Keywords in data are expanded, if templater was initialized.'''
   159         Keywords in data are expanded, if templater was initialized.'''
   158         if util.binary(data):
   160         if util.binary(data):
   159             return data, None
   161             return data, None
   160         if self.ct:
   162         if expand:
   161             return self.substitute(node, data, self.re_kw.subn)
   163             return self.substitute(node, data, self.re_kw.subn)
   162         return data, self.re_kw.search(data)
   164         return data, self.re_kw.search(data)
   163 
   165 
   164     def shrink(self, text):
   166     def shrink(self, text):
   165         '''Returns text with all keyword substitutions removed.'''
   167         '''Returns text with all keyword substitutions removed.'''
   166         if util.binary(text):
   168         if util.binary(text):
   167             return text
   169             return text
   168         return self.re_kw.sub(r'$\1$', text)
   170         return self.re_kw.sub(r'$\1$', text)
   169 
   171 
   170     def overwrite(self, candidates, man, commit):
       
   171         '''Overwrites files in working directory if keywords are detected.
       
   172         Keywords are expanded if keyword templater is initialized,
       
   173         otherwise their substitution is removed.'''
       
   174         expand = self.ct is not None
       
   175         action = ('shrinking', 'expanding')[expand]
       
   176         notify = (self.ui.note, self.ui.debug)[commit]
       
   177         for f in candidates:
       
   178             fp = self.repo.file(f, kwexp=expand, kwmatch=True)
       
   179             data, kwfound = fp.kwctread(man[f])
       
   180             if kwfound:
       
   181                 notify(_('overwriting %s %s keywords\n') % (f, action))
       
   182                 self.repo.wwrite(f, data, man.flags(f))
       
   183                 self.repo.dirstate.normal(f)
       
   184 
       
   185 class kwfilelog(filelog.filelog):
   172 class kwfilelog(filelog.filelog):
   186     '''
   173     '''
   187     Subclass of filelog to hook into its read, add, cmp methods.
   174     Subclass of filelog to hook into its read, add, cmp methods.
   188     Keywords are "stored" unexpanded, and processed on reading.
   175     Keywords are "stored" unexpanded, and processed on reading.
   189     '''
   176     '''
   190     def __init__(self, opener, path, kwtemplater):
   177     def __init__(self, opener, path, kwtemplater):
   191         super(kwfilelog, self).__init__(opener, path)
   178         super(kwfilelog, self).__init__(opener, path)
   192         self.kwtemplater = kwtemplater
   179         self.kwtemplater = kwtemplater
   193 
   180 
   194     def kwctread(self, node):
   181     def kwctread(self, node, expand):
   195         '''Reads expanding and counting keywords
   182         '''Reads expanding and counting keywords
   196         (only called from kwtemplater.overwrite).'''
   183         (only called from kwtemplater.overwrite).'''
   197         data = super(kwfilelog, self).read(node)
   184         data = super(kwfilelog, self).read(node)
   198         return self.kwtemplater.process(node, data)
   185         return self.kwtemplater.process(node, data, expand)
   199 
   186 
   200     def read(self, node):
   187     def read(self, node):
   201         '''Expands keywords when reading filelog.'''
   188         '''Expands keywords when reading filelog.'''
   202         data = super(kwfilelog, self).read(node)
   189         data = super(kwfilelog, self).read(node)
   203         return self.kwtemplater.expand(node, data)
   190         return self.kwtemplater.expand(node, data)
   213         if self.renamed(node):
   200         if self.renamed(node):
   214             t2 = super(kwfilelog, self).read(node)
   201             t2 = super(kwfilelog, self).read(node)
   215             return t2 != text
   202             return t2 != text
   216         return revlog.revlog.cmp(self, node, text)
   203         return revlog.revlog.cmp(self, node, text)
   217 
   204 
   218 def _status(ui, repo, *pats, **opts):
   205 def _status(ui, repo, kwtemplater, *pats, **opts):
   219     '''Bails out if [keyword] configuration is not active.
   206     '''Bails out if [keyword] configuration is not active.
   220     Returns status of working directory.'''
   207     Returns status of working directory.'''
   221     if hasattr(ui, 'kwfmatcher'):
   208     if kwtemplater:
   222         files, match, anypats = cmdutil.matchpats(repo, pats, opts)
   209         files, match, anypats = cmdutil.matchpats(repo, pats, opts)
   223         return repo.status(files=files, match=match, list_clean=True)
   210         return repo.status(files=files, match=match, list_clean=True)
   224     if ui.configitems('keyword'):
   211     if ui.configitems('keyword'):
   225         raise util.Abort(_('[keyword] patterns cannot match'))
   212         raise util.Abort(_('[keyword] patterns cannot match'))
   226     raise util.Abort(_('no [keyword] patterns configured'))
   213     raise util.Abort(_('no [keyword] patterns configured'))
   227 
   214 
   228 def _iskwfile(ui, man, f):
   215 def _overwrite(ui, repo, kwtemplater, node=None, expand=True, files=None):
   229     return not man.linkf(f) and ui.kwfmatcher(f)
   216     '''Overwrites selected files expanding/shrinking keywords.'''
   230 
   217     ctx = repo.changectx(node)
   231 def _overwrite(ui, repo, files, node, man, expand, commit):
   218     mf = ctx.manifest()
   232     '''Passes given files to kwtemplater for overwriting.'''
   219     if files is None:
   233     files.sort()
   220         notify = ui.debug # commit
   234     kwt = kwtemplater(ui, repo, expand, node=node)
   221         files = [f for f in ctx.files() if mf.has_key(f)]
   235     kwt.overwrite(files, man, commit)
   222     else:
       
   223         notify = ui.note  # kwexpand/kwshrink
       
   224     candidates = [f for f in files if not mf.linkf(f) and kwtemplater.match(f)]
       
   225     if candidates:
       
   226         candidates.sort()
       
   227         action = expand and 'expanding' or 'shrinking'
       
   228         kwtemplater.node = node or ctx.node()
       
   229         for f in candidates:
       
   230             fp = repo.file(f, kwmatch=True)
       
   231             data, kwfound = fp.kwctread(mf[f], expand)
       
   232             if kwfound:
       
   233                 notify(_('overwriting %s %s keywords\n') % (f, action))
       
   234                 repo.wwrite(f, data, mf.flags(f))
       
   235                 repo.dirstate.normal(f)
   236 
   236 
   237 def _kwfwrite(ui, repo, expand, *pats, **opts):
   237 def _kwfwrite(ui, repo, expand, *pats, **opts):
   238     '''Selects files and passes them to _overwrite.'''
   238     '''Selects files and passes them to _overwrite.'''
   239     status = _status(ui, repo, *pats, **opts)
   239     global _kwtemplater
       
   240     status = _status(ui, repo, _kwtemplater, *pats, **opts)
   240     modified, added, removed, deleted, unknown, ignored, clean = status
   241     modified, added, removed, deleted, unknown, ignored, clean = status
   241     if modified or added or removed or deleted:
   242     if modified or added or removed or deleted:
   242         raise util.Abort(_('outstanding uncommitted changes in given files'))
   243         raise util.Abort(_('outstanding uncommitted changes in given files'))
   243     wlock = lock = None
   244     wlock = lock = None
   244     try:
   245     try:
   245         wlock = repo.wlock()
   246         wlock = repo.wlock()
   246         lock = repo.lock()
   247         lock = repo.lock()
   247         ctx = repo.changectx()
   248         _overwrite(ui, repo, _kwtemplater, expand=expand, files=clean)
   248         man = ctx.manifest()
       
   249         candidates = [f for f in clean if _iskwfile(ui, man, f)]
       
   250         if candidates:
       
   251             # 7th argument sets commit to False
       
   252             _overwrite(ui, repo, candidates, ctx.node(), man, expand, False)
       
   253     finally:
   249     finally:
   254         del wlock, lock
   250         del wlock, lock
   255 
   251 
   256 
   252 
   257 def demo(ui, repo, *args, **opts):
   253 def demo(ui, repo, *args, **opts):
   350 
   346 
   351     Crosscheck which files in working directory are potential targets for
   347     Crosscheck which files in working directory are potential targets for
   352     keyword expansion.
   348     keyword expansion.
   353     That is, files matched by [keyword] config patterns but not symlinks.
   349     That is, files matched by [keyword] config patterns but not symlinks.
   354     '''
   350     '''
   355     status = _status(ui, repo, *pats, **opts)
   351     global _kwtemplater
       
   352     status = _status(ui, repo, _kwtemplater, *pats, **opts)
   356     modified, added, removed, deleted, unknown, ignored, clean = status
   353     modified, added, removed, deleted, unknown, ignored, clean = status
   357     if opts['untracked']:
   354     if opts['untracked']:
   358         files = modified + added + unknown + clean
   355         files = modified + added + unknown + clean
   359     else:
   356     else:
   360         files = modified + added + clean
   357         files = modified + added + clean
   361     files.sort()
   358     files.sort()
   362     kwfiles = [f for f in files if ui.kwfmatcher(f) and not repo._link(f)]
   359     kwfiles = [f for f in files if _kwtemplater.match(f) and not repo._link(f)]
   363     cwd = pats and repo.getcwd() or ''
   360     cwd = pats and repo.getcwd() or ''
   364     allf = opts['all']
   361     allf = opts['all']
   365     ignore = opts['ignore']
   362     ignore = opts['ignore']
   366     if ignore:
   363     if ignore:
   367         kwfstats = ()
   364         kwfstats = ()
   419         else:
   416         else:
   420             exc.append(pat)
   417             exc.append(pat)
   421     if not inc:
   418     if not inc:
   422         return
   419         return
   423 
   420 
   424     ui.kwfmatcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
   421     global _kwtemplater
       
   422     _kwtemplater = kwtemplater(ui, repo, inc, exc)
   425 
   423 
   426     class kwrepo(repo.__class__):
   424     class kwrepo(repo.__class__):
   427         def file(self, f, kwexp=True, kwmatch=False):
   425         def file(self, f, kwmatch=False):
   428             if f[0] == '/':
   426             if f[0] == '/':
   429                 f = f[1:]
   427                 f = f[1:]
   430             if kwmatch or ui.kwfmatcher(f):
   428             if kwmatch or _kwtemplater.match(f):
   431                 kwt = kwtemplater(ui, self, kwexp, path=f)
   429                 _kwtemplater.path = f
   432                 return kwfilelog(self.sopener, f, kwt)
   430                 return kwfilelog(self.sopener, f, _kwtemplater)
   433             return filelog.filelog(self.sopener, f)
   431             return filelog.filelog(self.sopener, f)
   434 
   432 
   435         def commit(self, files=None, text='', user=None, date=None,
   433         def commit(self, files=None, text='', user=None, date=None,
   436                    match=util.always, force=False, force_editor=False,
   434                    match=util.always, force=False, force_editor=False,
   437                    p1=None, p2=None, extra={}):
   435                    p1=None, p2=None, extra={}):
   466 
   464 
   467                 # restore commit hooks
   465                 # restore commit hooks
   468                 for name, cmd in commithooks:
   466                 for name, cmd in commithooks:
   469                     ui.setconfig('hooks', name, cmd)
   467                     ui.setconfig('hooks', name, cmd)
   470                 if node is not None:
   468                 if node is not None:
   471                     cl = self.changelog.read(node)
   469                     _overwrite(ui, self, _kwtemplater, node=node)
   472                     mn = self.manifest.read(cl[0])
       
   473                     candidates = [f for f in cl[3] if mn.has_key(f)
       
   474                                   and _iskwfile(ui, mn, f)]
       
   475                     if candidates:
       
   476                         # 6th, 7th arguments set expansion, commit to True
       
   477                         _overwrite(ui, self, candidates, node, mn, True, True)
       
   478                     repo.hook('commit', node=node, parent1=_p1, parent2=_p2)
   470                     repo.hook('commit', node=node, parent1=_p1, parent2=_p2)
   479                 return node
   471                 return node
   480             finally:
   472             finally:
   481                 del wlock, lock
   473                 del wlock, lock
   482 
   474