keyword.py
576 lines
| 21.7 KiB
| text/x-python
|
PythonLexer
/ hgext / keyword.py
Christian Ebert
|
r5815 | # keyword.py - $Keyword$ expansion for Mercurial | ||
# | ||||
Christian Ebert
|
r10653 | # Copyright 2007-2010 Christian Ebert <blacktrash@gmx.net> | ||
Christian Ebert
|
r5815 | # | ||
Martin Geisler
|
r8225 | # This software may be used and distributed according to the terms of the | ||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
Christian Ebert
|
r5815 | # | ||
# $Id$ | ||||
# | ||||
# Keyword expansion hack against the grain of a DSCM | ||||
# | ||||
# There are many good reasons why this is not needed in a distributed | ||||
# SCM, still it may be useful in very small projects based on single | ||||
Martin Geisler
|
r7993 | # files (like LaTeX packages), that are mostly addressed to an | ||
# audience not running a version control system. | ||||
Christian Ebert
|
r5815 | # | ||
# For in-depth discussion refer to | ||||
Dirkjan Ochtman
|
r8936 | # <http://mercurial.selenic.com/wiki/KeywordPlan>. | ||
Christian Ebert
|
r5815 | # | ||
# Keyword expansion is based on Mercurial's changeset template mappings. | ||||
# | ||||
# Binary files are not touched. | ||||
# | ||||
# Files to act upon/ignore are specified in the [keyword] section. | ||||
# Customized keyword template mappings in the [keywordmaps] section. | ||||
# | ||||
# Run "hg help keyword" and "hg kwdemo" to get info on configuration. | ||||
Cédric Duval
|
r8894 | '''expand keywords in tracked files | ||
Christian Ebert
|
r5815 | |||
Martin Geisler
|
r9264 | This extension expands RCS/CVS-like or self-customized $Keywords$ in | ||
tracked text files selected by your configuration. | ||||
Christian Ebert
|
r5815 | |||
Martin Geisler
|
r9264 | Keywords are only expanded in local repositories and not stored in the | ||
change history. The mechanism can be regarded as a convenience for the | ||||
current user or for archive distribution. | ||||
Christian Ebert
|
r5815 | |||
Christian Ebert
|
r11214 | Configuration is done in the [keyword], [keywordset] and [keywordmaps] | ||
sections of hgrc files. | ||||
Christian Ebert
|
r5815 | |||
Martin Geisler
|
r9157 | Example:: | ||
Christian Ebert
|
r5815 | |||
[keyword] | ||||
# expand keywords in every python file except those matching "x*" | ||||
**.py = | ||||
x* = ignore | ||||
Christian Ebert
|
r11214 | [keywordset] | ||
# prefer svn- over cvs-like default keywordmaps | ||||
svn = True | ||||
Martin Geisler
|
r9264 | NOTE: the more specific you are in your filename patterns the less you | ||
lose speed in huge repositories. | ||||
Christian Ebert
|
r5815 | |||
Martin Geisler
|
r9264 | For [keywordmaps] template mapping and expansion demonstration and | ||
Martin Geisler
|
r10973 | control run :hg:`kwdemo`. See :hg:`help templates` for a list of | ||
Christian Ebert
|
r9307 | available templates and filters. | ||
Christian Ebert
|
r5815 | |||
Christian Ebert
|
r11213 | Three additional date template filters are provided:: | ||
utcdate "2006/09/18 15:13:13" | ||||
svnutcdate "2006-09-18 15:13:13Z" | ||||
svnisodate "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)" | ||||
Christian Ebert
|
r5815 | |||
Martin Geisler
|
r10973 | The default template mappings (view with :hg:`kwdemo -d`) can be | ||
replaced with customized keywords and templates. Again, run | ||||
:hg:`kwdemo` to control the results of your config changes. | ||||
Christian Ebert
|
r5815 | |||
Martin Geisler
|
r10973 | Before changing/disabling active keywords, run :hg:`kwshrink` to avoid | ||
Martin Geisler
|
r9264 | the risk of inadvertently storing expanded keywords in the change | ||
history. | ||||
Christian Ebert
|
r5815 | |||
Martin Geisler
|
r9264 | To force expansion after enabling it, or a configuration change, run | ||
Martin Geisler
|
r10973 | :hg:`kwexpand`. | ||
Christian Ebert
|
r5815 | |||
Martin Geisler
|
r9264 | Expansions spanning more than one line and incremental expansions, | ||
like CVS' $Log$, are not supported. A keyword template map "Log = | ||||
{desc}" expands to the first line of the changeset description. | ||||
Christian Ebert
|
r5815 | ''' | ||
Matt Mackall
|
r7216 | from mercurial import commands, cmdutil, dispatch, filelog, revlog, extensions | ||
Matt Mackall
|
r8566 | from mercurial import patch, localrepo, templater, templatefilters, util, match | ||
Christian Ebert
|
r6072 | from mercurial.hgweb import webcommands | ||
Christian Ebert
|
r5815 | from mercurial.i18n import _ | ||
Christian Ebert
|
r9308 | import re, shutil, tempfile | ||
Christian Ebert
|
r5815 | |||
commands.optionalrepo += ' kwdemo' | ||||
Christian Ebert
|
r6024 | # hg commands that do not act on keywords | ||
Christian Ebert
|
r6667 | nokwcommands = ('add addremove annotate bundle copy export grep incoming init' | ||
Christian Ebert
|
r6867 | ' log outgoing push rename rollback tip verify' | ||
Christian Ebert
|
r6082 | ' convert email glog') | ||
Christian Ebert
|
r6024 | |||
Christian Ebert
|
r5961 | # hg commands that trigger expansion only when writing to working dir, | ||
# not when reading filelog, and unexpand when reading from working dir | ||||
Christian Ebert
|
r11044 | restricted = 'merge record qrecord resolve transplant' | ||
Christian Ebert
|
r5961 | |||
Christian Ebert
|
r11045 | # commands using dorecord | ||
recordcommands = 'record qrecord' | ||||
Christian Ebert
|
r11168 | # names of extensions using dorecord | ||
recordextensions = 'record' | ||||
Christian Ebert
|
r11045 | |||
Christian Ebert
|
r11213 | # date like in cvs' $Date | ||
Christian Ebert
|
r10332 | utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S') | ||
Christian Ebert
|
r11213 | # date like in svn's $Date | ||
svnisodate = lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)') | ||||
# date like in svn's $Id | ||||
svnutcdate = lambda x: util.datestr((x[0], 0), '%Y-%m-%d %H:%M:%SZ') | ||||
Christian Ebert
|
r5815 | |||
Christian Ebert
|
r6115 | # make keyword tools accessible | ||
Christian Ebert
|
r6502 | kwtools = {'templater': None, 'hgcmd': '', 'inc': [], 'exc': ['.hg*']} | ||
Christian Ebert
|
r6114 | |||
Christian Ebert
|
r5815 | |||
Christian Ebert
|
r11214 | def _defaultkwmaps(ui): | ||
'''Returns default keywordmaps according to keywordset configuration.''' | ||||
Christian Ebert
|
r5815 | templates = { | ||
'Revision': '{node|short}', | ||||
'Author': '{author|user}', | ||||
Christian Ebert
|
r11214 | } | ||
kwsets = ({ | ||||
Christian Ebert
|
r5815 | 'Date': '{date|utcdate}', | ||
Christian Ebert
|
r9943 | 'RCSfile': '{file|basename},v', | ||
timeless
|
r9950 | 'RCSFile': '{file|basename},v', # kept for backwards compatibility | ||
# with hg-keyword | ||||
Christian Ebert
|
r5815 | 'Source': '{root}/{file},v', | ||
'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}', | ||||
'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}', | ||||
Christian Ebert
|
r11214 | }, { | ||
'Date': '{date|svnisodate}', | ||||
'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}', | ||||
'LastChangedRevision': '{node|short}', | ||||
'LastChangedBy': '{author|user}', | ||||
'LastChangedDate': '{date|svnisodate}', | ||||
}) | ||||
templates.update(kwsets[ui.configbool('keywordset', 'svn')]) | ||||
return templates | ||||
class kwtemplater(object): | ||||
''' | ||||
Sets up keyword templates, corresponding keyword regex, and | ||||
provides keyword substitution functions. | ||||
''' | ||||
Christian Ebert
|
r5815 | |||
Christian Ebert
|
r6502 | def __init__(self, ui, repo): | ||
Christian Ebert
|
r5815 | self.ui = ui | ||
self.repo = repo | ||||
Christian Ebert
|
r8638 | self.match = match.match(repo.root, '', [], | ||
kwtools['inc'], kwtools['exc']) | ||||
Christian Ebert
|
r6115 | self.restrict = kwtools['hgcmd'] in restricted.split() | ||
Christian Ebert
|
r11045 | self.record = kwtools['hgcmd'] in recordcommands.split() | ||
Christian Ebert
|
r5815 | |||
kwmaps = self.ui.configitems('keywordmaps') | ||||
if kwmaps: # override default templates | ||||
Christian Ebert
|
r9081 | self.templates = dict((k, templater.parsestring(v, False)) | ||
for k, v in kwmaps) | ||||
Christian Ebert
|
r11214 | else: | ||
self.templates = _defaultkwmaps(self.ui) | ||||
Christian Ebert
|
r5815 | escaped = map(re.escape, self.templates.keys()) | ||
kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped) | ||||
self.re_kw = re.compile(kwpat) | ||||
Matt Mackall
|
r5976 | templatefilters.filters['utcdate'] = utcdate | ||
Christian Ebert
|
r11213 | templatefilters.filters['svnisodate'] = svnisodate | ||
templatefilters.filters['svnutcdate'] = svnutcdate | ||||
Christian Ebert
|
r5815 | |||
Dirkjan Ochtman
|
r7375 | def substitute(self, data, path, ctx, subfunc): | ||
Christian Ebert
|
r6114 | '''Replaces keywords in data with expanded template.''' | ||
Christian Ebert
|
r5815 | def kwsub(mobj): | ||
kw = mobj.group(1) | ||||
Christian Ebert
|
r10894 | ct = cmdutil.changeset_templater(self.ui, self.repo, | ||
False, None, '', False) | ||||
ct.use_template(self.templates[kw]) | ||||
Christian Ebert
|
r5815 | self.ui.pushbuffer() | ||
Christian Ebert
|
r10894 | ct.show(ctx, root=self.repo.root, file=path) | ||
Christian Ebert
|
r6023 | ekw = templatefilters.firstline(self.ui.popbuffer()) | ||
return '$%s: %s $' % (kw, ekw) | ||||
Christian Ebert
|
r5815 | return subfunc(kwsub, data) | ||
Christian Ebert
|
r6114 | def expand(self, path, node, data): | ||
Christian Ebert
|
r5815 | '''Returns data with keywords expanded.''' | ||
Christian Ebert
|
r8638 | if not self.restrict and self.match(path) and not util.binary(data): | ||
Dirkjan Ochtman
|
r7375 | ctx = self.repo.filectx(path, fileid=node).changectx() | ||
return self.substitute(data, path, ctx, self.re_kw.sub) | ||||
Christian Ebert
|
r6114 | return data | ||
Matt Mackall
|
r6749 | def iskwfile(self, path, flagfunc): | ||
Christian Ebert
|
r6114 | '''Returns true if path matches [keyword] pattern | ||
and is not a symbolic link. | ||||
Caveat: localrepository._link fails on Windows.''' | ||||
Christian Ebert
|
r8638 | return self.match(path) and not 'l' in flagfunc(path) | ||
Christian Ebert
|
r5815 | |||
Christian Ebert
|
r11320 | def overwrite(self, ctx, candidates, iswctx, expand): | ||
Christian Ebert
|
r6114 | '''Overwrites selected files expanding/shrinking keywords.''' | ||
Christian Ebert
|
r11253 | if self.record: | ||
Christian Ebert
|
r11350 | candidates = [f for f in ctx.files() if f in ctx] | ||
Christian Ebert
|
r10712 | candidates = [f for f in candidates if self.iskwfile(f, ctx.flags)] | ||
Christian Ebert
|
r6114 | if candidates: | ||
Christian Ebert
|
r11320 | self.restrict = True # do not expand when reading | ||
Christian Ebert
|
r11350 | mf = ctx.manifest() | ||
Christian Ebert
|
r8098 | msg = (expand and _('overwriting %s expanding keywords\n') | ||
or _('overwriting %s shrinking keywords\n')) | ||||
Christian Ebert
|
r6114 | for f in candidates: | ||
Christian Ebert
|
r11087 | if not self.record: | ||
Christian Ebert
|
r11045 | data = self.repo.file(f).read(mf[f]) | ||
else: | ||||
data = self.repo.wread(f) | ||||
Bryan O'Sullivan
|
r6508 | if util.binary(data): | ||
Christian Ebert
|
r6114 | continue | ||
if expand: | ||||
Christian Ebert
|
r11320 | if iswctx: | ||
Dirkjan Ochtman
|
r7375 | ctx = self.repo.filectx(f, fileid=mf[f]).changectx() | ||
data, found = self.substitute(data, f, ctx, | ||||
Christian Ebert
|
r6114 | self.re_kw.subn) | ||
else: | ||||
found = self.re_kw.search(data) | ||||
if found: | ||||
Christian Ebert
|
r10711 | self.ui.note(msg % f) | ||
Christian Ebert
|
r6114 | self.repo.wwrite(f, data, mf.flags(f)) | ||
Christian Ebert
|
r11320 | if iswctx: | ||
Christian Ebert
|
r8996 | self.repo.dirstate.normal(f) | ||
Christian Ebert
|
r11252 | elif self.record: | ||
self.repo.dirstate.normallookup(f) | ||||
Christian Ebert
|
r6114 | self.restrict = False | ||
Christian Ebert
|
r5815 | |||
Christian Ebert
|
r6114 | def shrinktext(self, text): | ||
'''Unconditionally removes all keyword substitutions from text.''' | ||||
return self.re_kw.sub(r'$\1$', text) | ||||
def shrink(self, fname, text): | ||||
Christian Ebert
|
r5815 | '''Returns text with all keyword substitutions removed.''' | ||
Christian Ebert
|
r8638 | if self.match(fname) and not util.binary(text): | ||
Christian Ebert
|
r6114 | return self.shrinktext(text) | ||
return text | ||||
def shrinklines(self, fname, lines): | ||||
'''Returns lines with keyword substitutions removed.''' | ||||
Christian Ebert
|
r8638 | if self.match(fname): | ||
Christian Ebert
|
r6114 | text = ''.join(lines) | ||
Bryan O'Sullivan
|
r6508 | if not util.binary(text): | ||
Christian Ebert
|
r6114 | return self.shrinktext(text).splitlines(True) | ||
return lines | ||||
def wread(self, fname, data): | ||||
'''If in restricted mode returns data read from wdir with | ||||
keyword substitutions removed.''' | ||||
return self.restrict and self.shrink(fname, data) or data | ||||
Christian Ebert
|
r5815 | |||
class kwfilelog(filelog.filelog): | ||||
''' | ||||
Subclass of filelog to hook into its read, add, cmp methods. | ||||
Keywords are "stored" unexpanded, and processed on reading. | ||||
''' | ||||
Christian Ebert
|
r6503 | def __init__(self, opener, kwt, path): | ||
Christian Ebert
|
r5815 | super(kwfilelog, self).__init__(opener, path) | ||
Christian Ebert
|
r6503 | self.kwt = kwt | ||
Christian Ebert
|
r6114 | self.path = path | ||
Christian Ebert
|
r5815 | |||
def read(self, node): | ||||
'''Expands keywords when reading filelog.''' | ||||
data = super(kwfilelog, self).read(node) | ||||
Christian Ebert
|
r6115 | return self.kwt.expand(self.path, node, data) | ||
Christian Ebert
|
r5815 | |||
def add(self, text, meta, tr, link, p1=None, p2=None): | ||||
'''Removes keyword substitutions when adding to filelog.''' | ||||
Christian Ebert
|
r6115 | text = self.kwt.shrink(self.path, text) | ||
Christian Ebert
|
r6504 | return super(kwfilelog, self).add(text, meta, tr, link, p1, p2) | ||
Christian Ebert
|
r5815 | |||
def cmp(self, node, text): | ||||
'''Removes keyword substitutions for comparison.''' | ||||
Christian Ebert
|
r6115 | text = self.kwt.shrink(self.path, text) | ||
Christian Ebert
|
r5815 | if self.renamed(node): | ||
t2 = super(kwfilelog, self).read(node) | ||||
return t2 != text | ||||
return revlog.revlog.cmp(self, node, text) | ||||
Christian Ebert
|
r9491 | def _status(ui, repo, kwt, *pats, **opts): | ||
Christian Ebert
|
r5815 | '''Bails out if [keyword] configuration is not active. | ||
Returns status of working directory.''' | ||||
Christian Ebert
|
r6115 | if kwt: | ||
Christian Ebert
|
r9491 | return repo.status(match=cmdutil.match(repo, pats, opts), clean=True, | ||
Christian Ebert
|
r10652 | unknown=opts.get('unknown') or opts.get('all')) | ||
Christian Ebert
|
r5815 | if ui.configitems('keyword'): | ||
raise util.Abort(_('[keyword] patterns cannot match')) | ||||
raise util.Abort(_('no [keyword] patterns configured')) | ||||
def _kwfwrite(ui, repo, expand, *pats, **opts): | ||||
Christian Ebert
|
r6114 | '''Selects files and passes them to kwtemplater.overwrite.''' | ||
Christian Ebert
|
r11320 | wctx = repo[None] | ||
if len(wctx.parents()) > 1: | ||||
Christian Ebert
|
r6672 | raise util.Abort(_('outstanding uncommitted merge')) | ||
Christian Ebert
|
r6115 | kwt = kwtools['templater'] | ||
Christian Ebert
|
r10604 | wlock = repo.wlock() | ||
Christian Ebert
|
r5815 | try: | ||
Christian Ebert
|
r10604 | status = _status(ui, repo, kwt, *pats, **opts) | ||
modified, added, removed, deleted, unknown, ignored, clean = status | ||||
if modified or added or removed or deleted: | ||||
raise util.Abort(_('outstanding uncommitted changes')) | ||||
Christian Ebert
|
r11320 | kwt.overwrite(wctx, clean, True, expand) | ||
Christian Ebert
|
r5815 | finally: | ||
Christian Ebert
|
r10604 | wlock.release() | ||
Christian Ebert
|
r5815 | |||
def demo(ui, repo, *args, **opts): | ||||
'''print [keywordmaps] configuration and an expansion example | ||||
Martin Geisler
|
r7993 | Show current, custom, or default keyword template maps and their | ||
timeless
|
r8763 | expansions. | ||
Christian Ebert
|
r5815 | |||
Christian Ebert
|
r9281 | Extend the current configuration by specifying maps as arguments | ||
and using -f/--rcfile to source an external hgrc file. | ||||
Christian Ebert
|
r5815 | |||
Christian Ebert
|
r9281 | Use -d/--default to disable current configuration. | ||
Christian Ebert
|
r9307 | |||
Martin Geisler
|
r11193 | See :hg:`help templates` for information on templates and filters. | ||
Christian Ebert
|
r5815 | ''' | ||
def demoitems(section, items): | ||||
ui.write('[%s]\n' % section) | ||||
Martin Geisler
|
r9942 | for k, v in sorted(items): | ||
Christian Ebert
|
r5815 | ui.write('%s = %s\n' % (k, v)) | ||
fn = 'demo.txt' | ||||
tmpdir = tempfile.mkdtemp('', 'kwdemo.') | ||||
Martin Geisler
|
r8027 | ui.note(_('creating temporary repository at %s\n') % tmpdir) | ||
Christian Ebert
|
r6504 | repo = localrepo.localrepository(ui, tmpdir, True) | ||
Christian Ebert
|
r5815 | ui.setconfig('keyword', fn, '') | ||
Christian Ebert
|
r9281 | |||
uikwmaps = ui.configitems('keywordmaps') | ||||
Christian Ebert
|
r5815 | if args or opts.get('rcfile'): | ||
Christian Ebert
|
r9281 | ui.status(_('\n\tconfiguration using custom keyword template maps\n')) | ||
if uikwmaps: | ||||
ui.status(_('\textending current template maps\n')) | ||||
if opts.get('default') or not uikwmaps: | ||||
ui.status(_('\toverriding default template maps\n')) | ||||
if opts.get('rcfile'): | ||||
ui.readconfig(opts.get('rcfile')) | ||||
if args: | ||||
# simulate hgrc parsing | ||||
rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args] | ||||
fp = repo.opener('hgrc', 'w') | ||||
fp.writelines(rcmaps) | ||||
fp.close() | ||||
ui.readconfig(repo.join('hgrc')) | ||||
kwmaps = dict(ui.configitems('keywordmaps')) | ||||
elif opts.get('default'): | ||||
ui.status(_('\n\tconfiguration using default keyword template maps\n')) | ||||
Christian Ebert
|
r11214 | kwmaps = _defaultkwmaps(ui) | ||
Christian Ebert
|
r9281 | if uikwmaps: | ||
ui.status(_('\tdisabling current template maps\n')) | ||||
Christian Ebert
|
r5946 | for k, v in kwmaps.iteritems(): | ||
Christian Ebert
|
r5815 | ui.setconfig('keywordmaps', k, v) | ||
Christian Ebert
|
r9281 | else: | ||
ui.status(_('\n\tconfiguration using current keyword template maps\n')) | ||||
Christian Ebert
|
r11214 | kwmaps = dict(uikwmaps) or _defaultkwmaps(ui) | ||
Christian Ebert
|
r9281 | |||
Christian Ebert
|
r6502 | uisetup(ui) | ||
Christian Ebert
|
r5815 | reposetup(ui, repo) | ||
Christian Ebert
|
r10714 | ui.write('[extensions]\nkeyword =\n') | ||
Christian Ebert
|
r5815 | demoitems('keyword', ui.configitems('keyword')) | ||
Christian Ebert
|
r5946 | demoitems('keywordmaps', kwmaps.iteritems()) | ||
Martin Geisler
|
r9942 | keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n' | ||
Christian Ebert
|
r5815 | repo.wopener(fn, 'w').write(keywords) | ||
Dirkjan Ochtman
|
r11303 | repo[None].add([fn]) | ||
Christian Ebert
|
r10713 | ui.note(_('\nkeywords written to %s:\n') % fn) | ||
Christian Ebert
|
r5815 | ui.note(keywords) | ||
Christian Ebert
|
r10713 | repo.dirstate.setbranch('demobranch') | ||
Christian Ebert
|
r5815 | for name, cmd in ui.configitems('hooks'): | ||
if name.split('.', 1)[0].find('commit') > -1: | ||||
repo.ui.setconfig('hooks', name, '') | ||||
Christian Ebert
|
r10499 | msg = _('hg keyword configuration and expansion example') | ||
Christian Ebert
|
r10713 | ui.note("hg ci -m '%s'\n" % msg) | ||
Christian Ebert
|
r5815 | repo.commit(text=msg) | ||
Christian Ebert
|
r9281 | ui.status(_('\n\tkeywords expanded\n')) | ||
Christian Ebert
|
r5815 | ui.write(repo.wread(fn)) | ||
shutil.rmtree(tmpdir, ignore_errors=True) | ||||
def expand(ui, repo, *pats, **opts): | ||||
timeless
|
r8763 | '''expand keywords in the working directory | ||
Christian Ebert
|
r5815 | |||
Run after (re)enabling keyword expansion. | ||||
kwexpand refuses to run if given files contain local changes. | ||||
''' | ||||
# 3rd argument sets expansion to True | ||||
_kwfwrite(ui, repo, True, *pats, **opts) | ||||
def files(ui, repo, *pats, **opts): | ||||
Christian Ebert
|
r8957 | '''show files configured for keyword expansion | ||
Christian Ebert
|
r8950 | |||
Martin Geisler
|
r9264 | List which files in the working directory are matched by the | ||
[keyword] configuration patterns. | ||||
Christian Ebert
|
r5815 | |||
Martin Geisler
|
r9264 | Useful to prevent inadvertent keyword expansion and to speed up | ||
execution by including only files that are actual candidates for | ||||
expansion. | ||||
Christian Ebert
|
r8950 | |||
Martin Geisler
|
r10973 | See :hg:`help keyword` on how to construct patterns both for | ||
Martin Geisler
|
r9264 | inclusion and exclusion of files. | ||
Christian Ebert
|
r8957 | |||
Christian Ebert
|
r9494 | With -A/--all and -v/--verbose the codes used to show the status | ||
Martin Geisler
|
r9264 | of files are:: | ||
Christian Ebert
|
r9195 | |||
K = keyword expansion candidate | ||||
Christian Ebert
|
r9491 | k = keyword expansion candidate (not tracked) | ||
Christian Ebert
|
r9195 | I = ignored | ||
Christian Ebert
|
r9491 | i = ignored (not tracked) | ||
Christian Ebert
|
r5815 | ''' | ||
Christian Ebert
|
r6115 | kwt = kwtools['templater'] | ||
Christian Ebert
|
r9491 | status = _status(ui, repo, kwt, *pats, **opts) | ||
Christian Ebert
|
r9493 | cwd = pats and repo.getcwd() or '' | ||
Christian Ebert
|
r5815 | modified, added, removed, deleted, unknown, ignored, clean = status | ||
Christian Ebert
|
r9493 | files = [] | ||
Christian Ebert
|
r10652 | if not opts.get('unknown') or opts.get('all'): | ||
Christian Ebert
|
r9493 | files = sorted(modified + added + clean) | ||
Matt Mackall
|
r6747 | wctx = repo[None] | ||
Matt Mackall
|
r6749 | kwfiles = [f for f in files if kwt.iskwfile(f, wctx.flags)] | ||
Christian Ebert
|
r9491 | kwunknown = [f for f in unknown if kwt.iskwfile(f, wctx.flags)] | ||
Christian Ebert
|
r9493 | if not opts.get('ignore') or opts.get('all'): | ||
showfiles = kwfiles, kwunknown | ||||
else: | ||||
showfiles = [], [] | ||||
Christian Ebert
|
r5815 | if opts.get('all') or opts.get('ignore'): | ||
Christian Ebert
|
r9493 | showfiles += ([f for f in files if f not in kwfiles], | ||
[f for f in unknown if f not in kwunknown]) | ||||
for char, filenames in zip('KkIi', showfiles): | ||||
Christian Ebert
|
r7417 | fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n' | ||
Christian Ebert
|
r5815 | for f in filenames: | ||
Christian Ebert
|
r7417 | ui.write(fmt % repo.pathto(f, cwd)) | ||
Christian Ebert
|
r5815 | |||
def shrink(ui, repo, *pats, **opts): | ||||
timeless
|
r8763 | '''revert expanded keywords in the working directory | ||
Christian Ebert
|
r5815 | |||
Martin Geisler
|
r7993 | Run before changing/disabling active keywords or if you experience | ||
Martin Geisler
|
r10973 | problems with :hg:`import` or :hg:`merge`. | ||
Christian Ebert
|
r5815 | |||
kwshrink refuses to run if given files contain local changes. | ||||
''' | ||||
# 3rd argument sets expansion to False | ||||
_kwfwrite(ui, repo, False, *pats, **opts) | ||||
Christian Ebert
|
r6502 | def uisetup(ui): | ||
'''Collects [keyword] config in kwtools. | ||||
Monkeypatches dispatch._parse if needed.''' | ||||
for pat, opt in ui.configitems('keyword'): | ||||
if opt != 'ignore': | ||||
kwtools['inc'].append(pat) | ||||
else: | ||||
kwtools['exc'].append(pat) | ||||
if kwtools['inc']: | ||||
Matt Mackall
|
r7216 | def kwdispatch_parse(orig, ui, args): | ||
Christian Ebert
|
r6502 | '''Monkeypatch dispatch._parse to obtain running hg command.''' | ||
Matt Mackall
|
r7216 | cmd, func, args, options, cmdoptions = orig(ui, args) | ||
Christian Ebert
|
r6502 | kwtools['hgcmd'] = cmd | ||
return cmd, func, args, options, cmdoptions | ||||
Matt Mackall
|
r7216 | extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse) | ||
Christian Ebert
|
r6502 | |||
Christian Ebert
|
r5815 | 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. | ||||
Christian Ebert
|
r6503 | Monkeypatches patch and webcommands.''' | ||
Christian Ebert
|
r5815 | |||
Matt Mackall
|
r7853 | try: | ||
if (not repo.local() or not kwtools['inc'] | ||||
or kwtools['hgcmd'] in nokwcommands.split() | ||||
or '.hg' in util.splitpath(repo.root) | ||||
or repo._url.startswith('bundle:')): | ||||
return | ||||
except AttributeError: | ||||
pass | ||||
Christian Ebert
|
r5815 | |||
Christian Ebert
|
r6502 | kwtools['templater'] = kwt = kwtemplater(ui, repo) | ||
Christian Ebert
|
r5815 | |||
class kwrepo(repo.__class__): | ||||
Christian Ebert
|
r6114 | def file(self, f): | ||
Christian Ebert
|
r5815 | if f[0] == '/': | ||
f = f[1:] | ||||
Christian Ebert
|
r6503 | return kwfilelog(self.sopener, kwt, f) | ||
Christian Ebert
|
r5815 | |||
Christian Ebert
|
r5884 | def wread(self, filename): | ||
data = super(kwrepo, self).wread(filename) | ||||
Christian Ebert
|
r6115 | return kwt.wread(filename, data) | ||
Christian Ebert
|
r5884 | |||
Christian Ebert
|
r9096 | def commit(self, *args, **opts): | ||
Christian Ebert
|
r8996 | # use custom commitctx for user commands | ||
# other extensions can still wrap repo.commitctx directly | ||||
Christian Ebert
|
r9096 | self.commitctx = self.kwcommitctx | ||
try: | ||||
Christian Ebert
|
r10495 | return super(kwrepo, self).commit(*args, **opts) | ||
Christian Ebert
|
r9096 | finally: | ||
del self.commitctx | ||||
Christian Ebert
|
r8996 | |||
def kwcommitctx(self, ctx, error=False): | ||||
Christian Ebert
|
r10604 | n = super(kwrepo, self).commitctx(ctx, error) | ||
# no lock needed, only called from repo.commit() which already locks | ||||
Christian Ebert
|
r11045 | if not kwt.record: | ||
Christian Ebert
|
r11320 | kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()), | ||
False, True) | ||||
Christian Ebert
|
r10604 | return n | ||
Christian Ebert
|
r5815 | |||
Christian Ebert
|
r6503 | # monkeypatches | ||
Christian Ebert
|
r9013 | def kwpatchfile_init(orig, self, ui, fname, opener, | ||
Augie Fackler
|
r10967 | missing=False, eolmode=None): | ||
Christian Ebert
|
r6503 | '''Monkeypatch/wrap patch.patchfile.__init__ to avoid | ||
rejects or conflicts due to expanded keywords in working dir.''' | ||||
Augie Fackler
|
r10967 | orig(self, ui, fname, opener, missing, eolmode) | ||
Christian Ebert
|
r6503 | # shrink keywords read from working dir | ||
self.lines = kwt.shrinklines(self.fname, self.lines) | ||||
Dirkjan Ochtman
|
r7308 | def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None, | ||
opts=None): | ||||
Christian Ebert
|
r6503 | '''Monkeypatch patch.diff to avoid expansion except when | ||
comparing against working dir.''' | ||||
if node2 is not None: | ||||
Christian Ebert
|
r8638 | kwt.match = util.never | ||
Matt Mackall
|
r6747 | elif node1 is not None and node1 != repo['.'].node(): | ||
Christian Ebert
|
r6503 | kwt.restrict = True | ||
Dirkjan Ochtman
|
r7308 | return orig(repo, node1, node2, match, changes, opts) | ||
Christian Ebert
|
r6667 | |||
Matt Mackall
|
r7216 | def kwweb_skip(orig, web, req, tmpl): | ||
'''Wraps webcommands.x turning off keyword expansion.''' | ||||
Christian Ebert
|
r8638 | kwt.match = util.never | ||
Matt Mackall
|
r7216 | return orig(web, req, tmpl) | ||
Christian Ebert
|
r6503 | |||
Christian Ebert
|
r11045 | def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts): | ||
'''Wraps record.dorecord expanding keywords after recording.''' | ||||
wlock = repo.wlock() | ||||
try: | ||||
# record returns 0 even when nothing has changed | ||||
# therefore compare nodes before and after | ||||
ctx = repo['.'] | ||||
ret = orig(ui, repo, commitfunc, *pats, **opts) | ||||
Christian Ebert
|
r11320 | recordctx = repo['.'] | ||
if ctx != recordctx: | ||||
kwt.overwrite(recordctx, None, False, True) | ||||
Christian Ebert
|
r11045 | return ret | ||
finally: | ||||
wlock.release() | ||||
Christian Ebert
|
r5815 | repo.__class__ = kwrepo | ||
Christian Ebert
|
r6503 | |||
Matt Mackall
|
r7216 | extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init) | ||
Christian Ebert
|
r10116 | if not kwt.restrict: | ||
extensions.wrapfunction(patch, 'diff', kw_diff) | ||||
Matt Mackall
|
r7216 | for c in 'annotate changeset rev filediff diff'.split(): | ||
extensions.wrapfunction(webcommands, c, kwweb_skip) | ||||
Christian Ebert
|
r11168 | for name in recordextensions.split(): | ||
try: | ||||
record = extensions.find(name) | ||||
extensions.wrapfunction(record, 'dorecord', kw_dorecord) | ||||
except KeyError: | ||||
pass | ||||
Christian Ebert
|
r5815 | |||
cmdtable = { | ||||
'kwdemo': | ||||
(demo, | ||||
[('d', 'default', None, _('show default keyword template maps')), | ||||
FUJIWARA Katsunori
|
r11321 | ('f', 'rcfile', '', | ||
_('read maps from rcfile'), _('FILE'))], | ||||
Christian Ebert
|
r5815 | _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')), | ||
'kwexpand': (expand, commands.walkopts, | ||||
_('hg kwexpand [OPTION]... [FILE]...')), | ||||
'kwfiles': | ||||
(files, | ||||
Christian Ebert
|
r9494 | [('A', 'all', None, _('show keyword status flags of all files')), | ||
Christian Ebert
|
r5815 | ('i', 'ignore', None, _('show files excluded from expansion')), | ||
Christian Ebert
|
r9493 | ('u', 'unknown', None, _('only show unknown (not tracked) files')), | ||
Christian Ebert
|
r5815 | ] + commands.walkopts, | ||
_('hg kwfiles [OPTION]... [FILE]...')), | ||||
'kwshrink': (shrink, commands.walkopts, | ||||
_('hg kwshrink [OPTION]... [FILE]...')), | ||||
} | ||||