hgkw/keyword.py
branch0.9.2compat
changeset 411 b1a7f5eddeba
parent 402 5e3707030bce
child 412 79a44b1c202b
--- a/hgkw/keyword.py	Tue Feb 12 21:38:30 2008 +0100
+++ b/hgkw/keyword.py	Thu Feb 14 15:55:21 2008 +0100
@@ -85,8 +85,8 @@
         Or, better, use bundle/unbundle to share changes.
 '''
 
-from mercurial import commands, cmdutil, context, fancyopts
-from mercurial import filelog, localrepo, revlog, templater, util
+from mercurial import commands, cmdutil, context, fancyopts, filelog
+from mercurial import localrepo, patch, revlog, templater, util
 from mercurial.node import *
 from mercurial.i18n import gettext as _
 import getopt, os, re, shutil, tempfile, time
@@ -100,16 +100,17 @@
 
 # hg commands that trigger expansion only when writing to working dir,
 # not when reading filelog, and unexpand when reading from working dir
-restricted = 'diff1 record qfold qimport qnew qpush qrefresh qrecord'
+restricted = 'record qfold qimport qnew qpush qrefresh qrecord'
 
 def utcdate(date):
     '''Returns hgdate in cvs-like UTC format.'''
     return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
 
 
-_kwtemplater = _cmd = _cmdoptions = None
+# make keyword tools accessible
+kwx = { 'templater': None, 'hgcmd': None }
 
-# backwards compatibility hacks
+# monkeypatches and backwards compatibility hacks
 
 try:
     # cmdutil.parse moves to dispatch._parse in 18a9fbb5cd78
@@ -123,11 +124,10 @@
         _dispatch_parse = commands.parse
 
 def _kwdispatch_parse(ui, args):
-    '''Monkeypatch dispatch._parse to obtain
-    current command and command options (global _cmd, _cmdoptions).'''
-    global _cmd, _cmdoptions
-    _cmd, func, args, options, _cmdoptions = _dispatch_parse(ui, args)
-    return _cmd, func, args, options, _cmdoptions
+    '''Monkeypatch dispatch._parse to obtain running hg command.'''
+    cmd, func, args, options, cmdoptions = _dispatch_parse(ui, args)
+    kwx['hgcmd'] = cmd
+    return cmd, func, args, options, cmdoptions
 
 try:
     setattr(dispatch, '_parse', _kwdispatch_parse)
@@ -140,8 +140,7 @@
 
 try:
     # avoid spurious rejects if patchfile is available
-    from mercurial.patch import patchfile
-    _patchfile_init = patchfile.__init__
+    _patchfile_init = patch.patchfile.__init__
 
     def _kwpatchfile_init(self, ui, fname, missing=False):
         '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
@@ -151,30 +150,34 @@
         except TypeError:
             # "missing" arg added in e90e72c6b4c7
             _patchfile_init(self, ui, fname)
-        if _kwtemplater.matcher(self.fname):
-            # shrink keywords read from working dir
-            kwshrunk = _kwtemplater.shrink(''.join(self.lines))
-            self.lines = kwshrunk.splitlines(True)
-except ImportError:
+        self.lines = kwx['templater'].shrinklines(self.fname, self.lines)
+except AttributeError:
     pass
 
+_patch_diff = patch.diff
+def _kw_diff(repo, node1=None, node2=None, files=None, match=util.always,
+             fp=None, changes=None, opts=None):
+    # only expand if comparing against working dir
+    if node2 is not None:
+        kwx['templater'].matcher = util.never
+    elif node1 is not None and node1 != repo.changectx().node():
+        kwx['templater'].restrict = True
+    _patch_diff(repo, node1=node1, node2=node2, files=files, match=match,
+                fp=fp, changes=changes, opts=opts)
+
 try:
     from mercurial.hgweb import webcommands
 
     def _kwweb_changeset(web, req, tmpl):
         '''Wraps webcommands.changeset turning off keyword expansion.'''
-        try:
-            _kwtemplater.matcher = util.never
-        except AttributeError:
-            pass
+        if kwx['templater']:
+            kwx['templater'].matcher = util.neve
         return web.changeset(tmpl, web.changectx(req))
 
     def _kwweb_filediff(web, req, tmpl):
         '''Wraps webcommands.filediff turning off keyword expansion.'''
-        try:
-            _kwtemplater.matcher = util.never
-        except AttributeError:
-            pass
+        if kwx['templater']:
+            kwx['templater'].matcher = util.neve
         return web.filediff(tmpl, web.filectx(req))
 
     webcommands.changeset = webcommands.rev = _kwweb_changeset
@@ -184,17 +187,13 @@
     from mercurial.hgweb.hgweb_mod import hgweb
 
     def _kwweb_do_changeset(self, req):
-        try:
-            _kwtemplater.matcher = util.never
-        except AttributeError:
-            pass
+        if kwx['templater']:
+            kwx['templater'].matcher = util.never
         req.write(self.changeset(self.changectx(req)))
 
     def _kwweb_do_filediff(self, req):
-        try:
-            _kwtemplater.matcher = util.never
-        except AttributeError:
-            pass
+        if kwx['templater']:
+            kwx['templater'].matcher = util.never
         req.write(self.filediff(self.filectx(req)))
 
     hgweb.do_changeset = hgweb.do_rev = _kwweb_do_changeset
@@ -308,13 +307,11 @@
         'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
     }
 
-    def __init__(self, ui, repo, inc, exc, hgcmd):
+    def __init__(self, ui, repo, inc, exc):
         self.ui = ui
         self.repo = repo
         self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
-        self.restrict = hgcmd in restricted.split()
-        self.commitnode = None
-        self.path = ''
+        self.restrict = kwx['hgcmd'] in restricted.split()
 
         kwmaps = self.ui.configitems('keywordmaps')
         if kwmaps: # override default templates
@@ -338,46 +335,89 @@
             return cmdutil.changeset_templater(self.ui, self.repo,
                                                False, None, '', False)
 
-    def substitute(self, node, data, subfunc):
-        '''Obtains file's changenode if commit node not given,
-        and calls given substitution function.'''
-        if self.commitnode:
-            fnode = self.commitnode
-        else:
-            c = context.filectx(self.repo, self.path, fileid=node)
-            fnode = c.node()
+    def getnode(self, path, fnode):
+        '''Derives changenode from file context.'''
+        c = context.filectx(self.repo, path, fileid=fnode)
+        return c.node()
 
+    def substitute(self, data, path, node, subfunc):
+        '''Replaces keywords in data with expanded template.'''
         def kwsub(mobj):
-            '''Substitutes keyword using corresponding template.'''
             kw = mobj.group(1)
             self.ct.use_template(self.templates[kw])
             self.ui.pushbuffer()
-            self.ct.show(changenode=fnode, root=self.repo.root, file=self.path)
+            self.ct.show(changenode=node, root=self.repo.root, file=path)
             return '$%s: %s $' % (kw, template_firstline(self.ui.popbuffer()))
-
         return subfunc(kwsub, data)
 
-    def expand(self, node, data):
+    def expand(self, path, node, data):
         '''Returns data with keywords expanded.'''
-        if self.restrict or util.binary(data):
-            return data
-        return self.substitute(node, data, self.re_kw.sub)
+        if not self.restrict and self.matcher(path) and not util.binary(data):
+            changenode = self.getnode(path, node)
+            return self.substitute(data, path, changenode, self.re_kw.sub)
+        return data
+
+    def iskwfile(self, path, islink):
+        '''Returns true if path matches [keyword] pattern
+        and is not a symbolic link.
+        Caveat: localrepository._link fails on Windows.'''
+        return self.matcher(path) and not islink(path)
 
-    def process(self, node, data, expand):
-        '''Returns a tuple: data, count.
-        Count is number of keywords/keyword substitutions,
-        telling caller whether to act on file containing data.'''
-        if util.binary(data):
-            return data, None
-        if expand:
-            return self.substitute(node, data, self.re_kw.subn)
-        return data, self.re_kw.search(data)
+    def overwrite(self, node=None, expand=True, files=None):
+        '''Overwrites selected files expanding/shrinking keywords.'''
+        ctx = self.repo.changectx(node)
+        mf = ctx.manifest()
+        if node is not None:     # commit
+            files = [f for f in ctx.files() if f in mf]
+            notify = self.ui.debug
+        else:                    # kwexpand/kwshrink
+            notify = self.ui.note
+        candidates = [f for f in files if self.iskwfile(f, mf.linkf)]
+        if candidates:
+            self.restrict = True # do not expand when reading
+            candidates.sort()
+            action = expand and 'expanding' or 'shrinking'
+            overwritten = []
+            for f in candidates:
+                fp = self.repo.file(f)
+                data = fp.read(mf[f])
+                if util.binary(data):
+                    continue
+                if expand:
+                    changenode = node or self.getnode(f, mf[f])
+                    data, found = self.substitute(data, f, changenode,
+                                                  self.re_kw.subn)
+                else:
+                    found = self.re_kw.search(data)
+                if found:
+                    notify(_('overwriting %s %s keywords\n') % (f, action))
+                    self.repo.wwrite(f, data, mf.flags(f))
+                    overwritten.append(f)
+            _normal(self.repo, overwritten)
+            self.restrict = False
 
-    def shrink(self, text):
+    def shrinktext(self, text):
+        '''Unconditionally removes all keyword substitutions from text.'''
+        return self.re_kw.sub(r'$\1$', text)
+
+    def shrink(self, fname, text):
         '''Returns text with all keyword substitutions removed.'''
-        if util.binary(text):
-            return text
-        return self.re_kw.sub(r'$\1$', text)
+        if self.matcher(fname) and not util.binary(text):
+            return self.shrinktext(text)
+        return text
+
+    def shrinklines(self, fname, lines):
+        '''Returns lines with keyword substitutions removed.'''
+        if self.matcher(fname):
+            text = ''.join(lines)
+            if not util.binary(text):
+                return self.shrinktext(text).splitlines(True)
+        return lines
+
+    def wread(self, fname, data):
+        '''If in restricted mode returns data read from wdir with
+        keyword substitutions removed.'''
+        return self.restrict and self.shrink(fname, data) or data
 
 class kwfilelog(filelog.filelog):
     '''
