hgkw/keyword.py
changeset 378 0683aaf458d8
parent 377 ce5eb725d0c6
parent 375 ed160b097bad
child 381 17e94f77de6a
--- a/hgkw/keyword.py	Wed Feb 06 11:32:34 2008 +0100
+++ b/hgkw/keyword.py	Wed Feb 06 17:25:37 2008 +0100
@@ -80,19 +80,29 @@
 "Log = {desc}" expands to the first line of the changeset description.
 '''
 
-from mercurial import commands, cmdutil, context, localrepo
-from mercurial import patch, revlog, templater, templatefilters, util
+from mercurial import commands, cmdutil, context, dispatch, filelog, revlog
+from mercurial import patch, localrepo, templater, templatefilters, util
 from mercurial.node import *
-from mercurial.hgweb import webcommands
 from mercurial.i18n import _
-import mimetypes, re, shutil, tempfile, time
+import re, shutil, sys, tempfile, time
 
 commands.optionalrepo += ' kwdemo'
 
+# hg commands that do not act on keywords
+nokwcommands = ('add addremove bundle copy export grep identify incoming init'
+                ' log outgoing push remove rename rollback tip convert')
+
+# 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'
+
 def utcdate(date):
     '''Returns hgdate in cvs-like UTC format.'''
     return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
 
+
+_kwtemplater = None
+
 class kwtemplater(object):
     '''
     Sets up keyword templates, corresponding keyword regex, and
@@ -108,11 +118,13 @@
         'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
     }
 
-    def __init__(self, ui, repo, inc, exc):
+    def __init__(self, ui, repo, inc, exc, restricted):
         self.ui = ui
         self.repo = repo
         self.matcher = util.matcher(repo.root, inc=inc, exc=exc)[1]
-        self.ctx = None
+        self.restricted = restricted
+        self.commitnode = None
+        self.path = ''
 
         kwmaps = self.ui.configitems('keywordmaps')
         if kwmaps: # override default templates
@@ -127,67 +139,102 @@
         self.ct = cmdutil.changeset_templater(self.ui, self.repo,
                                               False, '', False)
 
-    def substitute(self, path, data, node, subfunc):
-        '''Obtains file's changenode if node not given,
+    def substitute(self, node, data, subfunc):
+        '''Obtains file's changenode if commit node not given,
         and calls given substitution function.'''
-        if node is None:
-            # kwrepo.wwrite except when overwriting on commit
-            try:
-                fnode = self.ctx.filenode(path)
-                fl = self.repo.file(path)
-                c = context.filectx(self.repo, path, fileid=fnode, filelog=fl)
-                node = c.node()
-            except revlog.LookupError:
-                # eg: convert
-                return subfunc == self.re_kw.sub and data or (data, None)
+        if self.commitnode:
+            fnode = self.commitnode
+        else:
+            c = context.filectx(self.repo, self.path, fileid=node)
+            fnode = c.node()
 
         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=node, root=self.repo.root, file=path)
+            self.ct.show(changenode=fnode, root=self.repo.root, file=self.path)
             ekw = templatefilters.firstline(self.ui.popbuffer())
             return '$%s: %s $' % (kw, ekw)
 
         return subfunc(kwsub, data)
 
-    def expand(self, path, data, ctx):
+    def expand(self, node, data):
         '''Returns data with keywords expanded.'''
-        if util.binary(data):
+        if self.restricted or util.binary(data):
             return data
-        if self.ctx is None:
-            self.ctx = ctx or self.repo.changectx()
-        return self.substitute(path, data, None, self.re_kw.sub)
+        return self.substitute(node, data, self.re_kw.sub)
 
-    def process(self, path, data, expand, ctx, node):
+    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:
-            self.ctx = ctx
-            return self.substitute(path, data, node, self.re_kw.subn)
-        return self.re_kw.subn(r'$\1$', data)
+            return self.substitute(node, data, self.re_kw.subn)
+        return data, self.re_kw.search(data)
+
+    def shrink(self, text):
+        '''Returns text with all keyword substitutions removed.'''
+        if util.binary(text):
+            return text
+        return self.re_kw.sub(r'$\1$', text)
+
+class kwfilelog(filelog.filelog):
+    '''
+    Subclass of filelog to hook into its read, add, cmp methods.
+    Keywords are "stored" unexpanded, and processed on reading.
+    '''
+    def __init__(self, opener, path):
+        super(kwfilelog, self).__init__(opener, path)
+        _kwtemplater.path = path
 
-    def shrink(self, data):
-        '''Returns text with all keyword substitutions removed.'''
-        if util.binary(data):
-            return data
-        return self.re_kw.sub(r'$\1$', data)
+    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)
+
+    def read(self, node):
+        '''Expands keywords when reading filelog.'''
+        data = super(kwfilelog, self).read(node)
+        return _kwtemplater.expand(node, data)
+
+    def add(self, text, meta, tr, link, p1=None, p2=None):
+        '''Removes keyword substitutions when adding to filelog.'''
+        text = _kwtemplater.shrink(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)
+        if self.renamed(node):
+            t2 = super(kwfilelog, self).read(node)
+            return t2 != text
+        return revlog.revlog.cmp(self, node, text)
+
 
 # store original patch.patchfile.__init__
 _patchfile_init = patch.patchfile.__init__
 
+def _kwpatchfile_init(self, ui, fname, missing=False):
+    '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
+    rejects or conflicts due to expanded keywords in working dir.'''
+    _patchfile_init(self, ui, fname, missing=missing)
 
