[PATCH 1 of 2] keyword: refactor overriding wread, wwrite, wwritedata repo methods

Christian Ebert blacktrash at gmx.net
Wed Feb 6 02:47:57 CST 2008


# HG changeset patch
# User Christian Ebert <blacktrash at gmx.net>
# Date 1202286573 -3600
# Node ID 99b48075b401b531f5c3490d4d27aee6921b7cd5
# Parent  b7f44f01a632ab4a59f276de21dc3c5d8f1b8560
keyword: refactor overriding wread, wwrite, wwritedata repo methods

- restrict consistently to reading/writing in working dir
- filelog not touched at all
- no cludgy sys.argv parsing anymore
- extra kwcat command to output files with keywords expanded
  (falls back on "hg cat" if no filenames are configured)

Should ease collaboration with other extensions, or external tools
like TortoiseHg.

Changes in behaviour:
- "hg cat" does not expand; use new "hg kwcat"
- web: expansion only in raw file display and in downloaded
  archives
  (expansion in filerevision will perhaps be added, cludgy
  though, as relying on monkeypatching hgweb.filerevision ->
  conflict with highlight extension)

Thanks to Jesse Glick for inciting this approach.

Further internal change:
- kwtemplater kwrepo attribute instead of global variable

diff --git a/hgext/keyword.py b/hgext/keyword.py
--- a/hgext/keyword.py
+++ b/hgext/keyword.py
@@ -78,28 +78,18 @@
 "Log = {desc}" expands to the first line of the changeset description.
 '''
 
-from mercurial import commands, cmdutil, context, dispatch, filelog, revlog
-from mercurial import patch, localrepo, templater, templatefilters, util
+from mercurial import commands, cmdutil, context, localrepo
+from mercurial import patch, revlog, templater, templatefilters, util
 from mercurial.node import *
+from mercurial.hgweb import webcommands
 from mercurial.i18n import _
-import re, shutil, sys, tempfile, time
+import mimetypes, re, shutil, 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):
     '''
@@ -116,13 +106,11 @@
         'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
     }
 
-    def __init__(self, ui, repo, inc, exc, restricted):
+    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.restricted = restricted
-        self.commitnode = None
-        self.path = ''
+        self.ctx = None
 
         kwmaps = self.ui.configitems('keywordmaps')
         if kwmaps: # override default templates
@@ -137,102 +125,67 @@
         self.ct = cmdutil.changeset_templater(self.ui, self.repo,
                                               False, '', False)
 
-    def substitute(self, node, data, subfunc):
-        '''Obtains file's changenode if commit node not given,
+    def substitute(self, path, data, node, subfunc):
+        '''Obtains file's changenode if 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()
+        if node is None:
+            # kwrepo.wwrite except when overwriting on commit
+            if self.ctx is None:
+                self.ctx = self.repo.changectx()
+            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)
 
         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)
             ekw = templatefilters.firstline(self.ui.popbuffer())
             return '$%s: %s $' % (kw, ekw)
 
         return subfunc(kwsub, data)
 
-    def expand(self, node, data):
+    def expand(self, path, data, node):
         '''Returns data with keywords expanded.'''
-        if self.restricted or util.binary(data):
+        if util.binary(data):
             return data
-        return self.substitute(node, data, self.re_kw.sub)
+        return self.substitute(path, data, node, self.re_kw.sub)
 
-    def process(self, node, data, expand):
+    def process(self, path, data, expand, ctx, node):
         '''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)
+            self.ctx = ctx
+            return self.substitute(path, data, node, self.re_kw.subn)
+        return self.re_kw.subn(r'$\1$', data)
 
-    def shrink(self, text):
+    def shrink(self, data):
         '''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 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)
-
+        if util.binary(data):
+            return data
+        return self.re_kw.sub(r'$\1$', data)
 
 # 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)
 
