diff -r ce5eb725d0c6 -r 0683aaf458d8 hgkw/keyword.py --- 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')),