##// END OF EJS Templates
branch: pass current transaction when writing branch in keyword
marmoute -
r51153:dba35935 default
parent child Browse files
Show More
@@ -1,895 +1,895 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 = b'annotate changeset rev filediff diff comparison'
134 nokwwebcommands = b'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)
423 opts = pycompat.byteskwargs(opts)
424 return repo.status(
424 return repo.status(
425 match=scmutil.match(wctx, pats, opts),
425 match=scmutil.match(wctx, pats, opts),
426 clean=True,
426 clean=True,
427 unknown=opts.get(b'unknown') or opts.get(b'all'),
427 unknown=opts.get(b'unknown') or opts.get(b'all'),
428 )
428 )
429 if ui.configitems(b'keyword'):
429 if ui.configitems(b'keyword'):
430 raise error.Abort(_(b'[keyword] patterns cannot match'))
430 raise error.Abort(_(b'[keyword] patterns cannot match'))
431 raise error.Abort(_(b'no [keyword] patterns configured'))
431 raise error.Abort(_(b'no [keyword] patterns configured'))
432
432
433
433
434 def _kwfwrite(ui, repo, expand, *pats, **opts):
434 def _kwfwrite(ui, repo, expand, *pats, **opts):
435 '''Selects files and passes them to kwtemplater.overwrite.'''
435 '''Selects files and passes them to kwtemplater.overwrite.'''
436 wctx = repo[None]
436 wctx = repo[None]
437 if len(wctx.parents()) > 1:
437 if len(wctx.parents()) > 1:
438 raise error.Abort(_(b'outstanding uncommitted merge'))
438 raise error.Abort(_(b'outstanding uncommitted merge'))
439 kwt = getattr(repo, '_keywordkwt', None)
439 kwt = getattr(repo, '_keywordkwt', None)
440 with repo.wlock(), repo.dirstate.changing_files(repo):
440 with repo.wlock(), repo.dirstate.changing_files(repo):
441 status = _status(ui, repo, wctx, kwt, *pats, **opts)
441 status = _status(ui, repo, wctx, kwt, *pats, **opts)
442 if status.modified or status.added or status.removed or status.deleted:
442 if status.modified or status.added or status.removed or status.deleted:
443 raise error.Abort(_(b'outstanding uncommitted changes'))
443 raise error.Abort(_(b'outstanding uncommitted changes'))
444 kwt.overwrite(wctx, status.clean, True, expand)
444 kwt.overwrite(wctx, status.clean, True, expand)
445
445
446
446
447 @command(
447 @command(
448 b'kwdemo',
448 b'kwdemo',
449 [
449 [
450 (b'd', b'default', None, _(b'show default keyword template maps')),
450 (b'd', b'default', None, _(b'show default keyword template maps')),
451 (b'f', b'rcfile', b'', _(b'read maps from rcfile'), _(b'FILE')),
451 (b'f', b'rcfile', b'', _(b'read maps from rcfile'), _(b'FILE')),
452 ],
452 ],
453 _(b'hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'),
453 _(b'hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'),
454 optionalrepo=True,
454 optionalrepo=True,
455 )
455 )
456 def demo(ui, repo, *args, **opts):
456 def demo(ui, repo, *args, **opts):
457 """print [keywordmaps] configuration and an expansion example
457 """print [keywordmaps] configuration and an expansion example
458
458
459 Show current, custom, or default keyword template maps and their
459 Show current, custom, or default keyword template maps and their
460 expansions.
460 expansions.
461
461
462 Extend the current configuration by specifying maps as arguments
462 Extend the current configuration by specifying maps as arguments
463 and using -f/--rcfile to source an external hgrc file.
463 and using -f/--rcfile to source an external hgrc file.
464
464
465 Use -d/--default to disable current configuration.
465 Use -d/--default to disable current configuration.
466
466
467 See :hg:`help templates` for information on templates and filters.
467 See :hg:`help templates` for information on templates and filters.
468 """
468 """
469
469
470 def demoitems(section, items):
470 def demoitems(section, items):
471 ui.write(b'[%s]\n' % section)
471 ui.write(b'[%s]\n' % section)
472 for k, v in sorted(items):
472 for k, v in sorted(items):
473 if isinstance(v, bool):
473 if isinstance(v, bool):
474 v = stringutil.pprint(v)
474 v = stringutil.pprint(v)
475 ui.write(b'%s = %s\n' % (k, v))
475 ui.write(b'%s = %s\n' % (k, v))
476
476
477 fn = b'demo.txt'
477 fn = b'demo.txt'
478 tmpdir = pycompat.mkdtemp(b'', b'kwdemo.')
478 tmpdir = pycompat.mkdtemp(b'', b'kwdemo.')
479 ui.note(_(b'creating temporary repository at %s\n') % tmpdir)
479 ui.note(_(b'creating temporary repository at %s\n') % tmpdir)
480 if repo is None:
480 if repo is None:
481 baseui = ui
481 baseui = ui
482 else:
482 else:
483 baseui = repo.baseui
483 baseui = repo.baseui
484 repo = localrepo.instance(baseui, tmpdir, create=True)
484 repo = localrepo.instance(baseui, tmpdir, create=True)
485 ui.setconfig(b'keyword', fn, b'', b'keyword')
485 ui.setconfig(b'keyword', fn, b'', b'keyword')
486 svn = ui.configbool(b'keywordset', b'svn')
486 svn = ui.configbool(b'keywordset', b'svn')
487 # explicitly set keywordset for demo output
487 # explicitly set keywordset for demo output
488 ui.setconfig(b'keywordset', b'svn', svn, b'keyword')
488 ui.setconfig(b'keywordset', b'svn', svn, b'keyword')
489
489
490 uikwmaps = ui.configitems(b'keywordmaps')
490 uikwmaps = ui.configitems(b'keywordmaps')
491 if args or opts.get('rcfile'):
491 if args or opts.get('rcfile'):
492 ui.status(_(b'\n\tconfiguration using custom keyword template maps\n'))
492 ui.status(_(b'\n\tconfiguration using custom keyword template maps\n'))
493 if uikwmaps:
493 if uikwmaps:
494 ui.status(_(b'\textending current template maps\n'))
494 ui.status(_(b'\textending current template maps\n'))
495 if opts.get('default') or not uikwmaps:
495 if opts.get('default') or not uikwmaps:
496 if svn:
496 if svn:
497 ui.status(_(b'\toverriding default svn keywordset\n'))
497 ui.status(_(b'\toverriding default svn keywordset\n'))
498 else:
498 else:
499 ui.status(_(b'\toverriding default cvs keywordset\n'))
499 ui.status(_(b'\toverriding default cvs keywordset\n'))
500 if opts.get('rcfile'):
500 if opts.get('rcfile'):
501 ui.readconfig(opts.get(b'rcfile'))
501 ui.readconfig(opts.get(b'rcfile'))
502 if args:
502 if args:
503 # simulate hgrc parsing
503 # simulate hgrc parsing
504 rcmaps = b'[keywordmaps]\n%s\n' % b'\n'.join(args)
504 rcmaps = b'[keywordmaps]\n%s\n' % b'\n'.join(args)
505 repo.vfs.write(b'hgrc', rcmaps)
505 repo.vfs.write(b'hgrc', rcmaps)
506 ui.readconfig(repo.vfs.join(b'hgrc'))
506 ui.readconfig(repo.vfs.join(b'hgrc'))
507 kwmaps = dict(ui.configitems(b'keywordmaps'))
507 kwmaps = dict(ui.configitems(b'keywordmaps'))
508 elif opts.get('default'):
508 elif opts.get('default'):
509 if svn:
509 if svn:
510 ui.status(_(b'\n\tconfiguration using default svn keywordset\n'))
510 ui.status(_(b'\n\tconfiguration using default svn keywordset\n'))
511 else:
511 else:
512 ui.status(_(b'\n\tconfiguration using default cvs keywordset\n'))
512 ui.status(_(b'\n\tconfiguration using default cvs keywordset\n'))
513 kwmaps = _defaultkwmaps(ui)
513 kwmaps = _defaultkwmaps(ui)
514 if uikwmaps:
514 if uikwmaps:
515 ui.status(_(b'\tdisabling current template maps\n'))
515 ui.status(_(b'\tdisabling current template maps\n'))
516 for k, v in kwmaps.items():
516 for k, v in kwmaps.items():
517 ui.setconfig(b'keywordmaps', k, v, b'keyword')
517 ui.setconfig(b'keywordmaps', k, v, b'keyword')
518 else:
518 else:
519 ui.status(_(b'\n\tconfiguration using current keyword template maps\n'))
519 ui.status(_(b'\n\tconfiguration using current keyword template maps\n'))
520 if uikwmaps:
520 if uikwmaps:
521 kwmaps = dict(uikwmaps)
521 kwmaps = dict(uikwmaps)
522 else:
522 else:
523 kwmaps = _defaultkwmaps(ui)
523 kwmaps = _defaultkwmaps(ui)
524
524
525 uisetup(ui)
525 uisetup(ui)
526 reposetup(ui, repo)
526 reposetup(ui, repo)
527 ui.writenoi18n(b'[extensions]\nkeyword =\n')
527 ui.writenoi18n(b'[extensions]\nkeyword =\n')
528 demoitems(b'keyword', ui.configitems(b'keyword'))
528 demoitems(b'keyword', ui.configitems(b'keyword'))
529 demoitems(b'keywordset', ui.configitems(b'keywordset'))
529 demoitems(b'keywordset', ui.configitems(b'keywordset'))
530 demoitems(b'keywordmaps', kwmaps.items())
530 demoitems(b'keywordmaps', kwmaps.items())
531 keywords = b'$' + b'$\n$'.join(sorted(kwmaps.keys())) + b'$\n'
531 keywords = b'$' + b'$\n$'.join(sorted(kwmaps.keys())) + b'$\n'
532 repo.wvfs.write(fn, keywords)
532 repo.wvfs.write(fn, keywords)
533 with repo.wlock():
533 with repo.wlock():
534 with repo.dirstate.changing_files(repo):
534 with repo.dirstate.changing_files(repo):
535 repo[None].add([fn])
535 repo[None].add([fn])
536 ui.note(_(b'\nkeywords written to %s:\n') % fn)
536 ui.note(_(b'\nkeywords written to %s:\n') % fn)
537 ui.note(keywords)
537 ui.note(keywords)
538 repo.dirstate.setbranch(b'demobranch')
538 repo.dirstate.setbranch(b'demobranch', repo.currenttransaction())
539 for name, cmd in ui.configitems(b'hooks'):
539 for name, cmd in ui.configitems(b'hooks'):
540 if name.split(b'.', 1)[0].find(b'commit') > -1:
540 if name.split(b'.', 1)[0].find(b'commit') > -1:
541 repo.ui.setconfig(b'hooks', name, b'', b'keyword')
541 repo.ui.setconfig(b'hooks', name, b'', b'keyword')
542 msg = _(b'hg keyword configuration and expansion example')
542 msg = _(b'hg keyword configuration and expansion example')
543 ui.note((b"hg ci -m '%s'\n" % msg))
543 ui.note((b"hg ci -m '%s'\n" % msg))
544 repo.commit(text=msg)
544 repo.commit(text=msg)
545 ui.status(_(b'\n\tkeywords expanded\n'))
545 ui.status(_(b'\n\tkeywords expanded\n'))
546 ui.write(repo.wread(fn))
546 ui.write(repo.wread(fn))
547 repo.wvfs.rmtree(repo.root)
547 repo.wvfs.rmtree(repo.root)
548
548
549
549
550 @command(
550 @command(
551 b'kwexpand',
551 b'kwexpand',
552 cmdutil.walkopts,
552 cmdutil.walkopts,
553 _(b'hg kwexpand [OPTION]... [FILE]...'),
553 _(b'hg kwexpand [OPTION]... [FILE]...'),
554 inferrepo=True,
554 inferrepo=True,
555 )
555 )
556 def expand(ui, repo, *pats, **opts):
556 def expand(ui, repo, *pats, **opts):
557 """expand keywords in the working directory
557 """expand keywords in the working directory
558
558
559 Run after (re)enabling keyword expansion.
559 Run after (re)enabling keyword expansion.
560
560
561 kwexpand refuses to run if given files contain local changes.
561 kwexpand refuses to run if given files contain local changes.
562 """
562 """
563 # 3rd argument sets expansion to True
563 # 3rd argument sets expansion to True
564 _kwfwrite(ui, repo, True, *pats, **opts)
564 _kwfwrite(ui, repo, True, *pats, **opts)
565
565
566
566
567 @command(
567 @command(
568 b'kwfiles',
568 b'kwfiles',
569 [
569 [
570 (b'A', b'all', None, _(b'show keyword status flags of all files')),
570 (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')),
571 (b'i', b'ignore', None, _(b'show files excluded from expansion')),
572 (b'u', b'unknown', None, _(b'only show unknown (not tracked) files')),
572 (b'u', b'unknown', None, _(b'only show unknown (not tracked) files')),
573 ]
573 ]
574 + cmdutil.walkopts,
574 + cmdutil.walkopts,
575 _(b'hg kwfiles [OPTION]... [FILE]...'),
575 _(b'hg kwfiles [OPTION]... [FILE]...'),
576 inferrepo=True,
576 inferrepo=True,
577 )
577 )
578 def files(ui, repo, *pats, **opts):
578 def files(ui, repo, *pats, **opts):
579 """show files configured for keyword expansion
579 """show files configured for keyword expansion
580
580
581 List which files in the working directory are matched by the
581 List which files in the working directory are matched by the
582 [keyword] configuration patterns.
582 [keyword] configuration patterns.
583
583
584 Useful to prevent inadvertent keyword expansion and to speed up
584 Useful to prevent inadvertent keyword expansion and to speed up
585 execution by including only files that are actual candidates for
585 execution by including only files that are actual candidates for
586 expansion.
586 expansion.
587
587
588 See :hg:`help keyword` on how to construct patterns both for
588 See :hg:`help keyword` on how to construct patterns both for
589 inclusion and exclusion of files.
589 inclusion and exclusion of files.
590
590
591 With -A/--all and -v/--verbose the codes used to show the status
591 With -A/--all and -v/--verbose the codes used to show the status
592 of files are::
592 of files are::
593
593
594 K = keyword expansion candidate
594 K = keyword expansion candidate
595 k = keyword expansion candidate (not tracked)
595 k = keyword expansion candidate (not tracked)
596 I = ignored
596 I = ignored
597 i = ignored (not tracked)
597 i = ignored (not tracked)
598 """
598 """
599 kwt = getattr(repo, '_keywordkwt', None)
599 kwt = getattr(repo, '_keywordkwt', None)
600 wctx = repo[None]
600 wctx = repo[None]
601 status = _status(ui, repo, wctx, kwt, *pats, **opts)
601 status = _status(ui, repo, wctx, kwt, *pats, **opts)
602 if pats:
602 if pats:
603 cwd = repo.getcwd()
603 cwd = repo.getcwd()
604 else:
604 else:
605 cwd = b''
605 cwd = b''
606 files = []
606 files = []
607 opts = pycompat.byteskwargs(opts)
607 opts = pycompat.byteskwargs(opts)
608 if not opts.get(b'unknown') or opts.get(b'all'):
608 if not opts.get(b'unknown') or opts.get(b'all'):
609 files = sorted(status.modified + status.added + status.clean)
609 files = sorted(status.modified + status.added + status.clean)
610 kwfiles = kwt.iskwfile(files, wctx)
610 kwfiles = kwt.iskwfile(files, wctx)
611 kwdeleted = kwt.iskwfile(status.deleted, wctx)
611 kwdeleted = kwt.iskwfile(status.deleted, wctx)
612 kwunknown = kwt.iskwfile(status.unknown, wctx)
612 kwunknown = kwt.iskwfile(status.unknown, wctx)
613 if not opts.get(b'ignore') or opts.get(b'all'):
613 if not opts.get(b'ignore') or opts.get(b'all'):
614 showfiles = kwfiles, kwdeleted, kwunknown
614 showfiles = kwfiles, kwdeleted, kwunknown
615 else:
615 else:
616 showfiles = [], [], []
616 showfiles = [], [], []
617 if opts.get(b'all') or opts.get(b'ignore'):
617 if opts.get(b'all') or opts.get(b'ignore'):
618 showfiles += (
618 showfiles += (
619 [f for f in files if f not in kwfiles],
619 [f for f in files if f not in kwfiles],
620 [f for f in status.unknown if f not in kwunknown],
620 [f for f in status.unknown if f not in kwunknown],
621 )
621 )
622 kwlabels = b'enabled deleted enabledunknown ignored ignoredunknown'.split()
622 kwlabels = b'enabled deleted enabledunknown ignored ignoredunknown'.split()
623 kwstates = zip(kwlabels, pycompat.bytestr(b'K!kIi'), showfiles)
623 kwstates = zip(kwlabels, pycompat.bytestr(b'K!kIi'), showfiles)
624 fm = ui.formatter(b'kwfiles', opts)
624 fm = ui.formatter(b'kwfiles', opts)
625 fmt = b'%.0s%s\n'
625 fmt = b'%.0s%s\n'
626 if opts.get(b'all') or ui.verbose:
626 if opts.get(b'all') or ui.verbose:
627 fmt = b'%s %s\n'
627 fmt = b'%s %s\n'
628 for kwstate, char, filenames in kwstates:
628 for kwstate, char, filenames in kwstates:
629 label = b'kwfiles.' + kwstate
629 label = b'kwfiles.' + kwstate
630 for f in filenames:
630 for f in filenames:
631 fm.startitem()
631 fm.startitem()
632 fm.data(kwstatus=char, path=f)
632 fm.data(kwstatus=char, path=f)
633 fm.plain(fmt % (char, repo.pathto(f, cwd)), label=label)
633 fm.plain(fmt % (char, repo.pathto(f, cwd)), label=label)
634 fm.end()
634 fm.end()
635
635
636
636
637 @command(
637 @command(
638 b'kwshrink',
638 b'kwshrink',
639 cmdutil.walkopts,
639 cmdutil.walkopts,
640 _(b'hg kwshrink [OPTION]... [FILE]...'),
640 _(b'hg kwshrink [OPTION]... [FILE]...'),
641 inferrepo=True,
641 inferrepo=True,
642 )
642 )
643 def shrink(ui, repo, *pats, **opts):
643 def shrink(ui, repo, *pats, **opts):
644 """revert expanded keywords in the working directory
644 """revert expanded keywords in the working directory
645
645
646 Must be run before changing/disabling active keywords.
646 Must be run before changing/disabling active keywords.
647
647
648 kwshrink refuses to run if given files contain local changes.
648 kwshrink refuses to run if given files contain local changes.
649 """
649 """
650 # 3rd argument sets expansion to False
650 # 3rd argument sets expansion to False
651 _kwfwrite(ui, repo, False, *pats, **opts)
651 _kwfwrite(ui, repo, False, *pats, **opts)
652
652
653
653
654 # monkeypatches
654 # monkeypatches
655
655
656
656
657 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
657 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
658 """Monkeypatch/wrap patch.patchfile.__init__ to avoid
658 """Monkeypatch/wrap patch.patchfile.__init__ to avoid
659 rejects or conflicts due to expanded keywords in working dir."""
659 rejects or conflicts due to expanded keywords in working dir."""
660 orig(self, ui, gp, backend, store, eolmode)
660 orig(self, ui, gp, backend, store, eolmode)
661 kwt = getattr(getattr(backend, 'repo', None), '_keywordkwt', None)
661 kwt = getattr(getattr(backend, 'repo', None), '_keywordkwt', None)
662 if kwt:
662 if kwt:
663 # shrink keywords read from working dir
663 # shrink keywords read from working dir
664 self.lines = kwt.shrinklines(self.fname, self.lines)
664 self.lines = kwt.shrinklines(self.fname, self.lines)
665
665
666
666
667 def kwdiff(orig, repo, *args, **kwargs):
667 def kwdiff(orig, repo, *args, **kwargs):
668 '''Monkeypatch patch.diff to avoid expansion.'''
668 '''Monkeypatch patch.diff to avoid expansion.'''
669 kwt = getattr(repo, '_keywordkwt', None)
669 kwt = getattr(repo, '_keywordkwt', None)
670 if kwt:
670 if kwt:
671 restrict = kwt.restrict
671 restrict = kwt.restrict
672 kwt.restrict = True
672 kwt.restrict = True
673 try:
673 try:
674 for chunk in orig(repo, *args, **kwargs):
674 for chunk in orig(repo, *args, **kwargs):
675 yield chunk
675 yield chunk
676 finally:
676 finally:
677 if kwt:
677 if kwt:
678 kwt.restrict = restrict
678 kwt.restrict = restrict
679
679
680
680
681 def kwweb_skip(orig, web):
681 def kwweb_skip(orig, web):
682 '''Wraps webcommands.x turning off keyword expansion.'''
682 '''Wraps webcommands.x turning off keyword expansion.'''
683 kwt = getattr(web.repo, '_keywordkwt', None)
683 kwt = getattr(web.repo, '_keywordkwt', None)
684 if kwt:
684 if kwt:
685 origmatch = kwt.match
685 origmatch = kwt.match
686 kwt.match = util.never
686 kwt.match = util.never
687 try:
687 try:
688 for chunk in orig(web):
688 for chunk in orig(web):
689 yield chunk
689 yield chunk
690 finally:
690 finally:
691 if kwt:
691 if kwt:
692 kwt.match = origmatch
692 kwt.match = origmatch
693
693
694
694
695 def kw_amend(orig, ui, repo, old, extra, pats, opts):
695 def kw_amend(orig, ui, repo, old, extra, pats, opts):
696 '''Wraps cmdutil.amend expanding keywords after amend.'''
696 '''Wraps cmdutil.amend expanding keywords after amend.'''
697 kwt = getattr(repo, '_keywordkwt', None)
697 kwt = getattr(repo, '_keywordkwt', None)
698 if kwt is None:
698 if kwt is None:
699 return orig(ui, repo, old, extra, pats, opts)
699 return orig(ui, repo, old, extra, pats, opts)
700 with repo.wlock(), repo.dirstate.changing_parents(repo):
700 with repo.wlock(), repo.dirstate.changing_parents(repo):
701 kwt.postcommit = True
701 kwt.postcommit = True
702 newid = orig(ui, repo, old, extra, pats, opts)
702 newid = orig(ui, repo, old, extra, pats, opts)
703 if newid != old.node():
703 if newid != old.node():
704 ctx = repo[newid]
704 ctx = repo[newid]
705 kwt.restrict = True
705 kwt.restrict = True
706 kwt.overwrite(ctx, ctx.files(), False, True)
706 kwt.overwrite(ctx, ctx.files(), False, True)
707 kwt.restrict = False
707 kwt.restrict = False
708 return newid
708 return newid
709
709
710
710
711 def kw_copy(orig, ui, repo, pats, opts, rename=False):
711 def kw_copy(orig, ui, repo, pats, opts, rename=False):
712 """Wraps cmdutil.copy so that copy/rename destinations do not
712 """Wraps cmdutil.copy so that copy/rename destinations do not
713 contain expanded keywords.
713 contain expanded keywords.
714 Note that the source of a regular file destination may also be a
714 Note that the source of a regular file destination may also be a
715 symlink:
715 symlink:
716 hg cp sym x -> x is symlink
716 hg cp sym x -> x is symlink
717 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
717 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
718 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
719 target is configured for expansion and we therefore must unexpand the
720 keywords in the destination."""
720 keywords in the destination."""
721 kwt = getattr(repo, '_keywordkwt', None)
721 kwt = getattr(repo, '_keywordkwt', None)
722 if kwt is None:
722 if kwt is None:
723 return orig(ui, repo, pats, opts, rename)
723 return orig(ui, repo, pats, opts, rename)
724 with repo.wlock():
724 with repo.wlock():
725 orig(ui, repo, pats, opts, rename)
725 orig(ui, repo, pats, opts, rename)
726 if opts.get(b'dry_run'):
726 if opts.get(b'dry_run'):
727 return
727 return
728 wctx = repo[None]
728 wctx = repo[None]
729 cwd = repo.getcwd()
729 cwd = repo.getcwd()
730
730
731 def haskwsource(dest):
731 def haskwsource(dest):
732 """Returns true if dest is a regular file and configured for
732 """Returns true if dest is a regular file and configured for
733 expansion or a symlink which points to a file configured for
733 expansion or a symlink which points to a file configured for
734 expansion."""
734 expansion."""
735 source = repo.dirstate.copied(dest)
735 source = repo.dirstate.copied(dest)
736 if b'l' in wctx.flags(source):
736 if b'l' in wctx.flags(source):
737 source = pathutil.canonpath(
737 source = pathutil.canonpath(
738 repo.root, cwd, os.path.realpath(source)
738 repo.root, cwd, os.path.realpath(source)
739 )
739 )
740 return kwt.match(source)
740 return kwt.match(source)
741
741
742 candidates = [
742 candidates = [
743 f
743 f
744 for f in repo.dirstate.copies()
744 for f in repo.dirstate.copies()
745 if b'l' not in wctx.flags(f) and haskwsource(f)
745 if b'l' not in wctx.flags(f) and haskwsource(f)
746 ]
746 ]
747 kwt.overwrite(wctx, candidates, False, False)
747 kwt.overwrite(wctx, candidates, False, False)
748
748
749
749
750 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
750 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
751 '''Wraps record.dorecord expanding keywords after recording.'''
751 '''Wraps record.dorecord expanding keywords after recording.'''
752 kwt = getattr(repo, '_keywordkwt', None)
752 kwt = getattr(repo, '_keywordkwt', None)
753 if kwt is None:
753 if kwt is None:
754 return orig(ui, repo, commitfunc, *pats, **opts)
754 return orig(ui, repo, commitfunc, *pats, **opts)
755 with repo.wlock():
755 with repo.wlock():
756 # record returns 0 even when nothing has changed
756 # record returns 0 even when nothing has changed
757 # therefore compare nodes before and after
757 # therefore compare nodes before and after
758 kwt.postcommit = True
758 kwt.postcommit = True
759 ctx = repo[b'.']
759 ctx = repo[b'.']
760 wstatus = ctx.status()
760 wstatus = ctx.status()
761 ret = orig(ui, repo, commitfunc, *pats, **opts)
761 ret = orig(ui, repo, commitfunc, *pats, **opts)
762 recctx = repo[b'.']
762 recctx = repo[b'.']
763 if ctx != recctx:
763 if ctx != recctx:
764 modified, added = _preselect(wstatus, recctx.files())
764 modified, added = _preselect(wstatus, recctx.files())
765 kwt.restrict = False
765 kwt.restrict = False
766 with repo.dirstate.changing_parents(repo):
766 with repo.dirstate.changing_parents(repo):
767 kwt.overwrite(recctx, modified, False, True)
767 kwt.overwrite(recctx, modified, False, True)
768 kwt.overwrite(recctx, added, False, True, True)
768 kwt.overwrite(recctx, added, False, True, True)
769 kwt.restrict = True
769 kwt.restrict = True
770 return ret
770 return ret
771
771
772
772
773 def kwfilectx_cmp(orig, self, fctx):
773 def kwfilectx_cmp(orig, self, fctx):
774 if fctx._customcmp:
774 if fctx._customcmp:
775 return fctx.cmp(self)
775 return fctx.cmp(self)
776 kwt = getattr(self._repo, '_keywordkwt', None)
776 kwt = getattr(self._repo, '_keywordkwt', None)
777 if kwt is None:
777 if kwt is None:
778 return orig(self, fctx)
778 return orig(self, fctx)
779 # keyword affects data size, comparing wdir and filelog size does
779 # keyword affects data size, comparing wdir and filelog size does
780 # not make sense
780 # not make sense
781 if (
781 if (
782 fctx._filenode is None
782 fctx._filenode is None
783 and (
783 and (
784 self._repo._encodefilterpats
784 self._repo._encodefilterpats
785 or kwt.match(fctx.path())
785 or kwt.match(fctx.path())
786 and b'l' not in fctx.flags()
786 and b'l' not in fctx.flags()
787 or self.size() - 4 == fctx.size()
787 or self.size() - 4 == fctx.size()
788 )
788 )
789 or self.size() == fctx.size()
789 or self.size() == fctx.size()
790 ):
790 ):
791 return self._filelog.cmp(self._filenode, fctx.data())
791 return self._filelog.cmp(self._filenode, fctx.data())
792 return True
792 return True
793
793
794
794
795 def uisetup(ui):
795 def uisetup(ui):
796 """Monkeypatches dispatch._parse to retrieve user command.
796 """Monkeypatches dispatch._parse to retrieve user command.
797 Overrides file method to return kwfilelog instead of filelog
797 Overrides file method to return kwfilelog instead of filelog
798 if file matches user configuration.
798 if file matches user configuration.
799 Wraps commit to overwrite configured files with updated
799 Wraps commit to overwrite configured files with updated
800 keyword substitutions.
800 keyword substitutions.
801 Monkeypatches patch and webcommands."""
801 Monkeypatches patch and webcommands."""
802
802
803 def kwdispatch_parse(orig, ui, args):
803 def kwdispatch_parse(orig, ui, args):
804 '''Monkeypatch dispatch._parse to obtain running hg command.'''
804 '''Monkeypatch dispatch._parse to obtain running hg command.'''
805 cmd, func, args, options, cmdoptions = orig(ui, args)
805 cmd, func, args, options, cmdoptions = orig(ui, args)
806 kwtools[b'hgcmd'] = cmd
806 kwtools[b'hgcmd'] = cmd
807 return cmd, func, args, options, cmdoptions
807 return cmd, func, args, options, cmdoptions
808
808
809 extensions.wrapfunction(dispatch, b'_parse', kwdispatch_parse)
809 extensions.wrapfunction(dispatch, b'_parse', kwdispatch_parse)
810
810
811 extensions.wrapfunction(context.filectx, b'cmp', kwfilectx_cmp)
811 extensions.wrapfunction(context.filectx, b'cmp', kwfilectx_cmp)
812 extensions.wrapfunction(patch.patchfile, b'__init__', kwpatchfile_init)
812 extensions.wrapfunction(patch.patchfile, b'__init__', kwpatchfile_init)
813 extensions.wrapfunction(patch, b'diff', kwdiff)
813 extensions.wrapfunction(patch, b'diff', kwdiff)
814 extensions.wrapfunction(cmdutil, b'amend', kw_amend)
814 extensions.wrapfunction(cmdutil, b'amend', kw_amend)
815 extensions.wrapfunction(cmdutil, b'copy', kw_copy)
815 extensions.wrapfunction(cmdutil, b'copy', kw_copy)
816 extensions.wrapfunction(cmdutil, b'dorecord', kw_dorecord)
816 extensions.wrapfunction(cmdutil, b'dorecord', kw_dorecord)
817 for c in nokwwebcommands.split():
817 for c in nokwwebcommands.split():
818 extensions.wrapfunction(webcommands, c, kwweb_skip)
818 extensions.wrapfunction(webcommands, c, kwweb_skip)
819
819
820
820
821 def reposetup(ui, repo):
821 def reposetup(ui, repo):
822 '''Sets up repo as kwrepo for keyword substitution.'''
822 '''Sets up repo as kwrepo for keyword substitution.'''
823
823
824 try:
824 try:
825 if (
825 if (
826 not repo.local()
826 not repo.local()
827 or kwtools[b'hgcmd'] in nokwcommands.split()
827 or kwtools[b'hgcmd'] in nokwcommands.split()
828 or b'.hg' in util.splitpath(repo.root)
828 or b'.hg' in util.splitpath(repo.root)
829 or repo._url.startswith(b'bundle:')
829 or repo._url.startswith(b'bundle:')
830 ):
830 ):
831 return
831 return
832 except AttributeError:
832 except AttributeError:
833 pass
833 pass
834
834
835 inc, exc = [], [b'.hg*']
835 inc, exc = [], [b'.hg*']
836 for pat, opt in ui.configitems(b'keyword'):
836 for pat, opt in ui.configitems(b'keyword'):
837 if opt != b'ignore':
837 if opt != b'ignore':
838 inc.append(pat)
838 inc.append(pat)
839 else:
839 else:
840 exc.append(pat)
840 exc.append(pat)
841 if not inc:
841 if not inc:
842 return
842 return
843
843
844 kwt = kwtemplater(ui, repo, inc, exc)
844 kwt = kwtemplater(ui, repo, inc, exc)
845
845
846 class kwrepo(repo.__class__):
846 class kwrepo(repo.__class__):
847 def file(self, f):
847 def file(self, f):
848 if f[0] == b'/':
848 if f[0] == b'/':
849 f = f[1:]
849 f = f[1:]
850 return kwfilelog(self.svfs, kwt, f)
850 return kwfilelog(self.svfs, kwt, f)
851
851
852 def wread(self, filename):
852 def wread(self, filename):
853 data = super(kwrepo, self).wread(filename)
853 data = super(kwrepo, self).wread(filename)
854 return kwt.wread(filename, data)
854 return kwt.wread(filename, data)
855
855
856 def commit(self, *args, **opts):
856 def commit(self, *args, **opts):
857 # use custom commitctx for user commands
857 # use custom commitctx for user commands
858 # other extensions can still wrap repo.commitctx directly
858 # other extensions can still wrap repo.commitctx directly
859 self.commitctx = self.kwcommitctx
859 self.commitctx = self.kwcommitctx
860 try:
860 try:
861 return super(kwrepo, self).commit(*args, **opts)
861 return super(kwrepo, self).commit(*args, **opts)
862 finally:
862 finally:
863 del self.commitctx
863 del self.commitctx
864
864
865 def kwcommitctx(self, ctx, error=False, origctx=None):
865 def kwcommitctx(self, ctx, error=False, origctx=None):
866 n = super(kwrepo, self).commitctx(ctx, error, origctx)
866 n = super(kwrepo, self).commitctx(ctx, error, origctx)
867 # no lock needed, only called from repo.commit() which already locks
867 # no lock needed, only called from repo.commit() which already locks
868 if not kwt.postcommit:
868 if not kwt.postcommit:
869 restrict = kwt.restrict
869 restrict = kwt.restrict
870 kwt.restrict = True
870 kwt.restrict = True
871 kwt.overwrite(
871 kwt.overwrite(
872 self[n], sorted(ctx.added() + ctx.modified()), False, True
872 self[n], sorted(ctx.added() + ctx.modified()), False, True
873 )
873 )
874 kwt.restrict = restrict
874 kwt.restrict = restrict
875 return n
875 return n
876
876
877 def rollback(self, dryrun=False, force=False):
877 def rollback(self, dryrun=False, force=False):
878 with self.wlock():
878 with self.wlock():
879 origrestrict = kwt.restrict
879 origrestrict = kwt.restrict
880 try:
880 try:
881 if not dryrun:
881 if not dryrun:
882 changed = self[b'.'].files()
882 changed = self[b'.'].files()
883 ret = super(kwrepo, self).rollback(dryrun, force)
883 ret = super(kwrepo, self).rollback(dryrun, force)
884 if not dryrun:
884 if not dryrun:
885 ctx = self[b'.']
885 ctx = self[b'.']
886 modified, added = _preselect(ctx.status(), changed)
886 modified, added = _preselect(ctx.status(), changed)
887 kwt.restrict = False
887 kwt.restrict = False
888 kwt.overwrite(ctx, modified, True, True)
888 kwt.overwrite(ctx, modified, True, True)
889 kwt.overwrite(ctx, added, True, False)
889 kwt.overwrite(ctx, added, True, False)
890 return ret
890 return ret
891 finally:
891 finally:
892 kwt.restrict = origrestrict
892 kwt.restrict = origrestrict
893
893
894 repo.__class__ = kwrepo
894 repo.__class__ = kwrepo
895 repo._keywordkwt = kwt
895 repo._keywordkwt = kwt
General Comments 0
You need to be logged in to leave comments. Login now