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