-def _iskwfile(f, link, kwt):
-    return not link(f) and kwt.matcher(f)
+    if _kwtemplater.matcher(self.fname):
+        # shrink keywords read from working dir
+        kwshrunk = _kwtemplater.shrink(''.join(self.lines))
+        self.lines = kwshrunk.splitlines(True)
+
+
+def _iskwfile(f, link):
+    return not link(f) and _kwtemplater.matcher(f)
 
 def _status(ui, repo, *pats, **opts):
     '''Bails out if [keyword] configuration is not active.
     Returns status of working directory.'''
-    if hasattr(repo, '_kwt'):
+    if _kwtemplater:
         files, match, anypats = cmdutil.matchpats(repo, pats, opts)
         return repo.status(files=files, match=match, list_clean=True)
     if ui.configitems('keyword'):
@@ -198,22 +245,22 @@
     '''Overwrites selected files expanding/shrinking keywords.'''
     ctx = repo.changectx(node)
     mf = ctx.manifest()
-    if node is not None:
-        # commit
+    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
+    else:                  # kwexpand/kwshrink
         notify = ui.note
-    candidates = [f for f in files if _iskwfile(f, mf.linkf, repo._kwt)]
+    candidates = [f for f in files if _iskwfile(f, mf.linkf)]
     if candidates:
         candidates.sort()
         action = expand and 'expanding' or 'shrinking'
         for f in candidates:
-            data, kwfound = repo._wreadkwct(f, expand, ctx, node)
+            fp = repo.file(f, kwmatch=True)
+            data, kwfound = fp.kwctread(mf[f], expand)
             if kwfound:
                 notify(_('overwriting %s %s keywords\n') % (f, action))
-                repo.wwrite(f, data, mf.flags(f), overwrite=True)
+                repo.wwrite(f, data, mf.flags(f))
                 repo.dirstate.normal(f)
 
 def _kwfwrite(ui, repo, expand, *pats, **opts):
@@ -230,35 +277,6 @@
     finally:
         del wlock, lock
 
