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