# HG changeset patch # User Christian Ebert # Date 1202319494 -3600 # Node ID 0ed26effe190139de046c9a5846885873773fbd7 # Parent 290d023e83064d6939314419298631ffaa6460fc# Parent a8983fed9a9e4455fa6fb31d0555c50e2022830d (0.9.2compat) discard wread/wwrite approach diff -r a8983fed9a9e -r 0ed26effe190 hgkw/keyword.py --- 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')), diff -r a8983fed9a9e -r 0ed26effe190 tests/test-keyword --- a/tests/test-keyword Tue Feb 05 17:23:24 2008 +0100 +++ b/tests/test-keyword Wed Feb 06 18:38:14 2008 +0100 @@ -58,8 +58,8 @@ hg --quiet identify echo % cat cat a b -echo % hg kwcat -hg kwcat a b +echo % hg cat +hg cat a b echo echo % diff a hooktest @@ -91,20 +91,16 @@ echo % compare changenodes in a c cat a c -echo % qinit -c -hg qinit -c echo % qimport hg qimport -r tip -n mqtest.diff -echo % qcommit -hg qcommit -mqtest echo % keywords should not be expanded in patch cat .hg/patches/mqtest.diff -#echo % qpop -#hg qpop -#echo % qpush -#hg qpush -#echo % cat -#cat c +echo % qpop +hg qpop +echo % qpush +hg qpush +echo % cat +cat c echo % qpop and move on hg qpop @@ -149,8 +145,8 @@ echo % cat cat a b -echo % hg kwcat -hg kwcat a b +echo % hg cat +hg cat a b echo '$Xinfo$' >> a cat <> log @@ -171,8 +167,8 @@ echo % cat cat a b -echo % hg kwcat -hg kwcat a b +echo % hg cat +hg cat a b echo % remove hg remove a @@ -238,10 +234,17 @@ echo % kwexpand nonexistent hg kwexpand nonexistent +echo % switch off expansion echo % kwshrink with unknown file u cp a u hg --verbose kwshrink echo % cat cat a b -echo % hg kwcat -hg kwcat a b +echo % hg cat +hg cat a b + +rm $HGRCPATH +echo % cat +cat a b +echo % hg cat +hg cat a b diff -r a8983fed9a9e -r 0ed26effe190 tests/test-keyword.out --- a/tests/test-keyword.out Tue Feb 05 17:23:24 2008 +0100 +++ b/tests/test-keyword.out Wed Feb 06 18:38:14 2008 +0100 @@ -34,7 +34,6 @@ cause rejects if the patch context contains an active keyword. In that case run "hg kwshrink", and then reimport. Or, better, use bundle/unbundle to share changes. - kwcat output the current or given revision of files expanding keywords kwdemo print [keywordmaps] configuration and an expansion example kwexpand expand keywords in working directory kwfiles print files currently configured for keyword expansion @@ -97,7 +96,7 @@ do not process $Id: xxx $ ignore $Id$ -% hg kwcat +% hg cat expand $Id: a,v 7f0665a496fd 1970/01/01 00:00:00 user $ do not process $Id: xxx $ @@ -126,9 +125,7 @@ xxx $ $Id: c,v 7fefeeacf359 1970/01/01 00:00:01 user $ tests for different changenodes -% qinit -c % qimport -% qcommit % keywords should not be expanded in patch # HG changeset patch # User User Name @@ -143,6 +140,14 @@ @@ -0,0 +1,2 @@ +$Id$ +tests for different changenodes +% qpop +Patch queue now empty +% qpush +applying mqtest.diff +Now at: mqtest.diff +% cat +$Id: c,v 7fefeeacf359 1970/01/01 00:00:01 user $ +tests for different changenodes % qpop and move on Patch queue now empty % copy @@ -194,7 +199,7 @@ do not process $Id: xxx $ ignore $Id$ -% hg kwcat +% hg cat expand $Id: a 7f0665a496fd Thu, 01 Jan 1970 00:00:00 +0000 user $ do not process $Id: xxx $ @@ -209,7 +214,7 @@ xxx $ $Xinfo: User Name : firstline $ ignore $Id$ -% hg kwcat +% hg cat expand $Id: a 576a35651b0a Thu, 01 Jan 1970 00:00:02 +0000 user $ do not process $Id: xxx $ @@ -265,6 +270,7 @@ $Xinfo$ % kwexpand nonexistent nonexistent: No such file or directory +% switch off expansion % kwshrink with unknown file u overwriting a shrinking keywords overwriting x/a shrinking keywords @@ -274,9 +280,21 @@ xxx $ $Xinfo$ ignore $Id$ -% hg kwcat +% hg cat expand $Id: a 576a35651b0a Thu, 01 Jan 1970 00:00:02 +0000 user $ do not process $Id: xxx $ $Xinfo: User Name : firstline $ ignore $Id$ +% cat +expand $Id$ +do not process $Id: +xxx $ +$Xinfo$ +ignore $Id$ +% hg cat +expand $Id$ +do not process $Id: +xxx $ +$Xinfo$ +ignore $Id$