-def cat(ui, repo, file1, *pats, **opts):
-    '''output the current or given revision of files expanding keywords
-
-    Print the specified files as they were at the given revision.
-    If no revision is given, the parent of the working directory is used,
-    or tip if no revision is checked out.
-
-    Output may be to a file, in which case the name of the file is
-    given using a format string.  The formatting rules are the same as
-    for the export command, with the following additions:
-
-    %s   basename of file being printed
-    %d   dirname of file being printed, or '.' if in repo root
-    %p   root-relative path name of file being printed
-    '''
-    ctx = repo.changectx(opts['rev'])
-    err = 1
-    for src, abs, rel, exact in cmdutil.walk(repo, (file1,) + pats, opts,
-                                             ctx.node()):
-        fp = cmdutil.make_file(repo, opts['output'], ctx.node(), pathname=abs)
-        data = ctx.filectx(abs).data()
-        try:
-            if repo._kwt.matcher(abs):
-                data = repo._kwt.expand(abs, data, ctx)
-        except AttributeError:
-            pass
-        fp.write(data)
-        err = 0
-    return err
 
 def demo(ui, repo, *args, **opts):
     '''print [keywordmaps] configuration and an expansion example
@@ -336,7 +354,7 @@
     repo.commit(text=msg)
     format = ui.verbose and ' in %s' % path or ''
     demostatus('%s keywords expanded%s' % (kwstatus, format))
-    ui.write(repo.wopener(fn).read())
+    ui.write(repo.wread(fn))
     ui.debug(_('\nremoving temporary repo %s\n') % tmpdir)
     shutil.rmtree(tmpdir, ignore_errors=True)
 
@@ -363,7 +381,7 @@
     if opts.get('untracked'):
         files += unknown
     files.sort()
-    kwfiles = [f for f in files if _iskwfile(f, repo._link, repo._kwt)]
+    kwfiles = [f for f in files if _iskwfile(f, repo._link)]
     cwd = pats and repo.getcwd() or ''
     kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
     if opts.get('all') or opts.get('ignore'):
@@ -386,10 +404,31 @@
 
 
 def reposetup(ui, repo):
-    if not repo.local() or repo.root.endswith('/.hg/patches'):
+    '''Sets up repo as kwrepo for keyword substitution.
+    Overrides file method to return kwfilelog instead of filelog
+    if file matches user configuration.
+    Wraps commit to overwrite configured files with updated
+    keyword substitutions.
+    This is done for local repos only, and only if there are
+    files configured at all for keyword substitution.'''
+
+    if not repo.local():
         return
 
-    inc, exc = [], ['.hgtags', '.hg_archival.txt']
+    hgcmd, func, args, opts, cmdopts = dispatch._parse(ui, sys.argv[1:])
+    if hgcmd in nokwcommands.split():
+        return
+
+    if hgcmd == 'diff':
+        # only expand if comparing against working dir
+        node1, node2 = cmdutil.revpair(repo, cmdopts.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'
+
+    inc, exc = [], ['.hgtags']
     for pat, opt in ui.configitems('keyword'):
         if opt != 'ignore':
             inc.append(pat)
@@ -398,29 +437,24 @@
     if not inc:
         return
 
+    global _kwtemplater
+    _restricted = hgcmd in restricted.split()
+    _kwtemplater = kwtemplater(ui, repo, inc, exc, _restricted)
+
     class kwrepo(repo.__class__):
-        def _wreadkwct(self, filename, expand, ctx, node):
-            '''Reads filename and returns tuple of data with keywords
-            expanded/shrunk and count of keywords (for _overwrite).'''
-            data = super(kwrepo, self).wread(filename)
-            return self._kwt.process(filename, data, expand, ctx, node)
+        def file(self, f, kwmatch=False):
+            if f[0] == '/':
+                f = f[1:]
+            if kwmatch or _kwtemplater.matcher(f):
+                return kwfilelog(self.sopener, f)
+            return filelog.filelog(self.sopener, f)
 
         def wread(self, filename):
             data = super(kwrepo, self).wread(filename)
-            if self._kwt.matcher(filename):
-                return self._kwt.shrink(data)
+            if _restricted and _kwtemplater.matcher(filename):
+                return _kwtemplater.shrink(data)
             return data
 
-        def wwrite(self, filename, data, flags, overwrite=False):
-            if not overwrite and self._kwt.matcher(filename):
-                data = self._kwt.expand(filename, data, None)
-            super(kwrepo, self).wwrite(filename, data, flags)
-
-        def wwritedata(self, filename, data):
-            if self._kwt.matcher(filename):
-                data = self._kwt.expand(filename, data, None)
-            return super(kwrepo, self).wwritedata(filename, data)
-
         def commit(self, files=None, text='', user=None, date=None,
                    match=util.always, force=False, force_editor=False,
                    p1=None, p2=None, extra={}, empty_ok=False):
@@ -464,50 +498,11 @@
             finally:
                 del wlock, lock
 
-    kwt = kwrepo._kwt = kwtemplater(ui, repo, inc, exc)
-
-    def kwpatchfile_init(self, ui, fname, missing=False):
-        '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
-        rejects or conflicts due to expanded keywords in working dir.'''
-        _patchfile_init(self, ui, fname, missing=missing)
-
-        if kwt.matcher(self.fname):
-            # shrink keywords read from working dir
-            kwshrunk = kwt.shrink(''.join(self.lines))
-            self.lines = kwshrunk.splitlines(True)
-
-    def kwweb_rawfile(web, req, tmpl):
-        '''Monkeypatch webcommands.rawfile so it expands keywords.'''
-        path = web.cleanpath(req.form.get('file', [''])[0])
-        if not path:
-            content = web.manifest(tmpl, web.changectx(req), path)
-            req.respond(webcommands.HTTP_OK, web.ctype)
-            return content
-        try:
-            fctx = web.filectx(req)
-        except revlog.LookupError:
-            content = web.manifest(tmpl, web.changectx(req), path)
-            req.respond(webcommands.HTTP_OK, web.ctype)
-            return content
-        path = fctx.path()
-        text = fctx.data()
-        if kwt.matcher(path):
-            text = kwt.expand(path, text, web.changectx(req))
-        mt = mimetypes.guess_type(path)[0]
-        if mt is None or util.binary(text):
-            mt = mt or 'application/octet-stream'
-        req.respond(webcommands.HTTP_OK, mt, path, len(text))
-        return [text]
-
     repo.__class__ = kwrepo
-    patch.patchfile.__init__ = kwpatchfile_init
-    webcommands.rawfile = kwweb_rawfile
+    patch.patchfile.__init__ = _kwpatchfile_init
 
 
 cmdtable = {
-    'kwcat':
-        (cat, commands.table['cat'][1],
-         _('hg kwcat [OPTION]... FILE...')),
     'kwdemo':
         (demo,
          [('d', 'default', None, _('show default keyword template maps')),