keyword.py
886 lines
| 29.4 KiB
| text/x-python
|
PythonLexer
/ hgext / keyword.py
|
r5815 | # keyword.py - $Keyword$ expansion for Mercurial | ||
# | ||||
|
r23723 | # Copyright 2007-2015 Christian Ebert <blacktrash@gmx.net> | ||
|
r5815 | # | ||
|
r8225 | # This software may be used and distributed according to the terms of the | ||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r5815 | # | ||
# $Id$ | ||||
# | ||||
|
r17424 | # Keyword expansion hack against the grain of a Distributed SCM | ||
|
r5815 | # | ||
# 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 | ||||
|
r7993 | # files (like LaTeX packages), that are mostly addressed to an | ||
# audience not running a version control system. | ||||
|
r5815 | # | ||
# For in-depth discussion refer to | ||||
|
r26421 | # <https://mercurial-scm.org/wiki/KeywordPlan>. | ||
|
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. | ||||
# | ||||
|
r29969 | # Run 'hg help keyword' and 'hg kwdemo' to get info on configuration. | ||
|
r5815 | |||
|
r8894 | '''expand keywords in tracked files | ||
|
r5815 | |||
|
r9264 | This extension expands RCS/CVS-like or self-customized $Keywords$ in | ||
tracked text files selected by your configuration. | ||||
|
r5815 | |||
|
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. | ||||
|
r5815 | |||
|
r12203 | Keywords expand to the changeset data pertaining to the latest change | ||
relative to the working directory parent of each file. | ||||
|
r11214 | Configuration is done in the [keyword], [keywordset] and [keywordmaps] | ||
sections of hgrc files. | ||||
|
r5815 | |||
|
r9157 | Example:: | ||
|
r5815 | |||
[keyword] | ||||
# expand keywords in every python file except those matching "x*" | ||||
**.py = | ||||
x* = ignore | ||||
|
r11214 | [keywordset] | ||
# prefer svn- over cvs-like default keywordmaps | ||||
svn = True | ||||
|
r12390 | .. note:: | ||
|
r19997 | |||
|
r12390 | The more specific you are in your filename patterns the less you | ||
lose speed in huge repositories. | ||||
|
r5815 | |||
|
r9264 | For [keywordmaps] template mapping and expansion demonstration and | ||
|
r10973 | control run :hg:`kwdemo`. See :hg:`help templates` for a list of | ||
|
r9307 | available templates and filters. | ||
|
r5815 | |||
|
r13885 | Three additional date template filters are provided: | ||
|
r11213 | |||
|
r13885 | :``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)" | ||||
|
r5815 | |||
|
r10973 | The default template mappings (view with :hg:`kwdemo -d`) can be | ||
replaced with customized keywords and templates. Again, run | ||||
|
r13025 | :hg:`kwdemo` to control the results of your configuration changes. | ||
|
r5815 | |||
|
r13270 | Before changing/disabling active keywords, you must run :hg:`kwshrink` | ||
to avoid storing expanded keywords in the change history. | ||||
|
r5815 | |||
|
r9264 | To force expansion after enabling it, or a configuration change, run | ||
|
r10973 | :hg:`kwexpand`. | ||
|
r5815 | |||
|
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. | ||||
|
r5815 | ''' | ||
|
r28321 | |||
from __future__ import absolute_import | ||||
import os | ||||
import re | ||||
|
r33067 | import weakref | ||
|
r28321 | |||
|
r29205 | from mercurial.i18n import _ | ||
|
r6072 | from mercurial.hgweb import webcommands | ||
|
r28321 | |||
from mercurial import ( | ||||
cmdutil, | ||||
context, | ||||
dispatch, | ||||
error, | ||||
extensions, | ||||
filelog, | ||||
localrepo, | ||||
|
r35906 | logcmdutil, | ||
|
r28321 | match, | ||
patch, | ||||
pathutil, | ||||
|
r35002 | pycompat, | ||
|
r28694 | registrar, | ||
|
r28321 | scmutil, | ||
templatefilters, | ||||
|
r37245 | templateutil, | ||
|
r28321 | util, | ||
) | ||||
|
r37102 | from mercurial.utils import ( | ||
dateutil, | ||||
stringutil, | ||||
) | ||||
|
r5815 | |||
|
r14300 | cmdtable = {} | ||
|
r32337 | command = registrar.command(cmdtable) | ||
|
r29841 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||
|
r25186 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||
# be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | ||||
|
r43347 | testedwith = b'ships-with-hg-core' | ||
|
r14300 | |||
|
r6024 | # hg commands that do not act on keywords | ||
|
r43346 | nokwcommands = ( | ||
|
r43347 | b'add addremove annotate bundle export grep incoming init log' | ||
b' outgoing push tip verify convert email glog' | ||||
|
r43346 | ) | ||
|
r6024 | |||
|
r33065 | # webcommands that do not act on keywords | ||
|
r43347 | nokwwebcommands = b'annotate changeset rev filediff diff comparison' | ||
|
r33065 | |||
|
r5961 | # hg commands that trigger expansion only when writing to working dir, | ||
# not when reading filelog, and unexpand when reading from working dir | ||||
|
r43346 | restricted = ( | ||
|
r43347 | b'merge kwexpand kwshrink record qrecord resolve transplant' | ||
b' unshelve rebase graft backout histedit fetch' | ||||
|
r43346 | ) | ||
|
r5961 | |||
|
r11168 | # names of extensions using dorecord | ||
|
r43347 | recordextensions = b'record' | ||
|
r11045 | |||
|
r13078 | colortable = { | ||
|
r43347 | b'kwfiles.enabled': b'green bold', | ||
b'kwfiles.deleted': b'cyan bold underline', | ||||
b'kwfiles.enabledunknown': b'green', | ||||
b'kwfiles.ignored': b'bold', | ||||
b'kwfiles.ignoredunknown': b'none', | ||||
|
r13078 | } | ||
|
r28694 | templatefilter = registrar.templatefilter() | ||
|
r34501 | configtable = {} | ||
configitem = registrar.configitem(configtable) | ||||
|
r43346 | configitem( | ||
|
r43347 | b'keywordset', b'svn', default=False, | ||
|
r34501 | ) | ||
|
r11213 | # date like in cvs' $Date | ||
|
r43347 | @templatefilter(b'utcdate', intype=templateutil.date) | ||
|
r37245 | def utcdate(date): | ||
|
r28694 | '''Date. Returns a UTC-date in this format: "2009/08/18 11:00:13". | ||
|
r13633 | ''' | ||
|
r43347 | dateformat = b'%Y/%m/%d %H:%M:%S' | ||
|
r37245 | return dateutil.datestr((date[0], 0), dateformat) | ||
|
r43346 | |||
|
r11213 | # date like in svn's $Date | ||
|
r43347 | @templatefilter(b'svnisodate', intype=templateutil.date) | ||
|
r37245 | def svnisodate(date): | ||
|
r28694 | '''Date. Returns a date in this format: "2009-08-18 13:00:13 | ||
|
r13633 | +0200 (Tue, 18 Aug 2009)". | ||
''' | ||||
|
r43347 | return dateutil.datestr(date, b'%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)') | ||
|
r43346 | |||
|
r11213 | # date like in svn's $Id | ||
|
r43347 | @templatefilter(b'svnutcdate', intype=templateutil.date) | ||
|
r37245 | def svnutcdate(date): | ||
|
r28694 | '''Date. Returns a UTC-date in this format: "2009-08-18 | ||
|
r13633 | 11:00:13Z". | ||
''' | ||||
|
r43347 | dateformat = b'%Y-%m-%d %H:%M:%SZ' | ||
|
r37245 | return dateutil.datestr((date[0], 0), dateformat) | ||
|
r5815 | |||
|
r43346 | |||
|
r6115 | # make keyword tools accessible | ||
|
r43347 | kwtools = {b'hgcmd': b''} | ||
|
r6114 | |||
|
r43346 | |||
|
r11214 | def _defaultkwmaps(ui): | ||
'''Returns default keywordmaps according to keywordset configuration.''' | ||||
|
r5815 | templates = { | ||
|
r43347 | b'Revision': b'{node|short}', | ||
b'Author': b'{author|user}', | ||||
|
r11214 | } | ||
|
r43346 | kwsets = ( | ||
{ | ||||
|
r43347 | b'Date': b'{date|utcdate}', | ||
b'RCSfile': b'{file|basename},v', | ||||
b'RCSFile': b'{file|basename},v', # kept for backwards compatibility | ||||
|
r43346 | # with hg-keyword | ||
|
r43347 | b'Source': b'{root}/{file},v', | ||
b'Id': b'{file|basename},v {node|short} {date|utcdate} {author|user}', | ||||
b'Header': b'{root}/{file},v {node|short} {date|utcdate} {author|user}', | ||||
|
r43346 | }, | ||
{ | ||||
|
r43347 | b'Date': b'{date|svnisodate}', | ||
b'Id': b'{file|basename},v {node|short} {date|svnutcdate} {author|user}', | ||||
b'LastChangedRevision': b'{node|short}', | ||||
b'LastChangedBy': b'{author|user}', | ||||
b'LastChangedDate': b'{date|svnisodate}', | ||||
|
r43346 | }, | ||
) | ||||
|
r43347 | templates.update(kwsets[ui.configbool(b'keywordset', b'svn')]) | ||
|
r11214 | return templates | ||
|
r43346 | |||
|
r12625 | def _shrinktext(text, subfunc): | ||
'''Helper for keyword expansion removal in text. | ||||
Depending on subfunc also returns number of substitutions.''' | ||||
|
r39462 | return subfunc(br'$\1$', text) | ||
|
r12625 | |||
|
r43346 | |||
|
r12723 | def _preselect(wstatus, changed): | ||
|
r17424 | '''Retrieves modified and added files from a working directory state | ||
|
r12723 | and returns the subset of each contained in given changed files | ||
retrieved from a change context.''' | ||||
|
r22918 | modified = [f for f in wstatus.modified if f in changed] | ||
added = [f for f in wstatus.added if f in changed] | ||||
|
r12723 | return modified, added | ||
|
r12625 | |||
|
r11214 | class kwtemplater(object): | ||
''' | ||||
Sets up keyword templates, corresponding keyword regex, and | ||||
provides keyword substitution functions. | ||||
''' | ||||
|
r5815 | |||
|
r11678 | def __init__(self, ui, repo, inc, exc): | ||
|
r5815 | self.ui = ui | ||
|
r33067 | self._repo = weakref.ref(repo) | ||
|
r43347 | self.match = match.match(repo.root, b'', [], inc, exc) | ||
self.restrict = kwtools[b'hgcmd'] in restricted.split() | ||||
|
r16809 | self.postcommit = False | ||
|
r5815 | |||
|
r43347 | kwmaps = self.ui.configitems(b'keywordmaps') | ||
|
r43346 | if kwmaps: # override default templates | ||
|
r24987 | self.templates = dict(kwmaps) | ||
|
r11214 | else: | ||
self.templates = _defaultkwmaps(self.ui) | ||||
|
r5815 | |||
|
r33067 | @property | ||
def repo(self): | ||||
return self._repo() | ||||
|
r12926 | @util.propertycache | ||
def escape(self): | ||||
'''Returns bar-separated and escaped keywords.''' | ||||
|
r43347 | return b'|'.join(map(stringutil.reescape, self.templates.keys())) | ||
|
r12926 | |||
@util.propertycache | ||||
def rekw(self): | ||||
'''Returns regex for unexpanded keywords.''' | ||||
|
r39462 | return re.compile(br'\$(%s)\$' % self.escape) | ||
|
r12926 | |||
@util.propertycache | ||||
def rekwexp(self): | ||||
'''Returns regex for expanded keywords.''' | ||||
|
r39462 | return re.compile(br'\$(%s): [^$\n\r]*? \$' % self.escape) | ||
|
r12926 | |||
|
r7375 | def substitute(self, data, path, ctx, subfunc): | ||
|
r6114 | '''Replaces keywords in data with expanded template.''' | ||
|
r43346 | |||
|
r5815 | def kwsub(mobj): | ||
kw = mobj.group(1) | ||||
|
r43346 | ct = logcmdutil.maketemplater( | ||
self.ui, self.repo, self.templates[kw] | ||||
) | ||||
|
r5815 | self.ui.pushbuffer() | ||
|
r10894 | ct.show(ctx, root=self.repo.root, file=path) | ||
|
r6023 | ekw = templatefilters.firstline(self.ui.popbuffer()) | ||
|
r43347 | return b'$%s: %s $' % (kw, ekw) | ||
|
r43346 | |||
|
r5815 | return subfunc(kwsub, data) | ||
|
r12920 | def linkctx(self, path, fileid): | ||
'''Similar to filelog.linkrev, but returns a changectx.''' | ||||
return self.repo.filectx(path, fileid=fileid).changectx() | ||||
|
r6114 | def expand(self, path, node, data): | ||
|
r5815 | '''Returns data with keywords expanded.''' | ||
|
r43346 | if ( | ||
not self.restrict | ||||
and self.match(path) | ||||
and not stringutil.binary(data) | ||||
): | ||||
|
r12920 | ctx = self.linkctx(path, node) | ||
|
r12926 | return self.substitute(data, path, ctx, self.rekw.sub) | ||
|
r6114 | return data | ||
|
r12627 | def iskwfile(self, cand, ctx): | ||
'''Returns subset of candidates which are configured for keyword | ||||
|
r15324 | expansion but are not symbolic links.''' | ||
|
r43347 | return [f for f in cand if self.match(f) and b'l' not in ctx.flags(f)] | ||
|
r5815 | |||
|
r12685 | def overwrite(self, ctx, candidates, lookup, expand, rekw=False): | ||
|
r6114 | '''Overwrites selected files expanding/shrinking keywords.''' | ||
|
r43346 | if self.restrict or lookup or self.postcommit: # exclude kw_copy | ||
|
r12627 | candidates = self.iskwfile(candidates, ctx) | ||
|
r12625 | if not candidates: | ||
return | ||||
|
r43346 | kwcmd = self.restrict and lookup # kwexpand/kwshrink | ||
|
r12625 | if self.restrict or expand and lookup: | ||
|
r11350 | mf = ctx.manifest() | ||
|
r15030 | if self.restrict or rekw: | ||
re_kw = self.rekw | ||||
else: | ||||
re_kw = self.rekwexp | ||||
if expand: | ||||
|
r43347 | msg = _(b'overwriting %s expanding keywords\n') | ||
|
r15030 | else: | ||
|
r43347 | msg = _(b'overwriting %s shrinking keywords\n') | ||
|
r12625 | for f in candidates: | ||
if self.restrict: | ||||
data = self.repo.file(f).read(mf[f]) | ||||
else: | ||||
data = self.repo.wread(f) | ||||
|
r37102 | if stringutil.binary(data): | ||
|
r12625 | continue | ||
if expand: | ||||
|
r23622 | parents = ctx.parents() | ||
|
r12625 | if lookup: | ||
|
r15083 | ctx = self.linkctx(f, mf[f]) | ||
|
r23622 | elif self.restrict and len(parents) > 1: | ||
# merge commit | ||||
# in case of conflict f is in modified state during | ||||
# merge, even if f does not differ from f in parent | ||||
for p in parents: | ||||
if f in p and not p[f].cmp(ctx[f]): | ||||
ctx = p[f].changectx() | ||||
break | ||||
|
r15083 | data, found = self.substitute(data, f, ctx, re_kw.subn) | ||
|
r12625 | elif self.restrict: | ||
|
r12926 | found = re_kw.search(data) | ||
|
r12625 | else: | ||
|
r12926 | data, found = _shrinktext(data, re_kw.subn) | ||
|
r12625 | if found: | ||
self.ui.note(msg % f) | ||||
|
r43347 | fp = self.repo.wvfs(f, b"wb", atomictemp=True) | ||
|
r15083 | fp.write(data) | ||
fp.close() | ||||
|
r12844 | if kwcmd: | ||
|
r12625 | self.repo.dirstate.normal(f) | ||
|
r16809 | elif self.postcommit: | ||
|
r12625 | self.repo.dirstate.normallookup(f) | ||
|
r6114 | |||
def shrink(self, fname, text): | ||||
|
r5815 | '''Returns text with all keyword substitutions removed.''' | ||
|
r37102 | if self.match(fname) and not stringutil.binary(text): | ||
|
r12926 | return _shrinktext(text, self.rekwexp.sub) | ||
|
r6114 | return text | ||
def shrinklines(self, fname, lines): | ||||
'''Returns lines with keyword substitutions removed.''' | ||||
|
r8638 | if self.match(fname): | ||
|
r43347 | text = b''.join(lines) | ||
|
r37102 | if not stringutil.binary(text): | ||
|
r12926 | return _shrinktext(text, self.rekwexp.sub).splitlines(True) | ||
|
r6114 | return lines | ||
def wread(self, fname, data): | ||||
'''If in restricted mode returns data read from wdir with | ||||
keyword substitutions removed.''' | ||||
|
r15030 | if self.restrict: | ||
return self.shrink(fname, data) | ||||
return data | ||||
|
r5815 | |||
|
r43346 | |||
|
r5815 | class kwfilelog(filelog.filelog): | ||
''' | ||||
Subclass of filelog to hook into its read, add, cmp methods. | ||||
Keywords are "stored" unexpanded, and processed on reading. | ||||
''' | ||||
|
r43346 | |||
|
r6503 | def __init__(self, opener, kwt, path): | ||
|
r5815 | super(kwfilelog, self).__init__(opener, path) | ||
|
r6503 | self.kwt = kwt | ||
|
r6114 | self.path = path | ||
|
r5815 | |||
def read(self, node): | ||||
'''Expands keywords when reading filelog.''' | ||||
data = super(kwfilelog, self).read(node) | ||||
|
r12628 | if self.renamed(node): | ||
return data | ||||
|
r6115 | return self.kwt.expand(self.path, node, data) | ||
|
r5815 | |||
def add(self, text, meta, tr, link, p1=None, p2=None): | ||||
'''Removes keyword substitutions when adding to filelog.''' | ||||
|
r6115 | text = self.kwt.shrink(self.path, text) | ||
|
r6504 | return super(kwfilelog, self).add(text, meta, tr, link, p1, p2) | ||
|
r5815 | |||
def cmp(self, node, text): | ||||
'''Removes keyword substitutions for comparison.''' | ||||
|
r6115 | text = self.kwt.shrink(self.path, text) | ||
|
r12628 | return super(kwfilelog, self).cmp(node, text) | ||
|
r5815 | |||
|
r43346 | |||
|
r14835 | def _status(ui, repo, wctx, kwt, *pats, **opts): | ||
|
r5815 | '''Bails out if [keyword] configuration is not active. | ||
Returns status of working directory.''' | ||||
|
r6115 | if kwt: | ||
|
r35002 | opts = pycompat.byteskwargs(opts) | ||
|
r43346 | return repo.status( | ||
match=scmutil.match(wctx, pats, opts), | ||||
clean=True, | ||||
|
r43347 | unknown=opts.get(b'unknown') or opts.get(b'all'), | ||
|
r43346 | ) | ||
|
r43347 | if ui.configitems(b'keyword'): | ||
raise error.Abort(_(b'[keyword] patterns cannot match')) | ||||
raise error.Abort(_(b'no [keyword] patterns configured')) | ||||
|
r5815 | |||
|
r43346 | |||
|
r5815 | def _kwfwrite(ui, repo, expand, *pats, **opts): | ||
|
r6114 | '''Selects files and passes them to kwtemplater.overwrite.''' | ||
|
r11320 | wctx = repo[None] | ||
if len(wctx.parents()) > 1: | ||||
|
r43347 | raise error.Abort(_(b'outstanding uncommitted merge')) | ||
|
r33070 | kwt = getattr(repo, '_keywordkwt', None) | ||
|
r27815 | with repo.wlock(): | ||
|
r14835 | status = _status(ui, repo, wctx, kwt, *pats, **opts) | ||
|
r22918 | if status.modified or status.added or status.removed or status.deleted: | ||
|
r43347 | raise error.Abort(_(b'outstanding uncommitted changes')) | ||
|
r22918 | kwt.overwrite(wctx, status.clean, True, expand) | ||
|
r5815 | |||
|
r43346 | |||
@command( | ||||
|
r43347 | b'kwdemo', | ||
|
r43346 | [ | ||
|
r43347 | (b'd', b'default', None, _(b'show default keyword template maps')), | ||
(b'f', b'rcfile', b'', _(b'read maps from rcfile'), _(b'FILE')), | ||||
|
r43346 | ], | ||
|
r43347 | _(b'hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'), | ||
|
r43346 | optionalrepo=True, | ||
) | ||||
|
r5815 | def demo(ui, repo, *args, **opts): | ||
'''print [keywordmaps] configuration and an expansion example | ||||
|
r7993 | Show current, custom, or default keyword template maps and their | ||
|
r8763 | expansions. | ||
|
r5815 | |||
|
r9281 | Extend the current configuration by specifying maps as arguments | ||
and using -f/--rcfile to source an external hgrc file. | ||||
|
r5815 | |||
|
r9281 | Use -d/--default to disable current configuration. | ||
|
r9307 | |||
|
r11193 | See :hg:`help templates` for information on templates and filters. | ||
|
r5815 | ''' | ||
|
r43346 | |||
|
r5815 | def demoitems(section, items): | ||
|
r43347 | ui.write(b'[%s]\n' % section) | ||
|
r9942 | for k, v in sorted(items): | ||
|
r40256 | if isinstance(v, bool): | ||
v = stringutil.pprint(v) | ||||
|
r43347 | ui.write(b'%s = %s\n' % (k, v)) | ||
|
r5815 | |||
|
r43347 | fn = b'demo.txt' | ||
tmpdir = pycompat.mkdtemp(b'', b'kwdemo.') | ||||
ui.note(_(b'creating temporary repository at %s\n') % tmpdir) | ||||
|
r29634 | if repo is None: | ||
baseui = ui | ||||
else: | ||||
baseui = repo.baseui | ||||
|
r39584 | repo = localrepo.instance(baseui, tmpdir, create=True) | ||
|
r43347 | ui.setconfig(b'keyword', fn, b'', b'keyword') | ||
svn = ui.configbool(b'keywordset', b'svn') | ||||
|
r13298 | # explicitly set keywordset for demo output | ||
|
r43347 | ui.setconfig(b'keywordset', b'svn', svn, b'keyword') | ||
|
r9281 | |||
|
r43347 | uikwmaps = ui.configitems(b'keywordmaps') | ||
|
r35002 | if args or opts.get(r'rcfile'): | ||
|
r43347 | ui.status(_(b'\n\tconfiguration using custom keyword template maps\n')) | ||
|
r9281 | if uikwmaps: | ||
|
r43347 | ui.status(_(b'\textending current template maps\n')) | ||
|
r35002 | if opts.get(r'default') or not uikwmaps: | ||
|
r13298 | if svn: | ||
|
r43347 | ui.status(_(b'\toverriding default svn keywordset\n')) | ||
|
r13298 | else: | ||
|
r43347 | ui.status(_(b'\toverriding default cvs keywordset\n')) | ||
|
r35002 | if opts.get(r'rcfile'): | ||
|
r43347 | ui.readconfig(opts.get(b'rcfile')) | ||
|
r9281 | if args: | ||
# simulate hgrc parsing | ||||
|
r43347 | rcmaps = b'[keywordmaps]\n%s\n' % b'\n'.join(args) | ||
repo.vfs.write(b'hgrc', rcmaps) | ||||
ui.readconfig(repo.vfs.join(b'hgrc')) | ||||
kwmaps = dict(ui.configitems(b'keywordmaps')) | ||||
|
r35002 | elif opts.get(r'default'): | ||
|
r13298 | if svn: | ||
|
r43347 | ui.status(_(b'\n\tconfiguration using default svn keywordset\n')) | ||
|
r13298 | else: | ||
|
r43347 | ui.status(_(b'\n\tconfiguration using default cvs keywordset\n')) | ||
|
r11214 | kwmaps = _defaultkwmaps(ui) | ||
|
r9281 | if uikwmaps: | ||
|
r43347 | ui.status(_(b'\tdisabling current template maps\n')) | ||
|
r5946 | for k, v in kwmaps.iteritems(): | ||
|
r43347 | ui.setconfig(b'keywordmaps', k, v, b'keyword') | ||
|
r9281 | else: | ||
|
r43347 | ui.status(_(b'\n\tconfiguration using current keyword template maps\n')) | ||
|
r15030 | if uikwmaps: | ||
kwmaps = dict(uikwmaps) | ||||
else: | ||||
kwmaps = _defaultkwmaps(ui) | ||||
|
r9281 | |||
|
r6502 | uisetup(ui) | ||
|
r5815 | reposetup(ui, repo) | ||
|
r43350 | ui.writenoi18n(b'[extensions]\nkeyword =\n') | ||
|
r43347 | demoitems(b'keyword', ui.configitems(b'keyword')) | ||
demoitems(b'keywordset', ui.configitems(b'keywordset')) | ||||
demoitems(b'keywordmaps', kwmaps.iteritems()) | ||||
keywords = b'$' + b'$\n$'.join(sorted(kwmaps.keys())) + b'$\n' | ||||
|
r23879 | repo.wvfs.write(fn, keywords) | ||
|
r11303 | repo[None].add([fn]) | ||
|
r43347 | ui.note(_(b'\nkeywords written to %s:\n') % fn) | ||
|
r5815 | ui.note(keywords) | ||
|
r27816 | with repo.wlock(): | ||
|
r43347 | repo.dirstate.setbranch(b'demobranch') | ||
for name, cmd in ui.configitems(b'hooks'): | ||||
if name.split(b'.', 1)[0].find(b'commit') > -1: | ||||
repo.ui.setconfig(b'hooks', name, b'', b'keyword') | ||||
msg = _(b'hg keyword configuration and expansion example') | ||||
ui.note((b"hg ci -m '%s'\n" % msg)) | ||||
|
r5815 | repo.commit(text=msg) | ||
|
r43347 | ui.status(_(b'\n\tkeywords expanded\n')) | ||
|
r5815 | ui.write(repo.wread(fn)) | ||
|
r24905 | repo.wvfs.rmtree(repo.root) | ||
|
r5815 | |||
|
r43346 | |||
@command( | ||||
|
r43347 | b'kwexpand', | ||
|
r32375 | cmdutil.walkopts, | ||
|
r43347 | _(b'hg kwexpand [OPTION]... [FILE]...'), | ||
|
r43346 | inferrepo=True, | ||
) | ||||
|
r5815 | def expand(ui, repo, *pats, **opts): | ||
|
r8763 | '''expand keywords in the working directory | ||
|
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) | ||||
|
r43346 | |||
@command( | ||||
|
r43347 | b'kwfiles', | ||
|
r43346 | [ | ||
|
r43347 | (b'A', b'all', None, _(b'show keyword status flags of all files')), | ||
(b'i', b'ignore', None, _(b'show files excluded from expansion')), | ||||
(b'u', b'unknown', None, _(b'only show unknown (not tracked) files')), | ||||
|
r43346 | ] | ||
+ cmdutil.walkopts, | ||||
|
r43347 | _(b'hg kwfiles [OPTION]... [FILE]...'), | ||
|
r43346 | inferrepo=True, | ||
) | ||||
|
r5815 | def files(ui, repo, *pats, **opts): | ||
|
r8957 | '''show files configured for keyword expansion | ||
|
r8950 | |||
|
r9264 | List which files in the working directory are matched by the | ||
[keyword] configuration patterns. | ||||
|
r5815 | |||
|
r9264 | Useful to prevent inadvertent keyword expansion and to speed up | ||
execution by including only files that are actual candidates for | ||||
expansion. | ||||
|
r8950 | |||
|
r10973 | See :hg:`help keyword` on how to construct patterns both for | ||
|
r9264 | inclusion and exclusion of files. | ||
|
r8957 | |||
|
r9494 | With -A/--all and -v/--verbose the codes used to show the status | ||
|
r9264 | of files are:: | ||
|
r9195 | |||
K = keyword expansion candidate | ||||
|
r9491 | k = keyword expansion candidate (not tracked) | ||
|
r9195 | I = ignored | ||
|
r9491 | i = ignored (not tracked) | ||
|
r5815 | ''' | ||
|
r33070 | kwt = getattr(repo, '_keywordkwt', None) | ||
|
r14835 | wctx = repo[None] | ||
status = _status(ui, repo, wctx, kwt, *pats, **opts) | ||||
|
r24306 | if pats: | ||
cwd = repo.getcwd() | ||||
else: | ||||
|
r43347 | cwd = b'' | ||
|
r9493 | files = [] | ||
|
r35002 | opts = pycompat.byteskwargs(opts) | ||
|
r43347 | if not opts.get(b'unknown') or opts.get(b'all'): | ||
|
r22918 | files = sorted(status.modified + status.added + status.clean) | ||
|
r12627 | kwfiles = kwt.iskwfile(files, wctx) | ||
|
r22918 | kwdeleted = kwt.iskwfile(status.deleted, wctx) | ||
kwunknown = kwt.iskwfile(status.unknown, wctx) | ||||
|
r43347 | if not opts.get(b'ignore') or opts.get(b'all'): | ||
|
r13079 | showfiles = kwfiles, kwdeleted, kwunknown | ||
|
r9493 | else: | ||
|
r13079 | showfiles = [], [], [] | ||
|
r43347 | if opts.get(b'all') or opts.get(b'ignore'): | ||
|
r43346 | showfiles += ( | ||
[f for f in files if f not in kwfiles], | ||||
[f for f in status.unknown if f not in kwunknown], | ||||
) | ||||
|
r43347 | kwlabels = b'enabled deleted enabledunknown ignored ignoredunknown'.split() | ||
kwstates = zip(kwlabels, pycompat.bytestr(b'K!kIi'), showfiles) | ||||
fm = ui.formatter(b'kwfiles', opts) | ||||
fmt = b'%.0s%s\n' | ||||
if opts.get(b'all') or ui.verbose: | ||||
fmt = b'%s %s\n' | ||||
|
r17057 | for kwstate, char, filenames in kwstates: | ||
|
r43347 | label = b'kwfiles.' + kwstate | ||
|
r5815 | for f in filenames: | ||
|
r17057 | fm.startitem() | ||
|
r39405 | fm.data(kwstatus=char, path=f) | ||
fm.plain(fmt % (char, repo.pathto(f, cwd)), label=label) | ||||
|
r17057 | fm.end() | ||
|
r5815 | |||
|
r43346 | |||
@command( | ||||
|
r43347 | b'kwshrink', | ||
|
r32375 | cmdutil.walkopts, | ||
|
r43347 | _(b'hg kwshrink [OPTION]... [FILE]...'), | ||
|
r43346 | inferrepo=True, | ||
) | ||||
|
r5815 | def shrink(ui, repo, *pats, **opts): | ||
|
r8763 | '''revert expanded keywords in the working directory | ||
|
r5815 | |||
|
r13270 | Must be run before changing/disabling active keywords. | ||
|
r5815 | |||
kwshrink refuses to run if given files contain local changes. | ||||
''' | ||||
# 3rd argument sets expansion to False | ||||
_kwfwrite(ui, repo, False, *pats, **opts) | ||||
|
r43346 | |||
|
r33071 | # monkeypatches | ||
|
r43346 | |||
|
r33071 | def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None): | ||
'''Monkeypatch/wrap patch.patchfile.__init__ to avoid | ||||
rejects or conflicts due to expanded keywords in working dir.''' | ||||
orig(self, ui, gp, backend, store, eolmode) | ||||
kwt = getattr(getattr(backend, 'repo', None), '_keywordkwt', None) | ||||
if kwt: | ||||
# shrink keywords read from working dir | ||||
self.lines = kwt.shrinklines(self.fname, self.lines) | ||||
|
r43346 | |||
|
r33071 | def kwdiff(orig, repo, *args, **kwargs): | ||
'''Monkeypatch patch.diff to avoid expansion.''' | ||||
kwt = getattr(repo, '_keywordkwt', None) | ||||
if kwt: | ||||
restrict = kwt.restrict | ||||
kwt.restrict = True | ||||
try: | ||||
for chunk in orig(repo, *args, **kwargs): | ||||
yield chunk | ||||
finally: | ||||
if kwt: | ||||
kwt.restrict = restrict | ||||
|
r43346 | |||
|
r36903 | def kwweb_skip(orig, web): | ||
|
r33071 | '''Wraps webcommands.x turning off keyword expansion.''' | ||
kwt = getattr(web.repo, '_keywordkwt', None) | ||||
if kwt: | ||||
origmatch = kwt.match | ||||
kwt.match = util.never | ||||
try: | ||||
|
r36903 | for chunk in orig(web): | ||
|
r33071 | yield chunk | ||
finally: | ||||
if kwt: | ||||
kwt.match = origmatch | ||||
|
r43346 | |||
|
r34088 | def kw_amend(orig, ui, repo, old, extra, pats, opts): | ||
|
r33071 | '''Wraps cmdutil.amend expanding keywords after amend.''' | ||
kwt = getattr(repo, '_keywordkwt', None) | ||||
if kwt is None: | ||||
|
r34088 | return orig(ui, repo, old, extra, pats, opts) | ||
|
r33071 | with repo.wlock(): | ||
kwt.postcommit = True | ||||
|
r34088 | newid = orig(ui, repo, old, extra, pats, opts) | ||
|
r33071 | if newid != old.node(): | ||
ctx = repo[newid] | ||||
kwt.restrict = True | ||||
kwt.overwrite(ctx, ctx.files(), False, True) | ||||
kwt.restrict = False | ||||
return newid | ||||
|
r43346 | |||
|
r33071 | def kw_copy(orig, ui, repo, pats, opts, rename=False): | ||
'''Wraps cmdutil.copy so that copy/rename destinations do not | ||||
contain expanded keywords. | ||||
Note that the source of a regular file destination may also be a | ||||
symlink: | ||||
hg cp sym x -> x is symlink | ||||
cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords) | ||||
For the latter we have to follow the symlink to find out whether its | ||||
target is configured for expansion and we therefore must unexpand the | ||||
keywords in the destination.''' | ||||
kwt = getattr(repo, '_keywordkwt', None) | ||||
if kwt is None: | ||||
return orig(ui, repo, pats, opts, rename) | ||||
with repo.wlock(): | ||||
orig(ui, repo, pats, opts, rename) | ||||
|
r43347 | if opts.get(b'dry_run'): | ||
|
r33071 | return | ||
wctx = repo[None] | ||||
cwd = repo.getcwd() | ||||
def haskwsource(dest): | ||||
'''Returns true if dest is a regular file and configured for | ||||
expansion or a symlink which points to a file configured for | ||||
expansion. ''' | ||||
source = repo.dirstate.copied(dest) | ||||
|
r43347 | if b'l' in wctx.flags(source): | ||
|
r43346 | source = pathutil.canonpath( | ||
repo.root, cwd, os.path.realpath(source) | ||||
) | ||||
|
r33071 | return kwt.match(source) | ||
|
r43346 | candidates = [ | ||
f | ||||
for f in repo.dirstate.copies() | ||||
|
r43347 | if b'l' not in wctx.flags(f) and haskwsource(f) | ||
|
r43346 | ] | ||
|
r33071 | kwt.overwrite(wctx, candidates, False, False) | ||
|
r43346 | |||
|
r33071 | def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts): | ||
'''Wraps record.dorecord expanding keywords after recording.''' | ||||
kwt = getattr(repo, '_keywordkwt', None) | ||||
if kwt is None: | ||||
return orig(ui, repo, commitfunc, *pats, **opts) | ||||
with repo.wlock(): | ||||
# record returns 0 even when nothing has changed | ||||
# therefore compare nodes before and after | ||||
kwt.postcommit = True | ||||
|
r43347 | ctx = repo[b'.'] | ||
|
r33071 | wstatus = ctx.status() | ||
ret = orig(ui, repo, commitfunc, *pats, **opts) | ||||
|
r43347 | recctx = repo[b'.'] | ||
|
r33071 | if ctx != recctx: | ||
modified, added = _preselect(wstatus, recctx.files()) | ||||
kwt.restrict = False | ||||
kwt.overwrite(recctx, modified, False, True) | ||||
kwt.overwrite(recctx, added, False, True, True) | ||||
kwt.restrict = True | ||||
return ret | ||||
|
r43346 | |||
|
r33071 | def kwfilectx_cmp(orig, self, fctx): | ||
if fctx._customcmp: | ||||
return fctx.cmp(self) | ||||
kwt = getattr(self._repo, '_keywordkwt', None) | ||||
if kwt is None: | ||||
return orig(self, fctx) | ||||
# keyword affects data size, comparing wdir and filelog size does | ||||
# not make sense | ||||
|
r43346 | if ( | ||
fctx._filenode is None | ||||
and ( | ||||
self._repo._encodefilterpats | ||||
or kwt.match(fctx.path()) | ||||
|
r43347 | and b'l' not in fctx.flags() | ||
|
r43346 | or self.size() - 4 == fctx.size() | ||
) | ||||
or self.size() == fctx.size() | ||||
): | ||||
|
r33071 | return self._filelog.cmp(self._filenode, fctx.data()) | ||
return True | ||||
|
r5815 | |||
|
r43346 | |||
|
r6502 | def uisetup(ui): | ||
|
r33071 | ''' Monkeypatches dispatch._parse to retrieve user command. | ||
Overrides file method to return kwfilelog instead of filelog | ||||
if file matches user configuration. | ||||
Wraps commit to overwrite configured files with updated | ||||
keyword substitutions. | ||||
Monkeypatches patch and webcommands.''' | ||||
|
r6502 | |||
|
r11678 | def kwdispatch_parse(orig, ui, args): | ||
'''Monkeypatch dispatch._parse to obtain running hg command.''' | ||||
cmd, func, args, options, cmdoptions = orig(ui, args) | ||||
|
r43347 | kwtools[b'hgcmd'] = cmd | ||
|
r11678 | return cmd, func, args, options, cmdoptions | ||
|
r6502 | |||
|
r43347 | extensions.wrapfunction(dispatch, b'_parse', kwdispatch_parse) | ||
|
r6502 | |||
|
r43347 | extensions.wrapfunction(context.filectx, b'cmp', kwfilectx_cmp) | ||
extensions.wrapfunction(patch.patchfile, b'__init__', kwpatchfile_init) | ||||
extensions.wrapfunction(patch, b'diff', kwdiff) | ||||
extensions.wrapfunction(cmdutil, b'amend', kw_amend) | ||||
extensions.wrapfunction(cmdutil, b'copy', kw_copy) | ||||
extensions.wrapfunction(cmdutil, b'dorecord', kw_dorecord) | ||||
|
r33071 | for c in nokwwebcommands.split(): | ||
extensions.wrapfunction(webcommands, c, kwweb_skip) | ||||
|
r43346 | |||
|
r5815 | def reposetup(ui, repo): | ||
|
r33071 | '''Sets up repo as kwrepo for keyword substitution.''' | ||
|
r5815 | |||
|
r7853 | try: | ||
|
r43346 | if ( | ||
not repo.local() | ||||
|
r43347 | or kwtools[b'hgcmd'] in nokwcommands.split() | ||
or b'.hg' in util.splitpath(repo.root) | ||||
or repo._url.startswith(b'bundle:') | ||||
|
r43346 | ): | ||
|
r7853 | return | ||
except AttributeError: | ||||
pass | ||||
|
r5815 | |||
|
r43347 | inc, exc = [], [b'.hg*'] | ||
for pat, opt in ui.configitems(b'keyword'): | ||||
if opt != b'ignore': | ||||
|
r11678 | inc.append(pat) | ||
else: | ||||
exc.append(pat) | ||||
if not inc: | ||||
return | ||||
|
r33070 | kwt = kwtemplater(ui, repo, inc, exc) | ||
|
r5815 | |||
class kwrepo(repo.__class__): | ||||
|
r6114 | def file(self, f): | ||
|
r43347 | if f[0] == b'/': | ||
|
r5815 | f = f[1:] | ||
|
r23878 | return kwfilelog(self.svfs, kwt, f) | ||
|
r5815 | |||
|
r5884 | def wread(self, filename): | ||
data = super(kwrepo, self).wread(filename) | ||||
|
r6115 | return kwt.wread(filename, data) | ||
|
r5884 | |||
|
r9096 | def commit(self, *args, **opts): | ||
|
r8996 | # use custom commitctx for user commands | ||
# other extensions can still wrap repo.commitctx directly | ||||
|
r9096 | self.commitctx = self.kwcommitctx | ||
try: | ||||
|
r10495 | return super(kwrepo, self).commit(*args, **opts) | ||
|
r9096 | finally: | ||
del self.commitctx | ||||
|
r8996 | |||
|
r42839 | def kwcommitctx(self, ctx, error=False, origctx=None): | ||
n = super(kwrepo, self).commitctx(ctx, error, origctx) | ||||
|
r10604 | # no lock needed, only called from repo.commit() which already locks | ||
|
r16809 | if not kwt.postcommit: | ||
|
r12625 | restrict = kwt.restrict | ||
kwt.restrict = True | ||||
|
r43346 | kwt.overwrite( | ||
self[n], sorted(ctx.added() + ctx.modified()), False, True | ||||
) | ||||
|
r12625 | kwt.restrict = restrict | ||
|
r10604 | return n | ||
|
r5815 | |||
|
r15183 | def rollback(self, dryrun=False, force=False): | ||
|
r32935 | with self.wlock(): | ||
origrestrict = kwt.restrict | ||||
try: | ||||
if not dryrun: | ||||
|
r43347 | changed = self[b'.'].files() | ||
|
r32935 | ret = super(kwrepo, self).rollback(dryrun, force) | ||
if not dryrun: | ||||
|
r43347 | ctx = self[b'.'] | ||
|
r32935 | modified, added = _preselect(ctx.status(), changed) | ||
kwt.restrict = False | ||||
kwt.overwrite(ctx, modified, True, True) | ||||
kwt.overwrite(ctx, added, True, False) | ||||
return ret | ||||
finally: | ||||
kwt.restrict = origrestrict | ||||
|
r12498 | |||
|
r33067 | repo.__class__ = kwrepo | ||
repo._keywordkwt = kwt | ||||