@@ -386,71 +426,41 @@
     '''
     def __init__(self, opener, path):
         super(kwfilelog, self).__init__(opener, path)
-        _kwtemplater.path = path
-
-    def kwctread(self, node, expand):
-        '''Reads expanding and counting keywords, called from _overwrite.'''
-        data = super(kwfilelog, self).read(node)
-        return _kwtemplater.process(node, data, expand)
+        self.kwt = kwx['templater']
+        self.path = path
 
     def read(self, node):
         '''Expands keywords when reading filelog.'''
         data = super(kwfilelog, self).read(node)
-        return _kwtemplater.expand(node, data)
+        return self.kwt.expand(self.path, node, data)
 
     def add(self, text, meta, tr, link, p1=None, p2=None):
         '''Removes keyword substitutions when adding to filelog.'''
-        text = _kwtemplater.shrink(text)
+        text = self.kwt.shrink(self.path, text)
         return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2)
 
     def cmp(self, node, text):
         '''Removes keyword substitutions for comparison.'''
-        text = _kwtemplater.shrink(text)
+        text = self.kwt.shrink(self.path, text)
         if self.renamed(node):
             t2 = super(kwfilelog, self).read(node)
             return t2 != text
         return revlog.revlog.cmp(self, node, text)
 
-def _iskwfile(f, link):
-    return not link(f) and _kwtemplater.matcher(f)
-
-def _status(ui, repo, *pats, **opts):
+def _status(ui, repo, kwt, *pats, **opts):
     '''Bails out if [keyword] configuration is not active.
     Returns status of working directory.'''
-    if _kwtemplater:
+    if kwt:
         files, match, anypats = cmdutil.matchpats(repo, pats, opts)
         return repo.status(files=files, match=match, list_clean=True)
     if ui.configitems('keyword'):
         raise util.Abort(_('[keyword] patterns cannot match'))
     raise util.Abort(_('no [keyword] patterns configured'))
 
-def _overwrite(ui, repo, node=None, expand=True, files=None):
-    '''Overwrites selected files expanding/shrinking keywords.'''
-    ctx = repo.changectx(node)
-    mf = ctx.manifest()
-    if node is not None:   # commit
-        _kwtemplater.commitnode = node
-        files = [f for f in ctx.files() if f in mf]
-        notify = ui.debug
-    else:                  # kwexpand/kwshrink
-        notify = ui.note
-    candidates = [f for f in files if _iskwfile(f, mf.linkf)]
-    if candidates:
-        overwritten = []
-        candidates.sort()
-        action = expand and 'expanding' or 'shrinking'
-        for f in candidates:
-            fp = repo.file(f, kwmatch=True)
-            data, kwfound = fp.kwctread(mf[f], expand)
-            if kwfound:
-                notify(_('overwriting %s %s keywords\n') % (f, action))
-                _wwrite(repo, f, data, mf)
-                overwritten.append(f)
-        _normal(repo, overwritten)
-
 def _kwfwrite(ui, repo, expand, *pats, **opts):
-    '''Selects files and passes them to _overwrite.'''
-    status = _status(ui, repo, *pats, **opts)
+    '''Selects files and passes them to kwtemplater.overwrite.'''
+    kwt = kwx['templater']
+    status = _status(ui, repo, kwt, *pats, **opts)
     modified, added, removed, deleted, unknown, ignored, clean = status
     if modified or added or removed or deleted:
         raise util.Abort(_('outstanding uncommitted changes in given files'))
@@ -458,7 +468,7 @@
     try:
         wlock = repo.wlock()
         lock = repo.lock()
-        _overwrite(ui, repo, expand=expand, files=clean)
+        kwt.overwrite(expand=expand, files=clean)
     finally:
         del wlock, lock
 
@@ -560,7 +570,8 @@
     keyword expansion.
     That is, files matched by [keyword] config patterns but not symlinks.
     '''
