--- a/hgkw/keyword.py Tue Feb 05 17:23:24 2008 +0100
+++ b/hgkw/keyword.py Wed Feb 06 18:38:14 2008 +0100
@@ -85,20 +85,46 @@
Or, better, use bundle/unbundle to share changes.
'''
-from mercurial import commands, cmdutil, context
-from mercurial import localrepo, revlog, templater, util
+from mercurial import commands, cmdutil, context, fancyopts
+from mercurial import filelog, localrepo, revlog, templater, util
from mercurial.node import *
from mercurial.i18n import gettext as _
-import mimetypes, os.path, re, shutil, tempfile, time
+import getopt, os.path, 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
+
+# backwards compatibility hacks
try:
# avoid spurious rejects if patchfile is available
from mercurial.patch import patchfile
_patchfile_init = 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)
except ImportError:
- _patchfile_init = None
-
-# backwards compatibility hacks
+ pass
try:
# templatefilters module introduced in 9f1e6ab76069
@@ -110,13 +136,24 @@
template_firstline = templater.firstline
try:
- # webcommands module introduced in 08887121a652
- from mercurial.hgweb import webcommands
- _webcommands = True
- kwweb_func = webcommands.rawfile
+ # cmdutil.parse moves to dispatch._parse in 18a9fbb5cd78
+ # also avoid name conflict with other dispatch package(s)
+ from mercurial.dispatch import _parse
except ImportError:
- from mercurial.hgweb import hgweb_mod
- _webcommands = False
+ try:
+ # commands.parse moves to cmdutil.parse in 0c61124ad877
+ _parse = cmdutil.parse
+ except AttributeError:
+ _parse = commands.parse
+
+def _wwrite(repo, f, data, mf):
+ '''Makes repo.wwrite backwards compatible.'''
+ # 656e06eebda7 removed file descriptor argument
+ # 67982d3ee76c added flags argument
+ try:
+ repo.wwrite(f, data, mf.flags(f))
+ except (AttributeError, TypeError):
+ repo.wwrite(f, data)
def _normal(repo, files):
'''Backwards compatible repo.dirstate.normal/update.'''
@@ -134,12 +171,64 @@
except AttributeError:
return f
+# commands.parse/cmdutil.parse returned nothing for
+# "hg diff --rev" before 88803a69b24a due to bug in fancyopts
+def _fancyopts(args, options, state):
+ '''Fixed fancyopts from a9b7e425674f.'''
+ namelist = []
+ shortlist = ''
+ argmap = {}
+ defmap = {}
-commands.optionalrepo += ' kwdemo'
+ for short, name, default, comment in options:
+ # convert opts to getopt format
+ oname = name
+ name = name.replace('-', '_')
+
+ argmap['-' + short] = argmap['--' + oname] = name
+ defmap[name] = default
+
+ # copy defaults to state
+ if isinstance(default, list):
+ state[name] = default[:]
+ elif callable(default):
+ print "whoa", name, default
+ state[name] = None
+ else:
+ state[name] = default
-def utcdate(date):
- '''Returns hgdate in cvs-like UTC format.'''
- return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
+ # does it take a parameter?
+ if not (default is None or default is True or default is False):
+ if short: short += ':'
+ if oname: oname += '='
+ if short:
+ shortlist += short
+ if name:
+ namelist.append(oname)
+
+ # parse arguments
+ opts, args = getopt.getopt(args, shortlist, namelist)
+
+ # transfer result to state
+ for opt, val in opts:
+ name = argmap[opt]
+ t = type(defmap[name])
+ if t is type(fancyopts):
+ state[name] = defmap[name](val)
+ elif t is type(1):
+ state[name] = int(val)
+ elif t is type(''):
+ state[name] = val
+ elif t is type([]):
+ state[name].append(val)
+ elif t is type(None) or t is type(False):
+ state[name] = True
+
+ # return unparsed args
+ return args
+
+fancyopts.fancyopts = _fancyopts
+
class kwtemplater(object):
'''
@@ -156,11 +245,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
@@ -184,62 +275,86 @@
return cmdutil.changeset_templater(self.ui, self.repo,
False, None, '', 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
- 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)
+ 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)
- ekw = template_firstline(self.ui.popbuffer())
- return '$%s: %s $' % (kw, ekw)
+ self.ct.show(changenode=fnode, root=self.repo.root, file=self.path)
+ return '$%s: %s $' % (kw, template_firstline(self.ui.popbuffer()))
return subfunc(kwsub, data)
- def expand(self, path, data, node):
+ def expand(self, node, data):
'''Returns data with keywords expanded.'''
- if util.binary(data):
+ if self.restricted or util.binary(data):
return data
- return self.substitute(path, data, node, 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, data):
+ def shrink(self, text):
'''Returns text with all keyword substitutions removed.'''
- if util.binary(data):
- return data
- return self.re_kw.sub(r'$\1$', data)
+ 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)
+
+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'):
@@ -250,23 +365,23 @@
'''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 not mf.linkf(f) and repo._kwt.matcher(f)]
+ 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:
- 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)
+ _wwrite(repo, f, data, mf)
overwritten.append(f)
_normal(repo, overwritten)
@@ -284,37 +399,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'])
- 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
@@ -392,7 +476,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)
@@ -419,9 +503,9 @@
if opts.get('untracked'):
files += unknown
files.sort()
- # use full def of repo._link for backwards compatibility
- kwfiles = [f for f in files if
- not os.path.islink(repo.wjoin(f)) and repo._kwt.matcher(f)]
+ # use the full definition of repo._link for backwards compatibility
+ kwfiles = [f for f in files if _kwtemplater.matcher(f)
+ and not os.path.islink(repo.wjoin(f))]
cwd = pats and repo.getcwd() or ''
kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
if opts.get('all') or opts.get('ignore'):
@@ -444,10 +528,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 = _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)
@@ -456,34 +561,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=None, overwrite=False):
- if not overwrite and self._kwt.matcher(filename):
- data = self._kwt.expand(filename, data, None)
- try:
- super(kwrepo, self).wwrite(filename, data, flags)
- except (AttributeError, TypeError):
- # 656e06eebda7 removed file descriptor argument
- # 67982d3ee76c added flags argument
- super(kwrepo, self).wwrite(filename, data)
-
- 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, text, user, date, match, force, lock, wlock,
force_editor, p1, p2, extra, empty_ok):
'''Private commit wrapper for backwards compatibility.'''
@@ -558,94 +653,14 @@
finally:
del _wlock, _lock
- kwt = kwrepo._kwt = kwtemplater(ui, repo, inc, exc)
-
- if _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 kwt.matcher(self.fname):
- # shrink keywords read from working dir
- kwshrunk = kwt.shrink(''.join(self.lines))
- self.lines = kwshrunk.splitlines(True)
-
- patchfile.__init__ = kwpatchfile_init
-
- if _webcommands:
- 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]
-
- webcommands.rawfile = kwweb_rawfile
-
- else:
- def kwweb_filerevision(self, fctx):
- '''Monkeypatch hgweb_mod.hgweb.filerevision so keywords are
- expanded in raw file output.'''
- f = fctx.path()
- text = fctx.data()
- fl = fctx.filelog()
- n = fctx.filenode()
- parity = hgweb_mod.paritygen(self.stripecount)
- mt = mimetypes.guess_type(f)[0]
- rawtext = text
- if kwt.matcher(f):
- rawtext = kwt.expand(f, text, fctx.node())
- if util.binary(text):
- mt = mt or 'application/octet-stream'
- text = "(binary:%s)" % mt
- mt = mt or 'text/plain'
- def lines():
- for l, t in enumerate(text.splitlines(1)):
- yield {"line": t,
- "linenumber": "% 6d" % (l + 1),
- "parity": parity.next()}
- yield self.t("filerevision",
- file=f,
- path=hgweb_mod._up(f),
- text=lines(),
- raw=rawtext,
- mimetype=mt,
- rev=fctx.rev(),
- node=hex(fctx.node()),
- author=fctx.user(),
- date=fctx.date(),
- desc=fctx.description(),
- parent=self.siblings(fctx.parents()),
- child=self.siblings(fctx.children()),
- rename=self.renamelink(fl, n),
- permissions=fctx.manifest().flags(f))
-
- hgweb_mod.hgweb.filerevision = kwweb_filerevision
-
repo.__class__ = kwrepo
+ try:
+ patchfile.__init__ = _kwpatchfile_init
+ except NameError:
+ pass
cmdtable = {
- 'kwcat':
- (cat, commands.table['cat'][1],
- _('hg kwcat [OPTION]... FILE...')),
'kwdemo':
(demo,
[('d', 'default', None, _('show default keyword template maps')),