-    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 _iskwfile(f, link, kwt):
+    return not link(f) and kwt.matcher(f)
 
 def _status(ui, repo, *pats, **opts):
     '''Bails out if [keyword] configuration is not active.
     Returns status of working directory.'''
-    if _kwtemplater:
+    if hasattr(repo, '_kwt'):
         files, match, anypats = cmdutil.matchpats(repo, pats, opts)
         return repo.status(files=files, match=match, list_clean=True)
     if ui.configitems('keyword'):
@@ -243,22 +196,22 @@
     '''Overwrites selected files expanding/shrinking keywords.'''
     ctx = repo.changectx(node)
     mf = ctx.manifest()
-    if node is not None:   # commit
-        _kwtemplater.commitnode = node
+    if node is not None:
+        # commit
         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)]
+    candidates = [f for f in files if _iskwfile(f, mf.linkf, repo._kwt)]
     if candidates:
         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)
+            data, kwfound = repo._wreadkwct(f, expand, ctx, node)
             if kwfound:
                 notify(_('overwriting %s %s keywords\n') % (f, action))
-                repo.wwrite(f, data, mf.flags(f))
+                repo.wwrite(f, data, mf.flags(f), overwrite=True)
                 repo.dirstate.normal(f)
 
 def _kwfwrite(ui, repo, expand, *pats, **opts):
@@ -275,6 +228,37 @@
     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'])
+    try:
+        repo._kwt.ctx = ctx
+        kw = True
+    except AttributeError:
+        kw = False
+    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()
+        if kw and repo._kwt.matcher(abs):
+            data = repo._kwt.expand(abs, data, None)
+        fp.write(data)
+        err = 0
+    return err
 
 def demo(ui, repo, *args, **opts):
     '''print [keywordmaps] configuration and an expansion example
@@ -352,7 +336,7 @@
     repo.commit(text=msg)
     format = ui.verbose and ' in %s' % path or ''
     demostatus('%s keywords expanded%s' % (kwstatus, format))
-    ui.write(repo.wread(fn))
+    ui.write(repo.wopener(fn).read())
     ui.debug(_('\nremoving temporary repo %s\n') % tmpdir)
     shutil.rmtree(tmpdir, ignore_errors=True)
 
@@ -379,7 +363,7 @@
     if opts.get('untracked'):
         files += unknown
     files.sort()
-    kwfiles = [f for f in files if _iskwfile(f, repo._link)]
+    kwfiles = [f for f in files if _iskwfile(f, repo._link, repo._kwt)]
     cwd = pats and repo.getcwd() or ''
     kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
     if opts.get('all') or opts.get('ignore'):
@@ -402,31 +386,10 @@
 
 
 def reposetup(ui, repo):
-    '''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():
+    if not repo.local() or repo.root.endswith('/.hg/patches'):
         return
 
-    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']
+    inc, exc = [], ['.hgtags', '.hg_archival.txt']
     for pat, opt in ui.configitems('keyword'):
         if opt != 'ignore':
             inc.append(pat)
@@ -435,23 +398,28 @@
     if not inc:
         return
 
-    global _kwtemplater
-    _restricted = hgcmd in restricted.split()
-    _kwtemplater = kwtemplater(ui, repo, inc, exc, _restricted)
-
     class kwrepo(repo.__class__):
-        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 _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 wread(self, filename):
             data = super(kwrepo, self).wread(filename)
-            if _restricted and _kwtemplater.matcher(filename):
-                return _kwtemplater.shrink(data)
+            if self._kwt.matcher(filename):
+                return self._kwt.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,
@@ -496,11 +464,50 @@
             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, fctx.node())
+        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
+    patch.patchfile.__init__ = kwpatchfile_init
+    webcommands.rawfile = kwweb_rawfile
 
 
 cmdtable = {
+    'kwcat':
+        (cat, commands.table['cat'][1],
+         _('hg kwcat [OPTION]... FILE...')),
     'kwdemo':
         (demo,
          [('d', 'default', None, _('show default keyword template maps')),


More information about the Mercurial-devel mailing list