##// END OF EJS Templates
keyword: use `update_file_p1` instead of `normallookup`...
marmoute -
r48536:b9086ec4 default
parent child Browse files
Show More
@@ -1,888 +1,889 b''
1 # keyword.py - $Keyword$ expansion for Mercurial
1 # keyword.py - $Keyword$ expansion for Mercurial
2 #
2 #
3 # Copyright 2007-2015 Christian Ebert <blacktrash@gmx.net>
3 # Copyright 2007-2015 Christian Ebert <blacktrash@gmx.net>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 #
7 #
8 # $Id$
8 # $Id$
9 #
9 #
10 # Keyword expansion hack against the grain of a Distributed SCM
10 # Keyword expansion hack against the grain of a Distributed SCM
11 #
11 #
12 # There are many good reasons why this is not needed in a distributed
12 # There are many good reasons why this is not needed in a distributed
13 # SCM, still it may be useful in very small projects based on single
13 # SCM, still it may be useful in very small projects based on single
14 # files (like LaTeX packages), that are mostly addressed to an
14 # files (like LaTeX packages), that are mostly addressed to an
15 # audience not running a version control system.
15 # audience not running a version control system.
16 #
16 #
17 # For in-depth discussion refer to
17 # For in-depth discussion refer to
18 # <https://mercurial-scm.org/wiki/KeywordPlan>.
18 # <https://mercurial-scm.org/wiki/KeywordPlan>.
19 #
19 #
20 # Keyword expansion is based on Mercurial's changeset template mappings.
20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 #
21 #
22 # Binary files are not touched.
22 # Binary files are not touched.
23 #
23 #
24 # Files to act upon/ignore are specified in the [keyword] section.
24 # Files to act upon/ignore are specified in the [keyword] section.
25 # Customized keyword template mappings in the [keywordmaps] section.
25 # Customized keyword template mappings in the [keywordmaps] section.
26 #
26 #
27 # Run 'hg help keyword' and 'hg kwdemo' to get info on configuration.
27 # Run 'hg help keyword' and 'hg kwdemo' to get info on configuration.
28
28
29 '''expand keywords in tracked files
29 '''expand keywords in tracked files
30
30
31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
32 tracked text files selected by your configuration.
32 tracked text files selected by your configuration.
33
33
34 Keywords are only expanded in local repositories and not stored in the
34 Keywords are only expanded in local repositories and not stored in the
35 change history. The mechanism can be regarded as a convenience for the
35 change history. The mechanism can be regarded as a convenience for the
36 current user or for archive distribution.
36 current user or for archive distribution.
37
37
38 Keywords expand to the changeset data pertaining to the latest change
38 Keywords expand to the changeset data pertaining to the latest change
39 relative to the working directory parent of each file.
39 relative to the working directory parent of each file.
40
40
41 Configuration is done in the [keyword], [keywordset] and [keywordmaps]
41 Configuration is done in the [keyword], [keywordset] and [keywordmaps]
42 sections of hgrc files.
42 sections of hgrc files.
43
43
44 Example::
44 Example::
45
45
46 [keyword]
46 [keyword]
47 # expand keywords in every python file except those matching "x*"
47 # expand keywords in every python file except those matching "x*"
48 **.py =
48 **.py =
49 x* = ignore
49 x* = ignore
50
50
51 [keywordset]
51 [keywordset]
52 # prefer svn- over cvs-like default keywordmaps
52 # prefer svn- over cvs-like default keywordmaps
53 svn = True
53 svn = True
54
54
55 .. note::
55 .. note::
56
56
57 The more specific you are in your filename patterns the less you
57 The more specific you are in your filename patterns the less you
58 lose speed in huge repositories.
58 lose speed in huge repositories.
59
59
60 For [keywordmaps] template mapping and expansion demonstration and
60 For [keywordmaps] template mapping and expansion demonstration and
61 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
61 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
62 available templates and filters.
62 available templates and filters.
63
63
64 Three additional date template filters are provided:
64 Three additional date template filters are provided:
65
65
66 :``utcdate``: "2006/09/18 15:13:13"
66 :``utcdate``: "2006/09/18 15:13:13"
67 :``svnutcdate``: "2006-09-18 15:13:13Z"
67 :``svnutcdate``: "2006-09-18 15:13:13Z"
68 :``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
68 :``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
69
69
70 The default template mappings (view with :hg:`kwdemo -d`) can be
70 The default template mappings (view with :hg:`kwdemo -d`) can be
71 replaced with customized keywords and templates. Again, run
71 replaced with customized keywords and templates. Again, run
72 :hg:`kwdemo` to control the results of your configuration changes.
72 :hg:`kwdemo` to control the results of your configuration changes.
73
73
74 Before changing/disabling active keywords, you must run :hg:`kwshrink`
74 Before changing/disabling active keywords, you must run :hg:`kwshrink`
75 to avoid storing expanded keywords in the change history.
75 to avoid storing expanded keywords in the change history.
76
76
77 To force expansion after enabling it, or a configuration change, run
77 To force expansion after enabling it, or a configuration change, run
78 :hg:`kwexpand`.
78 :hg:`kwexpand`.
79
79
80 Expansions spanning more than one line and incremental expansions,
80 Expansions spanning more than one line and incremental expansions,
81 like CVS' $Log$, are not supported. A keyword template map "Log =
81 like CVS' $Log$, are not supported. A keyword template map "Log =
82 {desc}" expands to the first line of the changeset description.
82 {desc}" expands to the first line of the changeset description.
83 '''
83 '''
84
84
85
85
86 from __future__ import absolute_import
86 from __future__ import absolute_import
87
87
88 import os
88 import os
89 import re
89 import re
90 import weakref
90 import weakref
91
91
92 from mercurial.i18n import _
92 from mercurial.i18n import _
93 from mercurial.pycompat import getattr
93 from mercurial.pycompat import getattr
94 from mercurial.hgweb import webcommands
94 from mercurial.hgweb import webcommands
95
95
96 from mercurial import (
96 from mercurial import (
97 cmdutil,
97 cmdutil,
98 context,
98 context,
99 dispatch,
99 dispatch,
100 error,
100 error,
101 extensions,
101 extensions,
102 filelog,
102 filelog,
103 localrepo,
103 localrepo,
104 logcmdutil,
104 logcmdutil,
105 match,
105 match,
106 patch,
106 patch,
107 pathutil,
107 pathutil,
108 pycompat,
108 pycompat,
109 registrar,
109 registrar,
110 scmutil,
110 scmutil,
111 templatefilters,
111 templatefilters,
112 templateutil,
112 templateutil,
113 util,
113 util,
114 )
114 )
115 from mercurial.utils import (
115 from mercurial.utils import (
116 dateutil,
116 dateutil,
117 stringutil,
117 stringutil,
118 )
118 )
119
119
120 cmdtable = {}
120 cmdtable = {}
121 command = registrar.command(cmdtable)
121 command = registrar.command(cmdtable)
122 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
122 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
123 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
123 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
124 # be specifying the version(s) of Mercurial they are tested with, or
124 # be specifying the version(s) of Mercurial they are tested with, or
125 # leave the attribute unspecified.
125 # leave the attribute unspecified.
126 testedwith = b'ships-with-hg-core'
126 testedwith = b'ships-with-hg-core'
127
127
128 # hg commands that do not act on keywords
128 # hg commands that do not act on keywords
129 nokwcommands = (
129 nokwcommands = (
130 b'add addremove annotate bundle export grep incoming init log'
130 b'add addremove annotate bundle export grep incoming init log'
131 b' outgoing push tip verify convert email glog'
131 b' outgoing push tip verify convert email glog'
132 )
132 )
133
133
134 # webcommands that do not act on keywords
134 # webcommands that do not act on keywords
135 nokwwebcommands = b'annotate changeset rev filediff diff comparison'
135 nokwwebcommands = b'annotate changeset rev filediff diff comparison'
136
136
137 # hg commands that trigger expansion only when writing to working dir,
137 # hg commands that trigger expansion only when writing to working dir,
138 # not when reading filelog, and unexpand when reading from working dir
138 # not when reading filelog, and unexpand when reading from working dir
139 restricted = (
139 restricted = (
140 b'merge kwexpand kwshrink record qrecord resolve transplant'
140 b'merge kwexpand kwshrink record qrecord resolve transplant'
141 b' unshelve rebase graft backout histedit fetch'
141 b' unshelve rebase graft backout histedit fetch'
142 )
142 )
143
143
144 # names of extensions using dorecord
144 # names of extensions using dorecord
145 recordextensions = b'record'
145 recordextensions = b'record'
146
146
147 colortable = {
147 colortable = {
148 b'kwfiles.enabled': b'green bold',
148 b'kwfiles.enabled': b'green bold',
149 b'kwfiles.deleted': b'cyan bold underline',
149 b'kwfiles.deleted': b'cyan bold underline',
150 b'kwfiles.enabledunknown': b'green',
150 b'kwfiles.enabledunknown': b'green',
151 b'kwfiles.ignored': b'bold',
151 b'kwfiles.ignored': b'bold',
152 b'kwfiles.ignoredunknown': b'none',
152 b'kwfiles.ignoredunknown': b'none',
153 }
153 }
154
154
155 templatefilter = registrar.templatefilter()
155 templatefilter = registrar.templatefilter()
156
156
157 configtable = {}
157 configtable = {}
158 configitem = registrar.configitem(configtable)
158 configitem = registrar.configitem(configtable)
159
159
160 configitem(
160 configitem(
161 b'keywordset',
161 b'keywordset',
162 b'svn',
162 b'svn',
163 default=False,
163 default=False,
164 )
164 )
165 # date like in cvs' $Date
165 # date like in cvs' $Date
166 @templatefilter(b'utcdate', intype=templateutil.date)
166 @templatefilter(b'utcdate', intype=templateutil.date)
167 def utcdate(date):
167 def utcdate(date):
168 """Date. Returns a UTC-date in this format: "2009/08/18 11:00:13"."""
168 """Date. Returns a UTC-date in this format: "2009/08/18 11:00:13"."""
169 dateformat = b'%Y/%m/%d %H:%M:%S'
169 dateformat = b'%Y/%m/%d %H:%M:%S'
170 return dateutil.datestr((date[0], 0), dateformat)
170 return dateutil.datestr((date[0], 0), dateformat)
171
171
172
172
173 # date like in svn's $Date
173 # date like in svn's $Date
174 @templatefilter(b'svnisodate', intype=templateutil.date)
174 @templatefilter(b'svnisodate', intype=templateutil.date)
175 def svnisodate(date):
175 def svnisodate(date):
176 """Date. Returns a date in this format: "2009-08-18 13:00:13
176 """Date. Returns a date in this format: "2009-08-18 13:00:13
177 +0200 (Tue, 18 Aug 2009)".
177 +0200 (Tue, 18 Aug 2009)".
178 """
178 """
179 return dateutil.datestr(date, b'%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
179 return dateutil.datestr(date, b'%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
180
180
181
181
182 # date like in svn's $Id
182 # date like in svn's $Id
183 @templatefilter(b'svnutcdate', intype=templateutil.date)
183 @templatefilter(b'svnutcdate', intype=templateutil.date)
184 def svnutcdate(date):
184 def svnutcdate(date):
185 """Date. Returns a UTC-date in this format: "2009-08-18
185 """Date. Returns a UTC-date in this format: "2009-08-18
186 11:00:13Z".
186 11:00:13Z".
187 """
187 """
188 dateformat = b'%Y-%m-%d %H:%M:%SZ'
188 dateformat = b'%Y-%m-%d %H:%M:%SZ'
189 return dateutil.datestr((date[0], 0), dateformat)
189 return dateutil.datestr((date[0], 0), dateformat)
190
190
191
191
192 # make keyword tools accessible
192 # make keyword tools accessible
193 kwtools = {b'hgcmd': b''}
193 kwtools = {b'hgcmd': b''}
194
194
195
195
196 def _defaultkwmaps(ui):
196 def _defaultkwmaps(ui):
197 '''Returns default keywordmaps according to keywordset configuration.'''
197 '''Returns default keywordmaps according to keywordset configuration.'''
198 templates = {
198 templates = {
199 b'Revision': b'{node|short}',
199 b'Revision': b'{node|short}',
200 b'Author': b'{author|user}',
200 b'Author': b'{author|user}',
201 }
201 }
202 kwsets = (
202 kwsets = (
203 {
203 {
204 b'Date': b'{date|utcdate}',
204 b'Date': b'{date|utcdate}',
205 b'RCSfile': b'{file|basename},v',
205 b'RCSfile': b'{file|basename},v',
206 b'RCSFile': b'{file|basename},v', # kept for backwards compatibility
206 b'RCSFile': b'{file|basename},v', # kept for backwards compatibility
207 # with hg-keyword
207 # with hg-keyword
208 b'Source': b'{root}/{file},v',
208 b'Source': b'{root}/{file},v',
209 b'Id': b'{file|basename},v {node|short} {date|utcdate} {author|user}',
209 b'Id': b'{file|basename},v {node|short} {date|utcdate} {author|user}',
210 b'Header': b'{root}/{file},v {node|short} {date|utcdate} {author|user}',
210 b'Header': b'{root}/{file},v {node|short} {date|utcdate} {author|user}',
211 },
211 },
212 {
212 {
213 b'Date': b'{date|svnisodate}',
213 b'Date': b'{date|svnisodate}',
214 b'Id': b'{file|basename},v {node|short} {date|svnutcdate} {author|user}',
214 b'Id': b'{file|basename},v {node|short} {date|svnutcdate} {author|user}',
215 b'LastChangedRevision': b'{node|short}',
215 b'LastChangedRevision': b'{node|short}',
216 b'LastChangedBy': b'{author|user}',
216 b'LastChangedBy': b'{author|user}',
217 b'LastChangedDate': b'{date|svnisodate}',
217 b'LastChangedDate': b'{date|svnisodate}',
218 },
218 },
219 )
219 )
220 templates.update(kwsets[ui.configbool(b'keywordset', b'svn')])
220 templates.update(kwsets[ui.configbool(b'keywordset', b'svn')])
221 return templates
221 return templates
222
222
223
223
224 def _shrinktext(text, subfunc):
224 def _shrinktext(text, subfunc):
225 """Helper for keyword expansion removal in text.
225 """Helper for keyword expansion removal in text.
226 Depending on subfunc also returns number of substitutions."""
226 Depending on subfunc also returns number of substitutions."""
227 return subfunc(br'$\1$', text)
227 return subfunc(br'$\1$', text)
228
228
229
229
230 def _preselect(wstatus, changed):
230 def _preselect(wstatus, changed):
231 """Retrieves modified and added files from a working directory state
231 """Retrieves modified and added files from a working directory state
232 and returns the subset of each contained in given changed files
232 and returns the subset of each contained in given changed files
233 retrieved from a change context."""
233 retrieved from a change context."""
234 modified = [f for f in wstatus.modified if f in changed]
234 modified = [f for f in wstatus.modified if f in changed]
235 added = [f for f in wstatus.added if f in changed]
235 added = [f for f in wstatus.added if f in changed]
236 return modified, added
236 return modified, added
237
237
238
238
239 class kwtemplater(object):
239 class kwtemplater(object):
240 """
240 """
241 Sets up keyword templates, corresponding keyword regex, and
241 Sets up keyword templates, corresponding keyword regex, and
242 provides keyword substitution functions.
242 provides keyword substitution functions.
243 """
243 """
244
244
245 def __init__(self, ui, repo, inc, exc):
245 def __init__(self, ui, repo, inc, exc):
246 self.ui = ui
246 self.ui = ui
247 self._repo = weakref.ref(repo)
247 self._repo = weakref.ref(repo)
248 self.match = match.match(repo.root, b'', [], inc, exc)
248 self.match = match.match(repo.root, b'', [], inc, exc)
249 self.restrict = kwtools[b'hgcmd'] in restricted.split()
249 self.restrict = kwtools[b'hgcmd'] in restricted.split()
250 self.postcommit = False
250 self.postcommit = False
251
251
252 kwmaps = self.ui.configitems(b'keywordmaps')
252 kwmaps = self.ui.configitems(b'keywordmaps')
253 if kwmaps: # override default templates
253 if kwmaps: # override default templates
254 self.templates = dict(kwmaps)
254 self.templates = dict(kwmaps)
255 else:
255 else:
256 self.templates = _defaultkwmaps(self.ui)
256 self.templates = _defaultkwmaps(self.ui)
257
257
258 @property
258 @property
259 def repo(self):
259 def repo(self):
260 return self._repo()
260 return self._repo()
261
261
262 @util.propertycache
262 @util.propertycache
263 def escape(self):
263 def escape(self):
264 '''Returns bar-separated and escaped keywords.'''
264 '''Returns bar-separated and escaped keywords.'''
265 return b'|'.join(map(stringutil.reescape, self.templates.keys()))
265 return b'|'.join(map(stringutil.reescape, self.templates.keys()))
266
266
267 @util.propertycache
267 @util.propertycache
268 def rekw(self):
268 def rekw(self):
269 '''Returns regex for unexpanded keywords.'''
269 '''Returns regex for unexpanded keywords.'''
270 return re.compile(br'\$(%s)\$' % self.escape)
270 return re.compile(br'\$(%s)\$' % self.escape)
271
271
272 @util.propertycache
272 @util.propertycache
273 def rekwexp(self):
273 def rekwexp(self):
274 '''Returns regex for expanded keywords.'''
274 '''Returns regex for expanded keywords.'''
275 return re.compile(br'\$(%s): [^$\n\r]*? \$' % self.escape)
275 return re.compile(br'\$(%s): [^$\n\r]*? \$' % self.escape)
276
276
277 def substitute(self, data, path, ctx, subfunc):
277 def substitute(self, data, path, ctx, subfunc):
278 '''Replaces keywords in data with expanded template.'''
278 '''Replaces keywords in data with expanded template.'''
279
279
280 def kwsub(mobj):
280 def kwsub(mobj):
281 kw = mobj.group(1)
281 kw = mobj.group(1)
282 ct = logcmdutil.maketemplater(
282 ct = logcmdutil.maketemplater(
283 self.ui, self.repo, self.templates[kw]
283 self.ui, self.repo, self.templates[kw]
284 )
284 )
285 self.ui.pushbuffer()
285 self.ui.pushbuffer()
286 ct.show(ctx, root=self.repo.root, file=path)
286 ct.show(ctx, root=self.repo.root, file=path)
287 ekw = templatefilters.firstline(self.ui.popbuffer())
287 ekw = templatefilters.firstline(self.ui.popbuffer())
288 return b'$%s: %s $' % (kw, ekw)
288 return b'$%s: %s $' % (kw, ekw)
289
289
290 return subfunc(kwsub, data)
290 return subfunc(kwsub, data)
291
291
292 def linkctx(self, path, fileid):
292 def linkctx(self, path, fileid):
293 '''Similar to filelog.linkrev, but returns a changectx.'''
293 '''Similar to filelog.linkrev, but returns a changectx.'''
294 return self.repo.filectx(path, fileid=fileid).changectx()
294 return self.repo.filectx(path, fileid=fileid).changectx()
295
295
296 def expand(self, path, node, data):
296 def expand(self, path, node, data):
297 '''Returns data with keywords expanded.'''
297 '''Returns data with keywords expanded.'''
298 if (
298 if (
299 not self.restrict
299 not self.restrict
300 and self.match(path)
300 and self.match(path)
301 and not stringutil.binary(data)
301 and not stringutil.binary(data)
302 ):
302 ):
303 ctx = self.linkctx(path, node)
303 ctx = self.linkctx(path, node)
304 return self.substitute(data, path, ctx, self.rekw.sub)
304 return self.substitute(data, path, ctx, self.rekw.sub)
305 return data
305 return data
306
306
307 def iskwfile(self, cand, ctx):
307 def iskwfile(self, cand, ctx):
308 """Returns subset of candidates which are configured for keyword
308 """Returns subset of candidates which are configured for keyword
309 expansion but are not symbolic links."""
309 expansion but are not symbolic links."""
310 return [f for f in cand if self.match(f) and b'l' not in ctx.flags(f)]
310 return [f for f in cand if self.match(f) and b'l' not in ctx.flags(f)]
311
311
312 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
312 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
313 '''Overwrites selected files expanding/shrinking keywords.'''
313 '''Overwrites selected files expanding/shrinking keywords.'''
314 if self.restrict or lookup or self.postcommit: # exclude kw_copy
314 if self.restrict or lookup or self.postcommit: # exclude kw_copy
315 candidates = self.iskwfile(candidates, ctx)
315 candidates = self.iskwfile(candidates, ctx)
316 if not candidates:
316 if not candidates:
317 return
317 return
318 kwcmd = self.restrict and lookup # kwexpand/kwshrink
318 kwcmd = self.restrict and lookup # kwexpand/kwshrink
319 if self.restrict or expand and lookup:
319 if self.restrict or expand and lookup:
320 mf = ctx.manifest()
320 mf = ctx.manifest()
321 if self.restrict or rekw:
321 if self.restrict or rekw:
322 re_kw = self.rekw
322 re_kw = self.rekw
323 else:
323 else:
324 re_kw = self.rekwexp
324 re_kw = self.rekwexp
325 if expand:
325 if expand:
326 msg = _(b'overwriting %s expanding keywords\n')
326 msg = _(b'overwriting %s expanding keywords\n')
327 else:
327 else:
328 msg = _(b'overwriting %s shrinking keywords\n')
328 msg = _(b'overwriting %s shrinking keywords\n')
329 for f in candidates:
329 for f in candidates:
330 if self.restrict:
330 if self.restrict:
331 data = self.repo.file(f).read(mf[f])
331 data = self.repo.file(f).read(mf[f])
332 else:
332 else:
333 data = self.repo.wread(f)
333 data = self.repo.wread(f)
334 if stringutil.binary(data):
334 if stringutil.binary(data):
335 continue
335 continue
336 if expand:
336 if expand:
337 parents = ctx.parents()
337 parents = ctx.parents()
338 if lookup:
338 if lookup:
339 ctx = self.linkctx(f, mf[f])
339 ctx = self.linkctx(f, mf[f])
340 elif self.restrict and len(parents) > 1:
340 elif self.restrict and len(parents) > 1:
341 # merge commit
341 # merge commit
342 # in case of conflict f is in modified state during
342 # in case of conflict f is in modified state during
343 # merge, even if f does not differ from f in parent
343 # merge, even if f does not differ from f in parent
344 for p in parents:
344 for p in parents:
345 if f in p and not p[f].cmp(ctx[f]):
345 if f in p and not p[f].cmp(ctx[f]):
346 ctx = p[f].changectx()
346 ctx = p[f].changectx()
347 break
347 break
348 data, found = self.substitute(data, f, ctx, re_kw.subn)
348 data, found = self.substitute(data, f, ctx, re_kw.subn)
349 elif self.restrict:
349 elif self.restrict:
350 found = re_kw.search(data)
350 found = re_kw.search(data)
351 else:
351 else:
352 data, found = _shrinktext(data, re_kw.subn)
352 data, found = _shrinktext(data, re_kw.subn)
353 if found:
353 if found:
354 self.ui.note(msg % f)
354 self.ui.note(msg % f)
355 fp = self.repo.wvfs(f, b"wb", atomictemp=True)
355 fp = self.repo.wvfs(f, b"wb", atomictemp=True)
356 fp.write(data)
356 fp.write(data)
357 fp.close()
357 fp.close()
358 if kwcmd:
358 if kwcmd:
359 self.repo.dirstate.set_clean(f)
359 self.repo.dirstate.set_clean(f)
360 elif self.postcommit:
360 elif self.postcommit:
361 self.repo.dirstate.normallookup(f)
361 self.repo.dirstate.update_file_p1(f, p1_tracked=True)
362
362
363 def shrink(self, fname, text):
363 def shrink(self, fname, text):
364 '''Returns text with all keyword substitutions removed.'''
364 '''Returns text with all keyword substitutions removed.'''
365 if self.match(fname) and not stringutil.binary(text):
365 if self.match(fname) and not stringutil.binary(text):
366 return _shrinktext(text, self.rekwexp.sub)
366 return _shrinktext(text, self.rekwexp.sub)
367 return text
367 return text
368
368
369 def shrinklines(self, fname, lines):
369 def shrinklines(self, fname, lines):
370 '''Returns lines with keyword substitutions removed.'''
370 '''Returns lines with keyword substitutions removed.'''
371 if self.match(fname):
371 if self.match(fname):
372 text = b''.join(lines)
372 text = b''.join(lines)
373 if not stringutil.binary(text):
373 if not stringutil.binary(text):
374 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
374 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
375 return lines
375 return lines
376
376
377 def wread(self, fname, data):
377 def wread(self, fname, data):
378 """If in restricted mode returns data read from wdir with
378 """If in restricted mode returns data read from wdir with
379 keyword substitutions removed."""
379 keyword substitutions removed."""
380 if self.restrict:
380 if self.restrict:
381 return self.shrink(fname, data)
381 return self.shrink(fname, data)
382 return data
382 return data
383
383
384
384
385 class kwfilelog(filelog.filelog):
385 class kwfilelog(filelog.filelog):
386 """
386 """
387 Subclass of filelog to hook into its read, add, cmp methods.
387 Subclass of filelog to hook into its read, add, cmp methods.
388 Keywords are "stored" unexpanded, and processed on reading.
388 Keywords are "stored" unexpanded, and processed on reading.
389 """
389 """
390
390
391 def __init__(self, opener, kwt, path):
391 def __init__(self, opener, kwt, path):
392 super(kwfilelog, self).__init__(opener, path)
392 super(kwfilelog, self).__init__(opener, path)
393 self.kwt = kwt
393 self.kwt = kwt
394 self.path = path
394 self.path = path
395
395
396 def read(self, node):
396 def read(self, node):
397 '''Expands keywords when reading filelog.'''
397 '''Expands keywords when reading filelog.'''
398 data = super(kwfilelog, self).read(node)
398 data = super(kwfilelog, self).read(node)
399 if self.renamed(node):
399 if self.renamed(node):
400 return data
400 return data
401 return self.kwt.expand(self.path, node, data)
401 return self.kwt.expand(self.path, node, data)
402
402
403 def add(self, text, meta, tr, link, p1=None, p2=None):
403 def add(self, text, meta, tr, link, p1=None, p2=None):
404 '''Removes keyword substitutions when adding to filelog.'''
404 '''Removes keyword substitutions when adding to filelog.'''
405 text = self.kwt.shrink(self.path, text)
405 text = self.kwt.shrink(self.path, text)
406 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
406 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
407
407
408 def cmp(self, node, text):
408 def cmp(self, node, text):
409 '''Removes keyword substitutions for comparison.'''
409 '''Removes keyword substitutions for comparison.'''
410 text = self.kwt.shrink(self.path, text)
410 text = self.kwt.shrink(self.path, text)
411 return super(kwfilelog, self).cmp(node, text)
411 return super(kwfilelog, self).cmp(node, text)
412
412
413
413
414 def _status(ui, repo, wctx, kwt, *pats, **opts):
414 def _status(ui, repo, wctx, kwt, *pats, **opts):
415 """Bails out if [keyword] configuration is not active.
415 """Bails out if [keyword] configuration is not active.
416 Returns status of working directory."""
416 Returns status of working directory."""
417 if kwt:
417 if kwt:
418 opts = pycompat.byteskwargs(opts)
418 opts = pycompat.byteskwargs(opts)
419 return repo.status(
419 return repo.status(
420 match=scmutil.match(wctx, pats, opts),
420 match=scmutil.match(wctx, pats, opts),
421 clean=True,
421 clean=True,
422 unknown=opts.get(b'unknown') or opts.get(b'all'),
422 unknown=opts.get(b'unknown') or opts.get(b'all'),
423 )
423 )
424 if ui.configitems(b'keyword'):
424 if ui.configitems(b'keyword'):
425 raise error.Abort(_(b'[keyword] patterns cannot match'))
425 raise error.Abort(_(b'[keyword] patterns cannot match'))
426 raise error.Abort(_(b'no [keyword] patterns configured'))
426 raise error.Abort(_(b'no [keyword] patterns configured'))
427
427
428
428
429 def _kwfwrite(ui, repo, expand, *pats, **opts):
429 def _kwfwrite(ui, repo, expand, *pats, **opts):
430 '''Selects files and passes them to kwtemplater.overwrite.'''
430 '''Selects files and passes them to kwtemplater.overwrite.'''
431 wctx = repo[None]
431 wctx = repo[None]
432 if len(wctx.parents()) > 1:
432 if len(wctx.parents()) > 1:
433 raise error.Abort(_(b'outstanding uncommitted merge'))
433 raise error.Abort(_(b'outstanding uncommitted merge'))
434 kwt = getattr(repo, '_keywordkwt', None)
434 kwt = getattr(repo, '_keywordkwt', None)
435 with repo.wlock():
435 with repo.wlock():
436 status = _status(ui, repo, wctx, kwt, *pats, **opts)
436 status = _status(ui, repo, wctx, kwt, *pats, **opts)
437 if status.modified or status.added or status.removed or status.deleted:
437 if status.modified or status.added or status.removed or status.deleted:
438 raise error.Abort(_(b'outstanding uncommitted changes'))
438 raise error.Abort(_(b'outstanding uncommitted changes'))
439 kwt.overwrite(wctx, status.clean, True, expand)
439 kwt.overwrite(wctx, status.clean, True, expand)
440
440
441
441
442 @command(
442 @command(
443 b'kwdemo',
443 b'kwdemo',
444 [
444 [
445 (b'd', b'default', None, _(b'show default keyword template maps')),
445 (b'd', b'default', None, _(b'show default keyword template maps')),
446 (b'f', b'rcfile', b'', _(b'read maps from rcfile'), _(b'FILE')),
446 (b'f', b'rcfile', b'', _(b'read maps from rcfile'), _(b'FILE')),
447 ],
447 ],
448 _(b'hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'),
448 _(b'hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'),
449 optionalrepo=True,
449 optionalrepo=True,
450 )
450 )
451 def demo(ui, repo, *args, **opts):
451 def demo(ui, repo, *args, **opts):
452 """print [keywordmaps] configuration and an expansion example
452 """print [keywordmaps] configuration and an expansion example
453
453
454 Show current, custom, or default keyword template maps and their
454 Show current, custom, or default keyword template maps and their
455 expansions.
455 expansions.
456
456
457 Extend the current configuration by specifying maps as arguments
457 Extend the current configuration by specifying maps as arguments
458 and using -f/--rcfile to source an external hgrc file.
458 and using -f/--rcfile to source an external hgrc file.
459
459
460 Use -d/--default to disable current configuration.
460 Use -d/--default to disable current configuration.
461
461
462 See :hg:`help templates` for information on templates and filters.
462 See :hg:`help templates` for information on templates and filters.
463 """
463 """
464
464
465 def demoitems(section, items):
465 def demoitems(section, items):
466 ui.write(b'[%s]\n' % section)
466 ui.write(b'[%s]\n' % section)
467 for k, v in sorted(items):
467 for k, v in sorted(items):
468 if isinstance(v, bool):
468 if isinstance(v, bool):
469 v = stringutil.pprint(v)
469 v = stringutil.pprint(v)
470 ui.write(b'%s = %s\n' % (k, v))
470 ui.write(b'%s = %s\n' % (k, v))
471
471
472 fn = b'demo.txt'
472 fn = b'demo.txt'
473 tmpdir = pycompat.mkdtemp(b'', b'kwdemo.')
473 tmpdir = pycompat.mkdtemp(b'', b'kwdemo.')
474 ui.note(_(b'creating temporary repository at %s\n') % tmpdir)
474 ui.note(_(b'creating temporary repository at %s\n') % tmpdir)
475 if repo is None:
475 if repo is None:
476 baseui = ui
476 baseui = ui
477 else:
477 else:
478 baseui = repo.baseui
478 baseui = repo.baseui
479 repo = localrepo.instance(baseui, tmpdir, create=True)
479 repo = localrepo.instance(baseui, tmpdir, create=True)
480 ui.setconfig(b'keyword', fn, b'', b'keyword')
480 ui.setconfig(b'keyword', fn, b'', b'keyword')
481 svn = ui.configbool(b'keywordset', b'svn')
481 svn = ui.configbool(b'keywordset', b'svn')
482 # explicitly set keywordset for demo output
482 # explicitly set keywordset for demo output
483 ui.setconfig(b'keywordset', b'svn', svn, b'keyword')
483 ui.setconfig(b'keywordset', b'svn', svn, b'keyword')
484
484
485 uikwmaps = ui.configitems(b'keywordmaps')
485 uikwmaps = ui.configitems(b'keywordmaps')
486 if args or opts.get('rcfile'):
486 if args or opts.get('rcfile'):
487 ui.status(_(b'\n\tconfiguration using custom keyword template maps\n'))
487 ui.status(_(b'\n\tconfiguration using custom keyword template maps\n'))
488 if uikwmaps:
488 if uikwmaps:
489 ui.status(_(b'\textending current template maps\n'))
489 ui.status(_(b'\textending current template maps\n'))
490 if opts.get('default') or not uikwmaps:
490 if opts.get('default') or not uikwmaps:
491 if svn:
491 if svn:
492 ui.status(_(b'\toverriding default svn keywordset\n'))
492 ui.status(_(b'\toverriding default svn keywordset\n'))
493 else:
493 else:
494 ui.status(_(b'\toverriding default cvs keywordset\n'))
494 ui.status(_(b'\toverriding default cvs keywordset\n'))
495 if opts.get('rcfile'):
495 if opts.get('rcfile'):
496 ui.readconfig(opts.get(b'rcfile'))
496 ui.readconfig(opts.get(b'rcfile'))
497 if args:
497 if args:
498 # simulate hgrc parsing
498 # simulate hgrc parsing
499 rcmaps = b'[keywordmaps]\n%s\n' % b'\n'.join(args)
499 rcmaps = b'[keywordmaps]\n%s\n' % b'\n'.join(args)
500 repo.vfs.write(b'hgrc', rcmaps)
500 repo.vfs.write(b'hgrc', rcmaps)
501 ui.readconfig(repo.vfs.join(b'hgrc'))
501 ui.readconfig(repo.vfs.join(b'hgrc'))
502 kwmaps = dict(ui.configitems(b'keywordmaps'))
502 kwmaps = dict(ui.configitems(b'keywordmaps'))
503 elif opts.get('default'):
503 elif opts.get('default'):
504 if svn:
504 if svn:
505 ui.status(_(b'\n\tconfiguration using default svn keywordset\n'))
505 ui.status(_(b'\n\tconfiguration using default svn keywordset\n'))
506 else:
506 else:
507 ui.status(_(b'\n\tconfiguration using default cvs keywordset\n'))
507 ui.status(_(b'\n\tconfiguration using default cvs keywordset\n'))
508 kwmaps = _defaultkwmaps(ui)
508 kwmaps = _defaultkwmaps(ui)
509 if uikwmaps:
509 if uikwmaps:
510 ui.status(_(b'\tdisabling current template maps\n'))
510 ui.status(_(b'\tdisabling current template maps\n'))
511 for k, v in pycompat.iteritems(kwmaps):
511 for k, v in pycompat.iteritems(kwmaps):
512 ui.setconfig(b'keywordmaps', k, v, b'keyword')
512 ui.setconfig(b'keywordmaps', k, v, b'keyword')
513 else:
513 else:
514 ui.status(_(b'\n\tconfiguration using current keyword template maps\n'))
514 ui.status(_(b'\n\tconfiguration using current keyword template maps\n'))
515 if uikwmaps:
515 if uikwmaps:
516 kwmaps = dict(uikwmaps)
516 kwmaps = dict(uikwmaps)
517 else:
517 else:
518 kwmaps = _defaultkwmaps(ui)
518 kwmaps = _defaultkwmaps(ui)
519
519
520 uisetup(ui)
520 uisetup(ui)
521 reposetup(ui, repo)
521 reposetup(ui, repo)
522 ui.writenoi18n(b'[extensions]\nkeyword =\n')
522 ui.writenoi18n(b'[extensions]\nkeyword =\n')
523 demoitems(b'keyword', ui.configitems(b'keyword'))
523 demoitems(b'keyword', ui.configitems(b'keyword'))
524 demoitems(b'keywordset', ui.configitems(b'keywordset'))
524 demoitems(b'keywordset', ui.configitems(b'keywordset'))
525 demoitems(b'keywordmaps', pycompat.iteritems(kwmaps))
525 demoitems(b'keywordmaps', pycompat.iteritems(kwmaps))
526 keywords = b'$' + b'$\n$'.join(sorted(kwmaps.keys())) + b'$\n'
526 keywords = b'$' + b'$\n$'.join(sorted(kwmaps.keys())) + b'$\n'
527 repo.wvfs.write(fn, keywords)
527 repo.wvfs.write(fn, keywords)
528 repo[None].add([fn])
528 repo[None].add([fn])
529 ui.note(_(b'\nkeywords written to %s:\n') % fn)
529 ui.note(_(b'\nkeywords written to %s:\n') % fn)
530 ui.note(keywords)
530 ui.note(keywords)
531 with repo.wlock():
531 with repo.wlock():
532 repo.dirstate.setbranch(b'demobranch')
532 repo.dirstate.setbranch(b'demobranch')
533 for name, cmd in ui.configitems(b'hooks'):
533 for name, cmd in ui.configitems(b'hooks'):
534 if name.split(b'.', 1)[0].find(b'commit') > -1:
534 if name.split(b'.', 1)[0].find(b'commit') > -1:
535 repo.ui.setconfig(b'hooks', name, b'', b'keyword')
535 repo.ui.setconfig(b'hooks', name, b'', b'keyword')
536 msg = _(b'hg keyword configuration and expansion example')
536 msg = _(b'hg keyword configuration and expansion example')
537 ui.note((b"hg ci -m '%s'\n" % msg))
537 ui.note((b"hg ci -m '%s'\n" % msg))
538 repo.commit(text=msg)
538 repo.commit(text=msg)
539 ui.status(_(b'\n\tkeywords expanded\n'))
539 ui.status(_(b'\n\tkeywords expanded\n'))
540 ui.write(repo.wread(fn))
540 ui.write(repo.wread(fn))
541 repo.wvfs.rmtree(repo.root)
541 repo.wvfs.rmtree(repo.root)
542
542
543
543
544 @command(
544 @command(
545 b'kwexpand',
545 b'kwexpand',
546 cmdutil.walkopts,
546 cmdutil.walkopts,
547 _(b'hg kwexpand [OPTION]... [FILE]...'),
547 _(b'hg kwexpand [OPTION]... [FILE]...'),
548 inferrepo=True,
548 inferrepo=True,
549 )
549 )
550 def expand(ui, repo, *pats, **opts):
550 def expand(ui, repo, *pats, **opts):
551 """expand keywords in the working directory
551 """expand keywords in the working directory
552
552
553 Run after (re)enabling keyword expansion.
553 Run after (re)enabling keyword expansion.
554
554
555 kwexpand refuses to run if given files contain local changes.
555 kwexpand refuses to run if given files contain local changes.
556 """
556 """
557 # 3rd argument sets expansion to True
557 # 3rd argument sets expansion to True
558 _kwfwrite(ui, repo, True, *pats, **opts)
558 _kwfwrite(ui, repo, True, *pats, **opts)
559
559
560
560
561 @command(
561 @command(
562 b'kwfiles',
562 b'kwfiles',
563 [
563 [
564 (b'A', b'all', None, _(b'show keyword status flags of all files')),
564 (b'A', b'all', None, _(b'show keyword status flags of all files')),
565 (b'i', b'ignore', None, _(b'show files excluded from expansion')),
565 (b'i', b'ignore', None, _(b'show files excluded from expansion')),
566 (b'u', b'unknown', None, _(b'only show unknown (not tracked) files')),
566 (b'u', b'unknown', None, _(b'only show unknown (not tracked) files')),
567 ]
567 ]
568 + cmdutil.walkopts,
568 + cmdutil.walkopts,
569 _(b'hg kwfiles [OPTION]... [FILE]...'),
569 _(b'hg kwfiles [OPTION]... [FILE]...'),
570 inferrepo=True,
570 inferrepo=True,
571 )
571 )
572 def files(ui, repo, *pats, **opts):
572 def files(ui, repo, *pats, **opts):
573 """show files configured for keyword expansion
573 """show files configured for keyword expansion
574
574
575 List which files in the working directory are matched by the
575 List which files in the working directory are matched by the
576 [keyword] configuration patterns.
576 [keyword] configuration patterns.
577
577
578 Useful to prevent inadvertent keyword expansion and to speed up
578 Useful to prevent inadvertent keyword expansion and to speed up
579 execution by including only files that are actual candidates for
579 execution by including only files that are actual candidates for
580 expansion.
580 expansion.
581
581
582 See :hg:`help keyword` on how to construct patterns both for
582 See :hg:`help keyword` on how to construct patterns both for
583 inclusion and exclusion of files.
583 inclusion and exclusion of files.
584
584
585 With -A/--all and -v/--verbose the codes used to show the status
585 With -A/--all and -v/--verbose the codes used to show the status
586 of files are::
586 of files are::
587
587
588 K = keyword expansion candidate
588 K = keyword expansion candidate
589 k = keyword expansion candidate (not tracked)
589 k = keyword expansion candidate (not tracked)
590 I = ignored
590 I = ignored
591 i = ignored (not tracked)
591 i = ignored (not tracked)
592 """
592 """
593 kwt = getattr(repo, '_keywordkwt', None)
593 kwt = getattr(repo, '_keywordkwt', None)
594 wctx = repo[None]
594 wctx = repo[None]
595 status = _status(ui, repo, wctx, kwt, *pats, **opts)
595 status = _status(ui, repo, wctx, kwt, *pats, **opts)
596 if pats:
596 if pats:
597 cwd = repo.getcwd()
597 cwd = repo.getcwd()
598 else:
598 else:
599 cwd = b''
599 cwd = b''
600 files = []
600 files = []
601 opts = pycompat.byteskwargs(opts)
601 opts = pycompat.byteskwargs(opts)
602 if not opts.get(b'unknown') or opts.get(b'all'):
602 if not opts.get(b'unknown') or opts.get(b'all'):
603 files = sorted(status.modified + status.added + status.clean)
603 files = sorted(status.modified + status.added + status.clean)
604 kwfiles = kwt.iskwfile(files, wctx)
604 kwfiles = kwt.iskwfile(files, wctx)
605 kwdeleted = kwt.iskwfile(status.deleted, wctx)
605 kwdeleted = kwt.iskwfile(status.deleted, wctx)
606 kwunknown = kwt.iskwfile(status.unknown, wctx)
606 kwunknown = kwt.iskwfile(status.unknown, wctx)
607 if not opts.get(b'ignore') or opts.get(b'all'):
607 if not opts.get(b'ignore') or opts.get(b'all'):
608 showfiles = kwfiles, kwdeleted, kwunknown
608 showfiles = kwfiles, kwdeleted, kwunknown
609 else:
609 else:
610 showfiles = [], [], []
610 showfiles = [], [], []
611 if opts.get(b'all') or opts.get(b'ignore'):
611 if opts.get(b'all') or opts.get(b'ignore'):
612 showfiles += (
612 showfiles += (
613 [f for f in files if f not in kwfiles],
613 [f for f in files if f not in kwfiles],
614 [f for f in status.unknown if f not in kwunknown],
614 [f for f in status.unknown if f not in kwunknown],
615 )
615 )
616 kwlabels = b'enabled deleted enabledunknown ignored ignoredunknown'.split()
616 kwlabels = b'enabled deleted enabledunknown ignored ignoredunknown'.split()
617 kwstates = zip(kwlabels, pycompat.bytestr(b'K!kIi'), showfiles)
617 kwstates = zip(kwlabels, pycompat.bytestr(b'K!kIi'), showfiles)
618 fm = ui.formatter(b'kwfiles', opts)
618 fm = ui.formatter(b'kwfiles', opts)
619 fmt = b'%.0s%s\n'
619 fmt = b'%.0s%s\n'
620 if opts.get(b'all') or ui.verbose:
620 if opts.get(b'all') or ui.verbose:
621 fmt = b'%s %s\n'
621 fmt = b'%s %s\n'
622 for kwstate, char, filenames in kwstates:
622 for kwstate, char, filenames in kwstates:
623 label = b'kwfiles.' + kwstate
623 label = b'kwfiles.' + kwstate
624 for f in filenames:
624 for f in filenames:
625 fm.startitem()
625 fm.startitem()
626 fm.data(kwstatus=char, path=f)
626 fm.data(kwstatus=char, path=f)
627 fm.plain(fmt % (char, repo.pathto(f, cwd)), label=label)
627 fm.plain(fmt % (char, repo.pathto(f, cwd)), label=label)
628 fm.end()
628 fm.end()
629
629
630
630
631 @command(
631 @command(
632 b'kwshrink',
632 b'kwshrink',
633 cmdutil.walkopts,
633 cmdutil.walkopts,
634 _(b'hg kwshrink [OPTION]... [FILE]...'),
634 _(b'hg kwshrink [OPTION]... [FILE]...'),
635 inferrepo=True,
635 inferrepo=True,
636 )
636 )
637 def shrink(ui, repo, *pats, **opts):
637 def shrink(ui, repo, *pats, **opts):
638 """revert expanded keywords in the working directory
638 """revert expanded keywords in the working directory
639
639
640 Must be run before changing/disabling active keywords.
640 Must be run before changing/disabling active keywords.
641
641
642 kwshrink refuses to run if given files contain local changes.
642 kwshrink refuses to run if given files contain local changes.
643 """
643 """
644 # 3rd argument sets expansion to False
644 # 3rd argument sets expansion to False
645 _kwfwrite(ui, repo, False, *pats, **opts)
645 _kwfwrite(ui, repo, False, *pats, **opts)
646
646
647
647
648 # monkeypatches
648 # monkeypatches
649
649
650
650
651 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
651 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
652 """Monkeypatch/wrap patch.patchfile.__init__ to avoid
652 """Monkeypatch/wrap patch.patchfile.__init__ to avoid
653 rejects or conflicts due to expanded keywords in working dir."""
653 rejects or conflicts due to expanded keywords in working dir."""
654 orig(self, ui, gp, backend, store, eolmode)
654 orig(self, ui, gp, backend, store, eolmode)
655 kwt = getattr(getattr(backend, 'repo', None), '_keywordkwt', None)
655 kwt = getattr(getattr(backend, 'repo', None), '_keywordkwt', None)
656 if kwt:
656 if kwt:
657 # shrink keywords read from working dir
657 # shrink keywords read from working dir
658 self.lines = kwt.shrinklines(self.fname, self.lines)
658 self.lines = kwt.shrinklines(self.fname, self.lines)
659
659
660
660
661 def kwdiff(orig, repo, *args, **kwargs):
661 def kwdiff(orig, repo, *args, **kwargs):
662 '''Monkeypatch patch.diff to avoid expansion.'''
662 '''Monkeypatch patch.diff to avoid expansion.'''
663 kwt = getattr(repo, '_keywordkwt', None)
663 kwt = getattr(repo, '_keywordkwt', None)
664 if kwt:
664 if kwt:
665 restrict = kwt.restrict
665 restrict = kwt.restrict
666 kwt.restrict = True
666 kwt.restrict = True
667 try:
667 try:
668 for chunk in orig(repo, *args, **kwargs):
668 for chunk in orig(repo, *args, **kwargs):
669 yield chunk
669 yield chunk
670 finally:
670 finally:
671 if kwt:
671 if kwt:
672 kwt.restrict = restrict
672 kwt.restrict = restrict
673
673
674
674
675 def kwweb_skip(orig, web):
675 def kwweb_skip(orig, web):
676 '''Wraps webcommands.x turning off keyword expansion.'''
676 '''Wraps webcommands.x turning off keyword expansion.'''
677 kwt = getattr(web.repo, '_keywordkwt', None)
677 kwt = getattr(web.repo, '_keywordkwt', None)
678 if kwt:
678 if kwt:
679 origmatch = kwt.match
679 origmatch = kwt.match
680 kwt.match = util.never
680 kwt.match = util.never
681 try:
681 try:
682 for chunk in orig(web):
682 for chunk in orig(web):
683 yield chunk
683 yield chunk
684 finally:
684 finally:
685 if kwt:
685 if kwt:
686 kwt.match = origmatch
686 kwt.match = origmatch
687
687
688
688
689 def kw_amend(orig, ui, repo, old, extra, pats, opts):
689 def kw_amend(orig, ui, repo, old, extra, pats, opts):
690 '''Wraps cmdutil.amend expanding keywords after amend.'''
690 '''Wraps cmdutil.amend expanding keywords after amend.'''
691 kwt = getattr(repo, '_keywordkwt', None)
691 kwt = getattr(repo, '_keywordkwt', None)
692 if kwt is None:
692 if kwt is None:
693 return orig(ui, repo, old, extra, pats, opts)
693 return orig(ui, repo, old, extra, pats, opts)
694 with repo.wlock():
694 with repo.wlock(), repo.dirstate.parentchange():
695 kwt.postcommit = True
695 kwt.postcommit = True
696 newid = orig(ui, repo, old, extra, pats, opts)
696 newid = orig(ui, repo, old, extra, pats, opts)
697 if newid != old.node():
697 if newid != old.node():
698 ctx = repo[newid]
698 ctx = repo[newid]
699 kwt.restrict = True
699 kwt.restrict = True
700 kwt.overwrite(ctx, ctx.files(), False, True)
700 kwt.overwrite(ctx, ctx.files(), False, True)
701 kwt.restrict = False
701 kwt.restrict = False
702 return newid
702 return newid
703
703
704
704
705 def kw_copy(orig, ui, repo, pats, opts, rename=False):
705 def kw_copy(orig, ui, repo, pats, opts, rename=False):
706 """Wraps cmdutil.copy so that copy/rename destinations do not
706 """Wraps cmdutil.copy so that copy/rename destinations do not
707 contain expanded keywords.
707 contain expanded keywords.
708 Note that the source of a regular file destination may also be a
708 Note that the source of a regular file destination may also be a
709 symlink:
709 symlink:
710 hg cp sym x -> x is symlink
710 hg cp sym x -> x is symlink
711 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
711 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
712 For the latter we have to follow the symlink to find out whether its
712 For the latter we have to follow the symlink to find out whether its
713 target is configured for expansion and we therefore must unexpand the
713 target is configured for expansion and we therefore must unexpand the
714 keywords in the destination."""
714 keywords in the destination."""
715 kwt = getattr(repo, '_keywordkwt', None)
715 kwt = getattr(repo, '_keywordkwt', None)
716 if kwt is None:
716 if kwt is None:
717 return orig(ui, repo, pats, opts, rename)
717 return orig(ui, repo, pats, opts, rename)
718 with repo.wlock():
718 with repo.wlock():
719 orig(ui, repo, pats, opts, rename)
719 orig(ui, repo, pats, opts, rename)
720 if opts.get(b'dry_run'):
720 if opts.get(b'dry_run'):
721 return
721 return
722 wctx = repo[None]
722 wctx = repo[None]
723 cwd = repo.getcwd()
723 cwd = repo.getcwd()
724
724
725 def haskwsource(dest):
725 def haskwsource(dest):
726 """Returns true if dest is a regular file and configured for
726 """Returns true if dest is a regular file and configured for
727 expansion or a symlink which points to a file configured for
727 expansion or a symlink which points to a file configured for
728 expansion."""
728 expansion."""
729 source = repo.dirstate.copied(dest)
729 source = repo.dirstate.copied(dest)
730 if b'l' in wctx.flags(source):
730 if b'l' in wctx.flags(source):
731 source = pathutil.canonpath(
731 source = pathutil.canonpath(
732 repo.root, cwd, os.path.realpath(source)
732 repo.root, cwd, os.path.realpath(source)
733 )
733 )
734 return kwt.match(source)
734 return kwt.match(source)
735
735
736 candidates = [
736 candidates = [
737 f
737 f
738 for f in repo.dirstate.copies()
738 for f in repo.dirstate.copies()
739 if b'l' not in wctx.flags(f) and haskwsource(f)
739 if b'l' not in wctx.flags(f) and haskwsource(f)
740 ]
740 ]
741 kwt.overwrite(wctx, candidates, False, False)
741 kwt.overwrite(wctx, candidates, False, False)
742
742
743
743
744 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
744 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
745 '''Wraps record.dorecord expanding keywords after recording.'''
745 '''Wraps record.dorecord expanding keywords after recording.'''
746 kwt = getattr(repo, '_keywordkwt', None)
746 kwt = getattr(repo, '_keywordkwt', None)
747 if kwt is None:
747 if kwt is None:
748 return orig(ui, repo, commitfunc, *pats, **opts)
748 return orig(ui, repo, commitfunc, *pats, **opts)
749 with repo.wlock():
749 with repo.wlock():
750 # record returns 0 even when nothing has changed
750 # record returns 0 even when nothing has changed
751 # therefore compare nodes before and after
751 # therefore compare nodes before and after
752 kwt.postcommit = True
752 kwt.postcommit = True
753 ctx = repo[b'.']
753 ctx = repo[b'.']
754 wstatus = ctx.status()
754 wstatus = ctx.status()
755 ret = orig(ui, repo, commitfunc, *pats, **opts)
755 ret = orig(ui, repo, commitfunc, *pats, **opts)
756 recctx = repo[b'.']
756 recctx = repo[b'.']
757 if ctx != recctx:
757 if ctx != recctx:
758 modified, added = _preselect(wstatus, recctx.files())
758 modified, added = _preselect(wstatus, recctx.files())
759 kwt.restrict = False
759 kwt.restrict = False
760 kwt.overwrite(recctx, modified, False, True)
760 with repo.dirstate.parentchange():
761 kwt.overwrite(recctx, added, False, True, True)
761 kwt.overwrite(recctx, modified, False, True)
762 kwt.overwrite(recctx, added, False, True, True)
762 kwt.restrict = True
763 kwt.restrict = True
763 return ret
764 return ret
764
765
765
766
766 def kwfilectx_cmp(orig, self, fctx):
767 def kwfilectx_cmp(orig, self, fctx):
767 if fctx._customcmp:
768 if fctx._customcmp:
768 return fctx.cmp(self)
769 return fctx.cmp(self)
769 kwt = getattr(self._repo, '_keywordkwt', None)
770 kwt = getattr(self._repo, '_keywordkwt', None)
770 if kwt is None:
771 if kwt is None:
771 return orig(self, fctx)
772 return orig(self, fctx)
772 # keyword affects data size, comparing wdir and filelog size does
773 # keyword affects data size, comparing wdir and filelog size does
773 # not make sense
774 # not make sense
774 if (
775 if (
775 fctx._filenode is None
776 fctx._filenode is None
776 and (
777 and (
777 self._repo._encodefilterpats
778 self._repo._encodefilterpats
778 or kwt.match(fctx.path())
779 or kwt.match(fctx.path())
779 and b'l' not in fctx.flags()
780 and b'l' not in fctx.flags()
780 or self.size() - 4 == fctx.size()
781 or self.size() - 4 == fctx.size()
781 )
782 )
782 or self.size() == fctx.size()
783 or self.size() == fctx.size()
783 ):
784 ):
784 return self._filelog.cmp(self._filenode, fctx.data())
785 return self._filelog.cmp(self._filenode, fctx.data())
785 return True
786 return True
786
787
787
788
788 def uisetup(ui):
789 def uisetup(ui):
789 """Monkeypatches dispatch._parse to retrieve user command.
790 """Monkeypatches dispatch._parse to retrieve user command.
790 Overrides file method to return kwfilelog instead of filelog
791 Overrides file method to return kwfilelog instead of filelog
791 if file matches user configuration.
792 if file matches user configuration.
792 Wraps commit to overwrite configured files with updated
793 Wraps commit to overwrite configured files with updated
793 keyword substitutions.
794 keyword substitutions.
794 Monkeypatches patch and webcommands."""
795 Monkeypatches patch and webcommands."""
795
796
796 def kwdispatch_parse(orig, ui, args):
797 def kwdispatch_parse(orig, ui, args):
797 '''Monkeypatch dispatch._parse to obtain running hg command.'''
798 '''Monkeypatch dispatch._parse to obtain running hg command.'''
798 cmd, func, args, options, cmdoptions = orig(ui, args)
799 cmd, func, args, options, cmdoptions = orig(ui, args)
799 kwtools[b'hgcmd'] = cmd
800 kwtools[b'hgcmd'] = cmd
800 return cmd, func, args, options, cmdoptions
801 return cmd, func, args, options, cmdoptions
801
802
802 extensions.wrapfunction(dispatch, b'_parse', kwdispatch_parse)
803 extensions.wrapfunction(dispatch, b'_parse', kwdispatch_parse)
803
804
804 extensions.wrapfunction(context.filectx, b'cmp', kwfilectx_cmp)
805 extensions.wrapfunction(context.filectx, b'cmp', kwfilectx_cmp)
805 extensions.wrapfunction(patch.patchfile, b'__init__', kwpatchfile_init)
806 extensions.wrapfunction(patch.patchfile, b'__init__', kwpatchfile_init)
806 extensions.wrapfunction(patch, b'diff', kwdiff)
807 extensions.wrapfunction(patch, b'diff', kwdiff)
807 extensions.wrapfunction(cmdutil, b'amend', kw_amend)
808 extensions.wrapfunction(cmdutil, b'amend', kw_amend)
808 extensions.wrapfunction(cmdutil, b'copy', kw_copy)
809 extensions.wrapfunction(cmdutil, b'copy', kw_copy)
809 extensions.wrapfunction(cmdutil, b'dorecord', kw_dorecord)
810 extensions.wrapfunction(cmdutil, b'dorecord', kw_dorecord)
810 for c in nokwwebcommands.split():
811 for c in nokwwebcommands.split():
811 extensions.wrapfunction(webcommands, c, kwweb_skip)
812 extensions.wrapfunction(webcommands, c, kwweb_skip)
812
813
813
814
814 def reposetup(ui, repo):
815 def reposetup(ui, repo):
815 '''Sets up repo as kwrepo for keyword substitution.'''
816 '''Sets up repo as kwrepo for keyword substitution.'''
816
817
817 try:
818 try:
818 if (
819 if (
819 not repo.local()
820 not repo.local()
820 or kwtools[b'hgcmd'] in nokwcommands.split()
821 or kwtools[b'hgcmd'] in nokwcommands.split()
821 or b'.hg' in util.splitpath(repo.root)
822 or b'.hg' in util.splitpath(repo.root)
822 or repo._url.startswith(b'bundle:')
823 or repo._url.startswith(b'bundle:')
823 ):
824 ):
824 return
825 return
825 except AttributeError:
826 except AttributeError:
826 pass
827 pass
827
828
828 inc, exc = [], [b'.hg*']
829 inc, exc = [], [b'.hg*']
829 for pat, opt in ui.configitems(b'keyword'):
830 for pat, opt in ui.configitems(b'keyword'):
830 if opt != b'ignore':
831 if opt != b'ignore':
831 inc.append(pat)
832 inc.append(pat)
832 else:
833 else:
833 exc.append(pat)
834 exc.append(pat)
834 if not inc:
835 if not inc:
835 return
836 return
836
837
837 kwt = kwtemplater(ui, repo, inc, exc)
838 kwt = kwtemplater(ui, repo, inc, exc)
838
839
839 class kwrepo(repo.__class__):
840 class kwrepo(repo.__class__):
840 def file(self, f):
841 def file(self, f):
841 if f[0] == b'/':
842 if f[0] == b'/':
842 f = f[1:]
843 f = f[1:]
843 return kwfilelog(self.svfs, kwt, f)
844 return kwfilelog(self.svfs, kwt, f)
844
845
845 def wread(self, filename):
846 def wread(self, filename):
846 data = super(kwrepo, self).wread(filename)
847 data = super(kwrepo, self).wread(filename)
847 return kwt.wread(filename, data)
848 return kwt.wread(filename, data)
848
849
849 def commit(self, *args, **opts):
850 def commit(self, *args, **opts):
850 # use custom commitctx for user commands
851 # use custom commitctx for user commands
851 # other extensions can still wrap repo.commitctx directly
852 # other extensions can still wrap repo.commitctx directly
852 self.commitctx = self.kwcommitctx
853 self.commitctx = self.kwcommitctx
853 try:
854 try:
854 return super(kwrepo, self).commit(*args, **opts)
855 return super(kwrepo, self).commit(*args, **opts)
855 finally:
856 finally:
856 del self.commitctx
857 del self.commitctx
857
858
858 def kwcommitctx(self, ctx, error=False, origctx=None):
859 def kwcommitctx(self, ctx, error=False, origctx=None):
859 n = super(kwrepo, self).commitctx(ctx, error, origctx)
860 n = super(kwrepo, self).commitctx(ctx, error, origctx)
860 # no lock needed, only called from repo.commit() which already locks
861 # no lock needed, only called from repo.commit() which already locks
861 if not kwt.postcommit:
862 if not kwt.postcommit:
862 restrict = kwt.restrict
863 restrict = kwt.restrict
863 kwt.restrict = True
864 kwt.restrict = True
864 kwt.overwrite(
865 kwt.overwrite(
865 self[n], sorted(ctx.added() + ctx.modified()), False, True
866 self[n], sorted(ctx.added() + ctx.modified()), False, True
866 )
867 )
867 kwt.restrict = restrict
868 kwt.restrict = restrict
868 return n
869 return n
869
870
870 def rollback(self, dryrun=False, force=False):
871 def rollback(self, dryrun=False, force=False):
871 with self.wlock():
872 with self.wlock():
872 origrestrict = kwt.restrict
873 origrestrict = kwt.restrict
873 try:
874 try:
874 if not dryrun:
875 if not dryrun:
875 changed = self[b'.'].files()
876 changed = self[b'.'].files()
876 ret = super(kwrepo, self).rollback(dryrun, force)
877 ret = super(kwrepo, self).rollback(dryrun, force)
877 if not dryrun:
878 if not dryrun:
878 ctx = self[b'.']
879 ctx = self[b'.']
879 modified, added = _preselect(ctx.status(), changed)
880 modified, added = _preselect(ctx.status(), changed)
880 kwt.restrict = False
881 kwt.restrict = False
881 kwt.overwrite(ctx, modified, True, True)
882 kwt.overwrite(ctx, modified, True, True)
882 kwt.overwrite(ctx, added, True, False)
883 kwt.overwrite(ctx, added, True, False)
883 return ret
884 return ret
884 finally:
885 finally:
885 kwt.restrict = origrestrict
886 kwt.restrict = origrestrict
886
887
887 repo.__class__ = kwrepo
888 repo.__class__ = kwrepo
888 repo._keywordkwt = kwt
889 repo._keywordkwt = kwt
General Comments 0
You need to be logged in to leave comments. Login now