hgkw/keyword.py
branch0.9.2compat
changeset 444 987648c1ff69
parent 435 7bc92583f344
child 453 0804ead55da7
equal deleted inserted replaced
435:7bc92583f344 444:987648c1ff69
   104 
   104 
   105 def utcdate(date):
   105 def utcdate(date):
   106     '''Returns hgdate in cvs-like UTC format.'''
   106     '''Returns hgdate in cvs-like UTC format.'''
   107     return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
   107     return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
   108 
   108 
       
   109 def textsafe(s):
       
   110     '''Safe version of util.binary with reversed logic.
       
   111     Note: argument may not be None, which is allowed for util.binary.'''
       
   112     return '\0' not in s
   109 
   113 
   110 # make keyword tools accessible
   114 # make keyword tools accessible
   111 kwtools = {'templater': None, 'hgcmd': None}
   115 kwtools = {'templater': None, 'hgcmd': None}
   112 
   116 
   113 # monkeypatches and backwards compatibility hacks
   117 # monkeypatch argument parsing
   114 
   118 # due to backwards compatibility this can't be done in uisetup
   115 try:
   119 # uisetup introduced with extensions module in 930ed513c864
   116     # cmdutil.parse moves to dispatch._parse in 18a9fbb5cd78
       
   117     from mercurial import dispatch
       
   118     _dispatch_parse = dispatch._parse
       
   119 except ImportError:
       
   120     try:
       
   121         # commands.parse moves to cmdutil.parse in 0c61124ad877
       
   122         _dispatch_parse = cmdutil.parse
       
   123     except AttributeError:
       
   124         _dispatch_parse = commands.parse
       
   125 
       
   126 def _kwdispatch_parse(ui, args):
   120 def _kwdispatch_parse(ui, args):
   127     '''Monkeypatch dispatch._parse to obtain running hg command.'''
   121     '''Monkeypatch dispatch._parse to obtain running hg command.'''
   128     cmd, func, args, options, cmdoptions = _dispatch_parse(ui, args)
   122     cmd, func, args, options, cmdoptions = _dispatch_parse(ui, args)
   129     kwtools['hgcmd'] = cmd
   123     kwtools['hgcmd'] = cmd
   130     return cmd, func, args, options, cmdoptions
   124     return cmd, func, args, options, cmdoptions
   131 
   125 
   132 try:
   126 try:
   133     setattr(dispatch, '_parse', _kwdispatch_parse)
   127     # cmdutil.parse moves to dispatch._parse in 18a9fbb5cd78
   134 except (NameError, ImportError):
   128     from mercurial import dispatch
   135     # 0.9.4 needs ImportError
   129     _dispatch_parse = dispatch._parse
   136     if hasattr(cmdutil, 'parse'):
   130     dispatch._parse = _kwdispatch_parse
       
   131 except ImportError:
       
   132     try:
       
   133         # commands.parse moves to cmdutil.parse in 0c61124ad877
       
   134         _dispatch_parse = cmdutil.parse
   137         cmdutil.parse = _kwdispatch_parse
   135         cmdutil.parse = _kwdispatch_parse
   138     else:
   136     except AttributeError:
       
   137         _dispatch_parse = commands.parse
   139         commands.parse = _kwdispatch_parse
   138         commands.parse = _kwdispatch_parse
   140 
       
   141 try:
       
   142     # avoid spurious rejects if patchfile is available
       
   143     _patchfile_init = patch.patchfile.__init__
       
   144 
       
   145     def _kwpatchfile_init(self, ui, fname, missing=False):
       
   146         '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
       
   147         rejects or conflicts due to expanded keywords in working dir.'''
       
   148         try:
       
   149             _patchfile_init(self, ui, fname, missing=missing)
       
   150         except TypeError:
       
   151             # "missing" arg added in e90e72c6b4c7
       
   152             _patchfile_init(self, ui, fname)
       
   153         self.lines = kwtools['templater'].shrinklines(self.fname, self.lines)
       
   154 except AttributeError:
       
   155     pass
       
   156 
       
   157 _patch_diff = patch.diff
       
   158 def _kw_diff(repo, node1=None, node2=None, files=None, match=util.always,
       
   159              fp=None, changes=None, opts=None):
       
   160     # only expand if comparing against working dir
       
   161     if node2 is not None:
       
   162         kwtools['templater'].matcher = util.never
       
   163     elif node1 is not None and node1 != repo.changectx().node():
       
   164         kwtools['templater'].restrict = True
       
   165     _patch_diff(repo, node1=node1, node2=node2, files=files, match=match,
       
   166                 fp=fp, changes=changes, opts=opts)
       
   167 
       
   168 try:
       
   169     from mercurial.hgweb import webcommands
       
   170     _webcommands_changeset = webcommands.changeset
       
   171     _webcommands_filediff = webcommands.filediff
       
   172 
       
   173     def _kwweb_changeset(web, req, tmpl):
       
   174         '''Wraps webcommands.changeset turning off keyword expansion.'''
       
   175         kwtools['templater'].matcher = util.never
       
   176         return _webcommands_changeset(web, req, tmpl)
       
   177 
       
   178     def _kwweb_filediff(web, req, tmpl):
       
   179         '''Wraps webcommands.filediff turning off keyword expansion.'''
       
   180         kwtools['templater'].matcher = util.never
       
   181         return _webcommands_filediff(web, req, tmpl)
       
   182 
       
   183     webcommands.changeset = webcommands.rev = _kwweb_changeset
       
   184     webcommands.filediff = webcommands.diff = _kwweb_filediff
       
   185 
       
   186 except ImportError:
       
   187     from mercurial.hgweb.hgweb_mod import hgweb
       
   188 
       
   189     def _kwweb_do_changeset(self, req):
       
   190         kwtools['templater'].matcher = util.never
       
   191         req.write(self.changeset(self.changectx(req)))
       
   192 
       
   193     def _kwweb_do_filediff(self, req):
       
   194         kwtools['templater'].matcher = util.never
       
   195         req.write(self.filediff(self.filectx(req)))
       
   196 
       
   197     hgweb.do_changeset = hgweb.do_rev = _kwweb_do_changeset
       
   198     hgweb.do_filediff = hgweb.do_diff = _kwweb_do_filediff
       
   199 
   139 
   200 try:
   140 try:
   201     # templatefilters module introduced in 9f1e6ab76069
   141     # templatefilters module introduced in 9f1e6ab76069
   202     from mercurial import templatefilters
   142     from mercurial import templatefilters
   203     template_filters = templatefilters.filters
   143     template_filters = templatefilters.filters
   311         self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
   251         self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
   312         self.restrict = kwtools['hgcmd'] in restricted.split()
   252         self.restrict = kwtools['hgcmd'] in restricted.split()
   313 
   253 
   314         kwmaps = self.ui.configitems('keywordmaps')
   254         kwmaps = self.ui.configitems('keywordmaps')
   315         if kwmaps: # override default templates
   255         if kwmaps: # override default templates
   316             kwmaps = [(k, templater.parsestring(v, quoted=False))
   256             kwmaps = [(k, templater.parsestring(v, False))
   317                       for (k, v) in kwmaps]
   257                       for (k, v) in kwmaps]
   318             self.templates = dict(kwmaps)
   258             self.templates = dict(kwmaps)
   319         escaped = map(re.escape, self.templates.keys())
   259         escaped = map(re.escape, self.templates.keys())
   320         kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
   260         kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
   321         self.re_kw = re.compile(kwpat)
   261         self.re_kw = re.compile(kwpat)
   349             return '$%s: %s $' % (kw, template_firstline(self.ui.popbuffer()))
   289             return '$%s: %s $' % (kw, template_firstline(self.ui.popbuffer()))
   350         return subfunc(kwsub, data)
   290         return subfunc(kwsub, data)
   351 
   291 
   352     def expand(self, path, node, data):
   292     def expand(self, path, node, data):
   353         '''Returns data with keywords expanded.'''
   293         '''Returns data with keywords expanded.'''
   354         if not self.restrict and self.matcher(path) and not util.binary(data):
   294         if not self.restrict and self.matcher(path) and textsafe(data):
   355             changenode = self.getnode(path, node)
   295             changenode = self.getnode(path, node)
   356             return self.substitute(data, path, changenode, self.re_kw.sub)
   296             return self.substitute(data, path, changenode, self.re_kw.sub)
   357         return data
   297         return data
   358 
   298 
   359     def iskwfile(self, path, islink):
   299     def iskwfile(self, path, islink):
   360         '''Returns true if path matches [keyword] pattern
   300         '''Returns true if path matches [keyword] pattern
   361         and is not a symbolic link.
   301         and is not a symbolic link.
   362         Caveat: localrepository._link fails on Windows.'''
   302         Caveat: localrepository._link fails on Windows.'''
   363         return self.matcher(path) and not islink(path)
   303         return self.matcher(path) and not islink(path)
   364 
   304 
   365     def overwrite(self, node=None, expand=True, files=None):
   305     def overwrite(self, node, expand, files):
   366         '''Overwrites selected files expanding/shrinking keywords.'''
   306         '''Overwrites selected files expanding/shrinking keywords.'''
   367         ctx = self.repo.changectx(node)
   307         ctx = self.repo.changectx(node)
   368         mf = ctx.manifest()
   308         mf = ctx.manifest()
   369         if node is not None:     # commit
   309         if node is not None:     # commit
   370             files = [f for f in ctx.files() if f in mf]
   310             files = [f for f in ctx.files() if f in mf]
   378             action = expand and 'expanding' or 'shrinking'
   318             action = expand and 'expanding' or 'shrinking'
   379             overwritten = []
   319             overwritten = []
   380             for f in candidates:
   320             for f in candidates:
   381                 fp = self.repo.file(f)
   321                 fp = self.repo.file(f)
   382                 data = fp.read(mf[f])
   322                 data = fp.read(mf[f])
   383                 if util.binary(data):
   323                 if not textsafe(data):
   384                     continue
   324                     continue
   385                 if expand:
   325                 if expand:
   386                     changenode = node or self.getnode(f, mf[f])
   326                     changenode = node or self.getnode(f, mf[f])
   387                     data, found = self.substitute(data, f, changenode,
   327                     data, found = self.substitute(data, f, changenode,
   388                                                   self.re_kw.subn)
   328                                                   self.re_kw.subn)
   399         '''Unconditionally removes all keyword substitutions from text.'''
   339         '''Unconditionally removes all keyword substitutions from text.'''
   400         return self.re_kw.sub(r'$\1$', text)
   340         return self.re_kw.sub(r'$\1$', text)
   401 
   341 
   402     def shrink(self, fname, text):
   342     def shrink(self, fname, text):
   403         '''Returns text with all keyword substitutions removed.'''
   343         '''Returns text with all keyword substitutions removed.'''
   404         if self.matcher(fname) and not util.binary(text):
   344         if self.matcher(fname) and textsafe(text):
   405             return self.shrinktext(text)
   345             return self.shrinktext(text)
   406         return text
   346         return text
   407 
   347 
   408     def shrinklines(self, fname, lines):
   348     def shrinklines(self, fname, lines):
   409         '''Returns lines with keyword substitutions removed.'''
   349         '''Returns lines with keyword substitutions removed.'''
   410         if self.matcher(fname):
   350         if self.matcher(fname):
   411             text = ''.join(lines)
   351             text = ''.join(lines)
   412             if not util.binary(text):
   352             if textsafe(text):
   413                 return self.shrinktext(text).splitlines(True)
   353                 return self.shrinktext(text).splitlines(True)
   414         return lines
   354         return lines
   415 
   355 
   416     def wread(self, fname, data):
   356     def wread(self, fname, data):
   417         '''If in restricted mode returns data read from wdir with
   357         '''If in restricted mode returns data read from wdir with
   421 class kwfilelog(filelog.filelog):
   361 class kwfilelog(filelog.filelog):
   422     '''
   362     '''
   423     Subclass of filelog to hook into its read, add, cmp methods.
   363     Subclass of filelog to hook into its read, add, cmp methods.
   424     Keywords are "stored" unexpanded, and processed on reading.
   364     Keywords are "stored" unexpanded, and processed on reading.
   425     '''
   365     '''
   426     def __init__(self, opener, path):
   366     def __init__(self, opener, kwt, path):
   427         super(kwfilelog, self).__init__(opener, path)
   367         super(kwfilelog, self).__init__(opener, path)
   428         self.kwt = kwtools['templater']
   368         self.kwt = kwt
   429         self.path = path
   369         self.path = path
   430 
   370 
   431     def read(self, node):
   371     def read(self, node):
   432         '''Expands keywords when reading filelog.'''
   372         '''Expands keywords when reading filelog.'''
   433         data = super(kwfilelog, self).read(node)
   373         data = super(kwfilelog, self).read(node)
   434         return self.kwt.expand(self.path, node, data)
   374         return self.kwt.expand(self.path, node, data)
   435 
   375 
   436     def add(self, text, meta, tr, link, p1=None, p2=None):
   376     def add(self, text, meta, tr, link, p1=None, p2=None):
   437         '''Removes keyword substitutions when adding to filelog.'''
   377         '''Removes keyword substitutions when adding to filelog.'''
   438         text = self.kwt.shrink(self.path, text)
   378         text = self.kwt.shrink(self.path, text)
   439         return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2)
   379         return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
   440 
   380 
   441     def cmp(self, node, text):
   381     def cmp(self, node, text):
   442         '''Removes keyword substitutions for comparison.'''
   382         '''Removes keyword substitutions for comparison.'''
   443         text = self.kwt.shrink(self.path, text)
   383         text = self.kwt.shrink(self.path, text)
   444         if self.renamed(node):
   384         if self.renamed(node):
   465         raise util.Abort(_('outstanding uncommitted changes in given files'))
   405         raise util.Abort(_('outstanding uncommitted changes in given files'))
   466     wlock = lock = None
   406     wlock = lock = None
   467     try:
   407     try:
   468         wlock = repo.wlock()
   408         wlock = repo.wlock()
   469         lock = repo.lock()
   409         lock = repo.lock()
   470         kwt.overwrite(expand=expand, files=clean)
   410         kwt.overwrite(None, expand, clean)
   471     finally:
   411     finally:
   472         del wlock, lock
   412         del wlock, lock
   473 
   413 
   474 
   414 
   475 def demo(ui, repo, *args, **opts):
   415 def demo(ui, repo, *args, **opts):
   495     kwstatus = 'current'
   435     kwstatus = 'current'
   496     fn = 'demo.txt'
   436     fn = 'demo.txt'
   497     branchname = 'demobranch'
   437     branchname = 'demobranch'
   498     tmpdir = tempfile.mkdtemp('', 'kwdemo.')
   438     tmpdir = tempfile.mkdtemp('', 'kwdemo.')
   499     ui.note(_('creating temporary repo at %s\n') % tmpdir)
   439     ui.note(_('creating temporary repo at %s\n') % tmpdir)
   500     repo = localrepo.localrepository(ui, path=tmpdir, create=True)
   440     repo = localrepo.localrepository(ui, tmpdir, True)
   501     ui.setconfig('keyword', fn, '')
   441     ui.setconfig('keyword', fn, '')
   502     if args or opts.get('rcfile'):
   442     if args or opts.get('rcfile'):
   503         kwstatus = 'custom'
   443         kwstatus = 'custom'
   504     if opts.get('rcfile'):
   444     if opts.get('rcfile'):
   505         ui.readconfig(opts.get('rcfile'))
   445         ui.readconfig(opts.get('rcfile'))
   634 
   574 
   635     class kwrepo(repo.__class__):
   575     class kwrepo(repo.__class__):
   636         def file(self, f):
   576         def file(self, f):
   637             if f[0] == '/':
   577             if f[0] == '/':
   638                 f = f[1:]
   578                 f = f[1:]
   639             return kwfilelog(self.sopener, f)
   579             return kwfilelog(self.sopener, kwt, f)
   640 
   580 
   641         def wread(self, filename):
   581         def wread(self, filename):
   642             data = super(kwrepo, self).wread(filename)
   582             data = super(kwrepo, self).wread(filename)
   643             return kwt.wread(filename, data)
   583             return kwt.wread(filename, data)
   644 
   584 
   645         def _commit(self, files, text, user, date, match, force, lock, wlock,
   585         def _commit(self, files, text, user, date, match, force, lock, wlock,
   646                     force_editor, p1, p2, extra, empty_ok):
   586                     force_editor, p1, p2, extra, empty_ok):
   647             '''Private commit wrapper for backwards compatibility.'''
   587             '''Private commit wrapper for backwards compatibility.'''
   648             try:
   588             try:
   649                 return super(kwrepo, self).commit(files=files, text=text,
   589                 return super(kwrepo, self).commit(files, text, user, date,
   650                                                   user=user, date=date,
   590                                                   match, force,
   651                                                   match=match, force=force,
       
   652                                                   lock=lock, wlock=wlock,
   591                                                   lock=lock, wlock=wlock,
   653                                                   force_editor=force_editor,
   592                                                   force_editor=force_editor,
   654                                                   p1=p1, p2=p2, extra=extra)
   593                                                   p1=p1, p2=p2, extra=extra)
   655             except TypeError:
   594             except TypeError:
   656                 try:
   595                 try:
   657                     return super(kwrepo, self).commit(files=files, text=text,
   596                     return super(kwrepo,
   658                                                       user=user, date=date,
   597                                  self).commit(files, text, user, date,
   659                                                       match=match, force=force,
   598                                               match, force,
   660                                                       force_editor=force_editor,
   599                                               force_editor=force_editor,
   661                                                       p1=p1, p2=p2,
   600                                               p1=p1, p2=p2, extra=extra,
   662                                                       extra=extra,
   601                                               empty_ok=empty_ok)
   663                                                       empty_ok=empty_ok)
       
   664                 except TypeError:
   602                 except TypeError:
   665                     return super(kwrepo, self).commit(files=files, text=text,
   603                     return super(kwrepo,
   666                                                       user=user, date=date,
   604                                  self).commit(files, text, user, date,
   667                                                       match=match, force=force,
   605                                               match, force,
   668                                                       force_editor=force_editor,
   606                                               force_editor=force_editor,
   669                                                       p1=p1, p2=p2, extra=extra)
   607                                               p1=p1, p2=p2, extra=extra)
   670 
   608 
   671         def commit(self, files=None, text='', user=None, date=None,
   609         def commit(self, files=None, text='', user=None, date=None,
   672                    match=util.always, force=False, lock=None, wlock=None,
   610                    match=util.always, force=False, lock=None, wlock=None,
   673                    force_editor=False, p1=None, p2=None, extra={},
   611                    force_editor=False, p1=None, p2=None, extra={},
   674                    empty_ok=False):
   612                    empty_ok=False):
   700                     if _p2 == nullid:
   638                     if _p2 == nullid:
   701                         _p2 = ''
   639                         _p2 = ''
   702                     else:
   640                     else:
   703                         _p2 = hex(_p2)
   641                         _p2 = hex(_p2)
   704 
   642 
   705                 node = self._commit(files, text, user, date, match, force,
   643                 n = self._commit(files, text, user, date, match, force, _lock,
   706                                     _lock, _wlock, force_editor, p1, p2, extra,
   644                                  _wlock, force_editor, p1, p2, extra, empty_ok)
   707                                     empty_ok)
       
   708 
   645 
   709                 # restore commit hooks
   646                 # restore commit hooks
   710                 for name, cmd in commithooks.iteritems():
   647                 for name, cmd in commithooks.iteritems():
   711                     ui.setconfig('hooks', name, cmd)
   648                     ui.setconfig('hooks', name, cmd)
   712                 if node is not None:
   649                 if n is not None:
   713                     kwt.overwrite(node=node)
   650                     kwt.overwrite(n, True, None)
   714                     repo.hook('commit', node=node, parent1=_p1, parent2=_p2)
   651                     repo.hook('commit', node=n, parent1=_p1, parent2=_p2)
   715                 return node
   652                 return n
   716             finally:
   653             finally:
   717                 del _wlock, _lock
   654                 del _wlock, _lock
   718 
   655 
   719     repo.__class__ = kwrepo
   656     repo.__class__ = kwrepo
   720     patch.diff = _kw_diff
   657 
   721     if hasattr(patch, 'patchfile'):
   658     # monkeypatches
   722         patch.patchfile.__init__ = _kwpatchfile_init
   659     try:
       
   660         # avoid spurious rejects if patchfile is available
       
   661         def kwpatchfile_init(self, ui, fname, missing=False):
       
   662             '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
       
   663             rejects or conflicts due to expanded keywords in working dir.'''
       
   664             try:
       
   665                 patchfile_init(self, ui, fname, missing)
       
   666             except TypeError:
       
   667                 # "missing" arg added in e90e72c6b4c7
       
   668                 patchfile_init(self, ui, fname)
       
   669             self.lines = kwt.shrinklines(self.fname, self.lines)
       
   670 
       
   671         patchfile_init = patch.patchfile.__init__
       
   672         patch.patchfile.__init__ = kwpatchfile_init
       
   673     except AttributeError:
       
   674         pass
       
   675 
       
   676     def kw_diff(repo, node1=None, node2=None, files=None, match=util.always,
       
   677                  fp=None, changes=None, opts=None):
       
   678         # only expand if comparing against working dir
       
   679         if node2 is not None:
       
   680             kwt.matcher = util.never
       
   681         elif node1 is not None and node1 != repo.changectx().node():
       
   682             kwt.restrict = True
       
   683         patch_diff(repo, node1, node2, files, match, fp, changes, opts)
       
   684 
       
   685     patch_diff = patch.diff
       
   686     patch.diff = kw_diff
       
   687 
       
   688     try:
       
   689         from mercurial.hgweb import webcommands
       
   690         def kwweb_changeset(web, req, tmpl):
       
   691             '''Wraps webcommands.changeset turning off keyword expansion.'''
       
   692             kwt.matcher = util.never
       
   693             return webcommands_changeset(web, req, tmpl)
       
   694 
       
   695         def kwweb_filediff(web, req, tmpl):
       
   696             '''Wraps webcommands.filediff turning off keyword expansion.'''
       
   697             kwt.matcher = util.never
       
   698             return webcommands_filediff(web, req, tmpl)
       
   699 
       
   700         webcommands_changeset = webcommands.changeset
       
   701         webcommands_filediff = webcommands.filediff
       
   702         webcommands.changeset = webcommands.rev = kwweb_changeset
       
   703         webcommands.filediff = webcommands.diff = kwweb_filediff
       
   704 
       
   705     except ImportError:
       
   706         from mercurial.hgweb.hgweb_mod import hgweb
       
   707         def kwweb_do_changeset(self, req):
       
   708             kwt.matcher = util.never
       
   709             hgweb_do_changeset(self, req)
       
   710 
       
   711         def kwweb_do_filediff(self, req):
       
   712             kwt.matcher = util.never
       
   713             hgweb_do_filediff(self, req)
       
   714 
       
   715         hgweb_do_changeset = hgweb.do_changeset
       
   716         hgweb_do_filediff = hgweb.do_filediff
       
   717         hgweb.do_changeset = hgweb.do_rev = kwweb_do_changeset
       
   718         hgweb.do_filediff = hgweb.do_diff = kwweb_do_filediff
   723 
   719 
   724 
   720 
   725 cmdtable = {
   721 cmdtable = {
   726     'kwdemo':
   722     'kwdemo':
   727         (demo,
   723         (demo,