-    status = _status(ui, repo, *pats, **opts)
+    kwt = kwx['templater']
+    status = _status(ui, repo, kwt, *pats, **opts)
     modified, added, removed, deleted, unknown, ignored, clean = status
     files = modified + added + clean
     if opts.get('untracked'):
@@ -572,7 +583,7 @@
     else:
         mf = wctx.manifest()
         islink = mf.linkf
-    kwfiles = [f for f in files if _iskwfile(f, islink)]
+    kwfiles = [f for f in files if kwt.iskwfile(f, islink)]
     cwd = pats and repo.getcwd() or ''
     kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
     if opts.get('all') or opts.get('ignore'):
@@ -603,11 +614,8 @@
     This is done for local repos only, and only if there are
     files configured at all for keyword substitution.'''
 
-    global _kwtemplater
-    hgcmd, hgcmdopts = _cmd, _cmdoptions
-
     try:
-        if (not repo.local() or hgcmd in nokwcommands.split() 
+        if (not repo.local() or kwx['hgcmd'] in nokwcommands.split() 
             or '.hg' in repo.root.split(os.sep)
             or repo._url.startswith('bundle:')):
             return
@@ -623,30 +631,17 @@
     if not inc:
         return
 
-    if hgcmd == 'diff':
-        # only expand if comparing against working dir
-        node1, node2 = cmdutil.revpair(repo, hgcmdopts.get('rev'))
-        if node2 is not None:
-            return
-        # shrink if rev is not current node
-        if node1 is not None and node1 != repo.changectx().node():
-            hgcmd = 'diff1'
-
-    _kwtemplater = kwtemplater(ui, repo, inc, exc, hgcmd)
+    kwx['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
 
     class kwrepo(repo.__class__):
-        def file(self, f, kwmatch=False):
+        def file(self, f):
             if f[0] == '/':
                 f = f[1:]
-            if kwmatch or _kwtemplater.matcher(f):
-                return kwfilelog(self.sopener, f)
-            return filelog.filelog(self.sopener, f)
+            return kwfilelog(self.sopener, f)
 
         def wread(self, filename):
             data = super(kwrepo, self).wread(filename)
-            if _kwtemplater.restrict and _kwtemplater.matcher(filename):
-                return _kwtemplater.shrink(data)
-            return data
+            return kwt.wread(filename, data)
 
         def _commit(self, files, text, user, date, match, force, lock, wlock,
                     force_editor, p1, p2, extra, empty_ok):
@@ -716,17 +711,16 @@
                 for name, cmd in commithooks.iteritems():
                     ui.setconfig('hooks', name, cmd)
                 if node is not None:
-                    _overwrite(ui, self, node=node)
+                    kwt.overwrite(node=node)
                     repo.hook('commit', node=node, parent1=_p1, parent2=_p2)
                 return node
             finally:
                 del _wlock, _lock
 
     repo.__class__ = kwrepo
-    try:
-        patchfile.__init__ = _kwpatchfile_init
-    except NameError:
-        pass
+    patch.diff = _kw_diff
+    if hasattr(patch, 'patchfile'):
+        patch.patchfile.__init__ = _kwpatchfile_init
 
 
 cmdtable = {