##// END OF EJS Templates
dirstate: use the `changing_files` context in the `keyword` demo...
marmoute -
r50936:1346db77 default
parent child Browse files
Show More
@@ -1,895 +1,896 b''
1 # keyword.py - $Keyword$ expansion for Mercurial
1 # keyword.py - $Keyword$ expansion for Mercurial
2 #
2 #
3 # Copyright 2007-2015 Christian Ebert <blacktrash@gmx.net>
3 # Copyright 2007-2015 Christian Ebert <blacktrash@gmx.net>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 #
7 #
8 # $Id$
8 # $Id$
9 #
9 #
10 # Keyword expansion hack against the grain of a Distributed SCM
10 # Keyword expansion hack against the grain of a Distributed SCM
11 #
11 #
12 # There are many good reasons why this is not needed in a distributed
12 # There are many good reasons why this is not needed in a distributed
13 # SCM, still it may be useful in very small projects based on single
13 # SCM, still it may be useful in very small projects based on single
14 # files (like LaTeX packages), that are mostly addressed to an
14 # files (like LaTeX packages), that are mostly addressed to an
15 # audience not running a version control system.
15 # audience not running a version control system.
16 #
16 #
17 # For in-depth discussion refer to
17 # For in-depth discussion refer to
18 # <https://mercurial-scm.org/wiki/KeywordPlan>.
18 # <https://mercurial-scm.org/wiki/KeywordPlan>.
19 #
19 #
20 # Keyword expansion is based on Mercurial's changeset template mappings.
20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 #
21 #
22 # Binary files are not touched.
22 # Binary files are not touched.
23 #
23 #
24 # Files to act upon/ignore are specified in the [keyword] section.
24 # Files to act upon/ignore are specified in the [keyword] section.
25 # Customized keyword template mappings in the [keywordmaps] section.
25 # Customized keyword template mappings in the [keywordmaps] section.
26 #
26 #
27 # Run 'hg help keyword' and 'hg kwdemo' to get info on configuration.
27 # Run 'hg help keyword' and 'hg kwdemo' to get info on configuration.
28
28
29 '''expand keywords in tracked files
29 '''expand keywords in tracked files
30
30
31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
32 tracked text files selected by your configuration.
32 tracked text files selected by your configuration.
33
33
34 Keywords are only expanded in local repositories and not stored in the
34 Keywords are only expanded in local repositories and not stored in the
35 change history. The mechanism can be regarded as a convenience for the
35 change history. The mechanism can be regarded as a convenience for the
36 current user or for archive distribution.
36 current user or for archive distribution.
37
37
38 Keywords expand to the changeset data pertaining to the latest change
38 Keywords expand to the changeset data pertaining to the latest change
39 relative to the working directory parent of each file.
39 relative to the working directory parent of each file.
40
40
41 Configuration is done in the [keyword], [keywordset] and [keywordmaps]
41 Configuration is done in the [keyword], [keywordset] and [keywordmaps]
42 sections of hgrc files.
42 sections of hgrc files.
43
43
44 Example::
44 Example::
45
45
46 [keyword]
46 [keyword]
47 # expand keywords in every python file except those matching "x*"
47 # expand keywords in every python file except those matching "x*"
48 **.py =
48 **.py =
49 x* = ignore
49 x* = ignore
50
50
51 [keywordset]
51 [keywordset]
52 # prefer svn- over cvs-like default keywordmaps
52 # prefer svn- over cvs-like default keywordmaps
53 svn = True
53 svn = True
54
54
55 .. note::
55 .. note::
56
56
57 The more specific you are in your filename patterns the less you
57 The more specific you are in your filename patterns the less you
58 lose speed in huge repositories.
58 lose speed in huge repositories.
59
59
60 For [keywordmaps] template mapping and expansion demonstration and
60 For [keywordmaps] template mapping and expansion demonstration and
61 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
61 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
62 available templates and filters.
62 available templates and filters.
63
63
64 Three additional date template filters are provided:
64 Three additional date template filters are provided:
65
65
66 :``utcdate``: "2006/09/18 15:13:13"
66 :``utcdate``: "2006/09/18 15:13:13"
67 :``svnutcdate``: "2006-09-18 15:13:13Z"
67 :``svnutcdate``: "2006-09-18 15:13:13Z"
68 :``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
68 :``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
69
69
70 The default template mappings (view with :hg:`kwdemo -d`) can be
70 The default template mappings (view with :hg:`kwdemo -d`) can be
71 replaced with customized keywords and templates. Again, run
71 replaced with customized keywords and templates. Again, run
72 :hg:`kwdemo` to control the results of your configuration changes.
72 :hg:`kwdemo` to control the results of your configuration changes.
73
73
74 Before changing/disabling active keywords, you must run :hg:`kwshrink`
74 Before changing/disabling active keywords, you must run :hg:`kwshrink`
75 to avoid storing expanded keywords in the change history.
75 to avoid storing expanded keywords in the change history.
76
76
77 To force expansion after enabling it, or a configuration change, run
77 To force expansion after enabling it, or a configuration change, run
78 :hg:`kwexpand`.
78 :hg:`kwexpand`.
79
79
80 Expansions spanning more than one line and incremental expansions,
80 Expansions spanning more than one line and incremental expansions,
81 like CVS' $Log$, are not supported. A keyword template map "Log =
81 like CVS' $Log$, are not supported. A keyword template map "Log =
82 {desc}" expands to the first line of the changeset description.
82 {desc}" expands to the first line of the changeset description.
83 '''
83 '''
84
84
85
85
86 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():
440 with repo.wlock():
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 repo.dirstate.write(repo.currenttransaction())
445 repo.dirstate.write(repo.currenttransaction())
446
446
447
447
448 @command(
448 @command(
449 b'kwdemo',
449 b'kwdemo',
450 [
450 [
451 (b'd', b'default', None, _(b'show default keyword template maps')),
451 (b'd', b'default', None, _(b'show default keyword template maps')),
452 (b'f', b'rcfile', b'', _(b'read maps from rcfile'), _(b'FILE')),
452 (b'f', b'rcfile', b'', _(b'read maps from rcfile'), _(b'FILE')),
453 ],
453 ],
454 _(b'hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'),
454 _(b'hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'),
455 optionalrepo=True,
455 optionalrepo=True,
456 )
456 )
457 def demo(ui, repo, *args, **opts):
457 def demo(ui, repo, *args, **opts):
458 """print [keywordmaps] configuration and an expansion example
458 """print [keywordmaps] configuration and an expansion example
459
459
460 Show current, custom, or default keyword template maps and their
460 Show current, custom, or default keyword template maps and their
461 expansions.
461 expansions.
462
462
463 Extend the current configuration by specifying maps as arguments
463 Extend the current configuration by specifying maps as arguments
464 and using -f/--rcfile to source an external hgrc file.
464 and using -f/--rcfile to source an external hgrc file.
465
465
466 Use -d/--default to disable current configuration.
466 Use -d/--default to disable current configuration.
467
467
468 See :hg:`help templates` for information on templates and filters.
468 See :hg:`help templates` for information on templates and filters.
469 """
469 """
470
470
471 def demoitems(section, items):
471 def demoitems(section, items):
472 ui.write(b'[%s]\n' % section)
472 ui.write(b'[%s]\n' % section)
473 for k, v in sorted(items):
473 for k, v in sorted(items):
474 if isinstance(v, bool):
474 if isinstance(v, bool):
475 v = stringutil.pprint(v)
475 v = stringutil.pprint(v)
476 ui.write(b'%s = %s\n' % (k, v))
476 ui.write(b'%s = %s\n' % (k, v))
477
477
478 fn = b'demo.txt'
478 fn = b'demo.txt'
479 tmpdir = pycompat.mkdtemp(b'', b'kwdemo.')
479 tmpdir = pycompat.mkdtemp(b'', b'kwdemo.')
480 ui.note(_(b'creating temporary repository at %s\n') % tmpdir)
480 ui.note(_(b'creating temporary repository at %s\n') % tmpdir)
481 if repo is None:
481 if repo is None:
482 baseui = ui
482 baseui = ui
483 else:
483 else:
484 baseui = repo.baseui
484 baseui = repo.baseui
485 repo = localrepo.instance(baseui, tmpdir, create=True)
485 repo = localrepo.instance(baseui, tmpdir, create=True)
486 ui.setconfig(b'keyword', fn, b'', b'keyword')
486 ui.setconfig(b'keyword', fn, b'', b'keyword')
487 svn = ui.configbool(b'keywordset', b'svn')
487 svn = ui.configbool(b'keywordset', b'svn')
488 # explicitly set keywordset for demo output
488 # explicitly set keywordset for demo output
489 ui.setconfig(b'keywordset', b'svn', svn, b'keyword')
489 ui.setconfig(b'keywordset', b'svn', svn, b'keyword')
490
490
491 uikwmaps = ui.configitems(b'keywordmaps')
491 uikwmaps = ui.configitems(b'keywordmaps')
492 if args or opts.get('rcfile'):
492 if args or opts.get('rcfile'):
493 ui.status(_(b'\n\tconfiguration using custom keyword template maps\n'))
493 ui.status(_(b'\n\tconfiguration using custom keyword template maps\n'))
494 if uikwmaps:
494 if uikwmaps:
495 ui.status(_(b'\textending current template maps\n'))
495 ui.status(_(b'\textending current template maps\n'))
496 if opts.get('default') or not uikwmaps:
496 if opts.get('default') or not uikwmaps:
497 if svn:
497 if svn:
498 ui.status(_(b'\toverriding default svn keywordset\n'))
498 ui.status(_(b'\toverriding default svn keywordset\n'))
499 else:
499 else:
500 ui.status(_(b'\toverriding default cvs keywordset\n'))
500 ui.status(_(b'\toverriding default cvs keywordset\n'))
501 if opts.get('rcfile'):
501 if opts.get('rcfile'):
502 ui.readconfig(opts.get(b'rcfile'))
502 ui.readconfig(opts.get(b'rcfile'))
503 if args:
503 if args:
504 # simulate hgrc parsing
504 # simulate hgrc parsing
505 rcmaps = b'[keywordmaps]\n%s\n' % b'\n'.join(args)
505 rcmaps = b'[keywordmaps]\n%s\n' % b'\n'.join(args)
506 repo.vfs.write(b'hgrc', rcmaps)
506 repo.vfs.write(b'hgrc', rcmaps)
507 ui.readconfig(repo.vfs.join(b'hgrc'))
507 ui.readconfig(repo.vfs.join(b'hgrc'))
508 kwmaps = dict(ui.configitems(b'keywordmaps'))
508 kwmaps = dict(ui.configitems(b'keywordmaps'))
509 elif opts.get('default'):
509 elif opts.get('default'):
510 if svn:
510 if svn:
511 ui.status(_(b'\n\tconfiguration using default svn keywordset\n'))
511 ui.status(_(b'\n\tconfiguration using default svn keywordset\n'))
512 else:
512 else:
513 ui.status(_(b'\n\tconfiguration using default cvs keywordset\n'))
513 ui.status(_(b'\n\tconfiguration using default cvs keywordset\n'))
514 kwmaps = _defaultkwmaps(ui)
514 kwmaps = _defaultkwmaps(ui)
515 if uikwmaps:
515 if uikwmaps:
516 ui.status(_(b'\tdisabling current template maps\n'))
516 ui.status(_(b'\tdisabling current template maps\n'))
517 for k, v in kwmaps.items():
517 for k, v in kwmaps.items():
518 ui.setconfig(b'keywordmaps', k, v, b'keyword')
518 ui.setconfig(b'keywordmaps', k, v, b'keyword')
519 else:
519 else:
520 ui.status(_(b'\n\tconfiguration using current keyword template maps\n'))
520 ui.status(_(b'\n\tconfiguration using current keyword template maps\n'))
521 if uikwmaps:
521 if uikwmaps:
522 kwmaps = dict(uikwmaps)
522 kwmaps = dict(uikwmaps)
523 else:
523 else:
524 kwmaps = _defaultkwmaps(ui)
524 kwmaps = _defaultkwmaps(ui)
525
525
526 uisetup(ui)
526 uisetup(ui)
527 reposetup(ui, repo)
527 reposetup(ui, repo)
528 ui.writenoi18n(b'[extensions]\nkeyword =\n')
528 ui.writenoi18n(b'[extensions]\nkeyword =\n')
529 demoitems(b'keyword', ui.configitems(b'keyword'))
529 demoitems(b'keyword', ui.configitems(b'keyword'))
530 demoitems(b'keywordset', ui.configitems(b'keywordset'))
530 demoitems(b'keywordset', ui.configitems(b'keywordset'))
531 demoitems(b'keywordmaps', kwmaps.items())
531 demoitems(b'keywordmaps', kwmaps.items())
532 keywords = b'$' + b'$\n$'.join(sorted(kwmaps.keys())) + b'$\n'
532 keywords = b'$' + b'$\n$'.join(sorted(kwmaps.keys())) + b'$\n'
533 repo.wvfs.write(fn, keywords)
533 repo.wvfs.write(fn, keywords)
534 with repo.wlock():
534 with repo.wlock():
535 repo[None].add([fn])
535 with repo.dirstate.changing_files(repo):
536 repo[None].add([fn])
536 ui.note(_(b'\nkeywords written to %s:\n') % fn)
537 ui.note(_(b'\nkeywords written to %s:\n') % fn)
537 ui.note(keywords)
538 ui.note(keywords)
538 repo.dirstate.setbranch(b'demobranch')
539 repo.dirstate.setbranch(b'demobranch')
539 for name, cmd in ui.configitems(b'hooks'):
540 for name, cmd in ui.configitems(b'hooks'):
540 if name.split(b'.', 1)[0].find(b'commit') > -1:
541 if name.split(b'.', 1)[0].find(b'commit') > -1:
541 repo.ui.setconfig(b'hooks', name, b'', b'keyword')
542 repo.ui.setconfig(b'hooks', name, b'', b'keyword')
542 msg = _(b'hg keyword configuration and expansion example')
543 msg = _(b'hg keyword configuration and expansion example')
543 ui.note((b"hg ci -m '%s'\n" % msg))
544 ui.note((b"hg ci -m '%s'\n" % msg))
544 repo.commit(text=msg)
545 repo.commit(text=msg)
545 ui.status(_(b'\n\tkeywords expanded\n'))
546 ui.status(_(b'\n\tkeywords expanded\n'))
546 ui.write(repo.wread(fn))
547 ui.write(repo.wread(fn))
547 repo.wvfs.rmtree(repo.root)
548 repo.wvfs.rmtree(repo.root)
548
549
549
550
550 @command(
551 @command(
551 b'kwexpand',
552 b'kwexpand',
552 cmdutil.walkopts,
553 cmdutil.walkopts,
553 _(b'hg kwexpand [OPTION]... [FILE]...'),
554 _(b'hg kwexpand [OPTION]... [FILE]...'),
554 inferrepo=True,
555 inferrepo=True,
555 )
556 )
556 def expand(ui, repo, *pats, **opts):
557 def expand(ui, repo, *pats, **opts):
557 """expand keywords in the working directory
558 """expand keywords in the working directory
558
559
559 Run after (re)enabling keyword expansion.
560 Run after (re)enabling keyword expansion.
560
561
561 kwexpand refuses to run if given files contain local changes.
562 kwexpand refuses to run if given files contain local changes.
562 """
563 """
563 # 3rd argument sets expansion to True
564 # 3rd argument sets expansion to True
564 _kwfwrite(ui, repo, True, *pats, **opts)
565 _kwfwrite(ui, repo, True, *pats, **opts)
565
566
566
567
567 @command(
568 @command(
568 b'kwfiles',
569 b'kwfiles',
569 [
570 [
570 (b'A', b'all', None, _(b'show keyword status flags of all files')),
571 (b'A', b'all', None, _(b'show keyword status flags of all files')),
571 (b'i', b'ignore', None, _(b'show files excluded from expansion')),
572 (b'i', b'ignore', None, _(b'show files excluded from expansion')),
572 (b'u', b'unknown', None, _(b'only show unknown (not tracked) files')),
573 (b'u', b'unknown', None, _(b'only show unknown (not tracked) files')),
573 ]
574 ]
574 + cmdutil.walkopts,
575 + cmdutil.walkopts,
575 _(b'hg kwfiles [OPTION]... [FILE]...'),
576 _(b'hg kwfiles [OPTION]... [FILE]...'),
576 inferrepo=True,
577 inferrepo=True,
577 )
578 )
578 def files(ui, repo, *pats, **opts):
579 def files(ui, repo, *pats, **opts):
579 """show files configured for keyword expansion
580 """show files configured for keyword expansion
580
581
581 List which files in the working directory are matched by the
582 List which files in the working directory are matched by the
582 [keyword] configuration patterns.
583 [keyword] configuration patterns.
583
584
584 Useful to prevent inadvertent keyword expansion and to speed up
585 Useful to prevent inadvertent keyword expansion and to speed up
585 execution by including only files that are actual candidates for
586 execution by including only files that are actual candidates for
586 expansion.
587 expansion.
587
588
588 See :hg:`help keyword` on how to construct patterns both for
589 See :hg:`help keyword` on how to construct patterns both for
589 inclusion and exclusion of files.
590 inclusion and exclusion of files.
590
591
591 With -A/--all and -v/--verbose the codes used to show the status
592 With -A/--all and -v/--verbose the codes used to show the status
592 of files are::
593 of files are::
593
594
594 K = keyword expansion candidate
595 K = keyword expansion candidate
595 k = keyword expansion candidate (not tracked)
596 k = keyword expansion candidate (not tracked)
596 I = ignored
597 I = ignored
597 i = ignored (not tracked)
598 i = ignored (not tracked)
598 """
599 """
599 kwt = getattr(repo, '_keywordkwt', None)
600 kwt = getattr(repo, '_keywordkwt', None)
600 wctx = repo[None]
601 wctx = repo[None]
601 status = _status(ui, repo, wctx, kwt, *pats, **opts)
602 status = _status(ui, repo, wctx, kwt, *pats, **opts)
602 if pats:
603 if pats:
603 cwd = repo.getcwd()
604 cwd = repo.getcwd()
604 else:
605 else:
605 cwd = b''
606 cwd = b''
606 files = []
607 files = []
607 opts = pycompat.byteskwargs(opts)
608 opts = pycompat.byteskwargs(opts)
608 if not opts.get(b'unknown') or opts.get(b'all'):
609 if not opts.get(b'unknown') or opts.get(b'all'):
609 files = sorted(status.modified + status.added + status.clean)
610 files = sorted(status.modified + status.added + status.clean)
610 kwfiles = kwt.iskwfile(files, wctx)
611 kwfiles = kwt.iskwfile(files, wctx)
611 kwdeleted = kwt.iskwfile(status.deleted, wctx)
612 kwdeleted = kwt.iskwfile(status.deleted, wctx)
612 kwunknown = kwt.iskwfile(status.unknown, wctx)
613 kwunknown = kwt.iskwfile(status.unknown, wctx)
613 if not opts.get(b'ignore') or opts.get(b'all'):
614 if not opts.get(b'ignore') or opts.get(b'all'):
614 showfiles = kwfiles, kwdeleted, kwunknown
615 showfiles = kwfiles, kwdeleted, kwunknown
615 else:
616 else:
616 showfiles = [], [], []
617 showfiles = [], [], []
617 if opts.get(b'all') or opts.get(b'ignore'):
618 if opts.get(b'all') or opts.get(b'ignore'):
618 showfiles += (
619 showfiles += (
619 [f for f in files if f not in kwfiles],
620 [f for f in files if f not in kwfiles],
620 [f for f in status.unknown if f not in kwunknown],
621 [f for f in status.unknown if f not in kwunknown],
621 )
622 )
622 kwlabels = b'enabled deleted enabledunknown ignored ignoredunknown'.split()
623 kwlabels = b'enabled deleted enabledunknown ignored ignoredunknown'.split()
623 kwstates = zip(kwlabels, pycompat.bytestr(b'K!kIi'), showfiles)
624 kwstates = zip(kwlabels, pycompat.bytestr(b'K!kIi'), showfiles)
624 fm = ui.formatter(b'kwfiles', opts)
625 fm = ui.formatter(b'kwfiles', opts)
625 fmt = b'%.0s%s\n'
626 fmt = b'%.0s%s\n'
626 if opts.get(b'all') or ui.verbose:
627 if opts.get(b'all') or ui.verbose:
627 fmt = b'%s %s\n'
628 fmt = b'%s %s\n'
628 for kwstate, char, filenames in kwstates:
629 for kwstate, char, filenames in kwstates:
629 label = b'kwfiles.' + kwstate
630 label = b'kwfiles.' + kwstate
630 for f in filenames:
631 for f in filenames:
631 fm.startitem()
632 fm.startitem()
632 fm.data(kwstatus=char, path=f)
633 fm.data(kwstatus=char, path=f)
633 fm.plain(fmt % (char, repo.pathto(f, cwd)), label=label)
634 fm.plain(fmt % (char, repo.pathto(f, cwd)), label=label)
634 fm.end()
635 fm.end()
635
636
636
637
637 @command(
638 @command(
638 b'kwshrink',
639 b'kwshrink',
639 cmdutil.walkopts,
640 cmdutil.walkopts,
640 _(b'hg kwshrink [OPTION]... [FILE]...'),
641 _(b'hg kwshrink [OPTION]... [FILE]...'),
641 inferrepo=True,
642 inferrepo=True,
642 )
643 )
643 def shrink(ui, repo, *pats, **opts):
644 def shrink(ui, repo, *pats, **opts):
644 """revert expanded keywords in the working directory
645 """revert expanded keywords in the working directory
645
646
646 Must be run before changing/disabling active keywords.
647 Must be run before changing/disabling active keywords.
647
648
648 kwshrink refuses to run if given files contain local changes.
649 kwshrink refuses to run if given files contain local changes.
649 """
650 """
650 # 3rd argument sets expansion to False
651 # 3rd argument sets expansion to False
651 _kwfwrite(ui, repo, False, *pats, **opts)
652 _kwfwrite(ui, repo, False, *pats, **opts)
652
653
653
654
654 # monkeypatches
655 # monkeypatches
655
656
656
657
657 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
658 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
658 """Monkeypatch/wrap patch.patchfile.__init__ to avoid
659 """Monkeypatch/wrap patch.patchfile.__init__ to avoid
659 rejects or conflicts due to expanded keywords in working dir."""
660 rejects or conflicts due to expanded keywords in working dir."""
660 orig(self, ui, gp, backend, store, eolmode)
661 orig(self, ui, gp, backend, store, eolmode)
661 kwt = getattr(getattr(backend, 'repo', None), '_keywordkwt', None)
662 kwt = getattr(getattr(backend, 'repo', None), '_keywordkwt', None)
662 if kwt:
663 if kwt:
663 # shrink keywords read from working dir
664 # shrink keywords read from working dir
664 self.lines = kwt.shrinklines(self.fname, self.lines)
665 self.lines = kwt.shrinklines(self.fname, self.lines)
665
666
666
667
667 def kwdiff(orig, repo, *args, **kwargs):
668 def kwdiff(orig, repo, *args, **kwargs):
668 '''Monkeypatch patch.diff to avoid expansion.'''
669 '''Monkeypatch patch.diff to avoid expansion.'''
669 kwt = getattr(repo, '_keywordkwt', None)
670 kwt = getattr(repo, '_keywordkwt', None)
670 if kwt:
671 if kwt:
671 restrict = kwt.restrict
672 restrict = kwt.restrict
672 kwt.restrict = True
673 kwt.restrict = True
673 try:
674 try:
674 for chunk in orig(repo, *args, **kwargs):
675 for chunk in orig(repo, *args, **kwargs):
675 yield chunk
676 yield chunk
676 finally:
677 finally:
677 if kwt:
678 if kwt:
678 kwt.restrict = restrict
679 kwt.restrict = restrict
679
680
680
681
681 def kwweb_skip(orig, web):
682 def kwweb_skip(orig, web):
682 '''Wraps webcommands.x turning off keyword expansion.'''
683 '''Wraps webcommands.x turning off keyword expansion.'''
683 kwt = getattr(web.repo, '_keywordkwt', None)
684 kwt = getattr(web.repo, '_keywordkwt', None)
684 if kwt:
685 if kwt:
685 origmatch = kwt.match
686 origmatch = kwt.match
686 kwt.match = util.never
687 kwt.match = util.never
687 try:
688 try:
688 for chunk in orig(web):
689 for chunk in orig(web):
689 yield chunk
690 yield chunk
690 finally:
691 finally:
691 if kwt:
692 if kwt:
692 kwt.match = origmatch
693 kwt.match = origmatch
693
694
694
695
695 def kw_amend(orig, ui, repo, old, extra, pats, opts):
696 def kw_amend(orig, ui, repo, old, extra, pats, opts):
696 '''Wraps cmdutil.amend expanding keywords after amend.'''
697 '''Wraps cmdutil.amend expanding keywords after amend.'''
697 kwt = getattr(repo, '_keywordkwt', None)
698 kwt = getattr(repo, '_keywordkwt', None)
698 if kwt is None:
699 if kwt is None:
699 return orig(ui, repo, old, extra, pats, opts)
700 return orig(ui, repo, old, extra, pats, opts)
700 with repo.wlock(), repo.dirstate.changing_parents(repo):
701 with repo.wlock(), repo.dirstate.changing_parents(repo):
701 kwt.postcommit = True
702 kwt.postcommit = True
702 newid = orig(ui, repo, old, extra, pats, opts)
703 newid = orig(ui, repo, old, extra, pats, opts)
703 if newid != old.node():
704 if newid != old.node():
704 ctx = repo[newid]
705 ctx = repo[newid]
705 kwt.restrict = True
706 kwt.restrict = True
706 kwt.overwrite(ctx, ctx.files(), False, True)
707 kwt.overwrite(ctx, ctx.files(), False, True)
707 kwt.restrict = False
708 kwt.restrict = False
708 return newid
709 return newid
709
710
710
711
711 def kw_copy(orig, ui, repo, pats, opts, rename=False):
712 def kw_copy(orig, ui, repo, pats, opts, rename=False):
712 """Wraps cmdutil.copy so that copy/rename destinations do not
713 """Wraps cmdutil.copy so that copy/rename destinations do not
713 contain expanded keywords.
714 contain expanded keywords.
714 Note that the source of a regular file destination may also be a
715 Note that the source of a regular file destination may also be a
715 symlink:
716 symlink:
716 hg cp sym x -> x is symlink
717 hg cp sym x -> x is symlink
717 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
718 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
718 For the latter we have to follow the symlink to find out whether its
719 For the latter we have to follow the symlink to find out whether its
719 target is configured for expansion and we therefore must unexpand the
720 target is configured for expansion and we therefore must unexpand the
720 keywords in the destination."""
721 keywords in the destination."""
721 kwt = getattr(repo, '_keywordkwt', None)
722 kwt = getattr(repo, '_keywordkwt', None)
722 if kwt is None:
723 if kwt is None:
723 return orig(ui, repo, pats, opts, rename)
724 return orig(ui, repo, pats, opts, rename)
724 with repo.wlock():
725 with repo.wlock():
725 orig(ui, repo, pats, opts, rename)
726 orig(ui, repo, pats, opts, rename)
726 if opts.get(b'dry_run'):
727 if opts.get(b'dry_run'):
727 return
728 return
728 wctx = repo[None]
729 wctx = repo[None]
729 cwd = repo.getcwd()
730 cwd = repo.getcwd()
730
731
731 def haskwsource(dest):
732 def haskwsource(dest):
732 """Returns true if dest is a regular file and configured for
733 """Returns true if dest is a regular file and configured for
733 expansion or a symlink which points to a file configured for
734 expansion or a symlink which points to a file configured for
734 expansion."""
735 expansion."""
735 source = repo.dirstate.copied(dest)
736 source = repo.dirstate.copied(dest)
736 if b'l' in wctx.flags(source):
737 if b'l' in wctx.flags(source):
737 source = pathutil.canonpath(
738 source = pathutil.canonpath(
738 repo.root, cwd, os.path.realpath(source)
739 repo.root, cwd, os.path.realpath(source)
739 )
740 )
740 return kwt.match(source)
741 return kwt.match(source)
741
742
742 candidates = [
743 candidates = [
743 f
744 f
744 for f in repo.dirstate.copies()
745 for f in repo.dirstate.copies()
745 if b'l' not in wctx.flags(f) and haskwsource(f)
746 if b'l' not in wctx.flags(f) and haskwsource(f)
746 ]
747 ]
747 kwt.overwrite(wctx, candidates, False, False)
748 kwt.overwrite(wctx, candidates, False, False)
748
749
749
750
750 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
751 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
751 '''Wraps record.dorecord expanding keywords after recording.'''
752 '''Wraps record.dorecord expanding keywords after recording.'''
752 kwt = getattr(repo, '_keywordkwt', None)
753 kwt = getattr(repo, '_keywordkwt', None)
753 if kwt is None:
754 if kwt is None:
754 return orig(ui, repo, commitfunc, *pats, **opts)
755 return orig(ui, repo, commitfunc, *pats, **opts)
755 with repo.wlock():
756 with repo.wlock():
756 # record returns 0 even when nothing has changed
757 # record returns 0 even when nothing has changed
757 # therefore compare nodes before and after
758 # therefore compare nodes before and after
758 kwt.postcommit = True
759 kwt.postcommit = True
759 ctx = repo[b'.']
760 ctx = repo[b'.']
760 wstatus = ctx.status()
761 wstatus = ctx.status()
761 ret = orig(ui, repo, commitfunc, *pats, **opts)
762 ret = orig(ui, repo, commitfunc, *pats, **opts)
762 recctx = repo[b'.']
763 recctx = repo[b'.']
763 if ctx != recctx:
764 if ctx != recctx:
764 modified, added = _preselect(wstatus, recctx.files())
765 modified, added = _preselect(wstatus, recctx.files())
765 kwt.restrict = False
766 kwt.restrict = False
766 with repo.dirstate.changing_parents(repo):
767 with repo.dirstate.changing_parents(repo):
767 kwt.overwrite(recctx, modified, False, True)
768 kwt.overwrite(recctx, modified, False, True)
768 kwt.overwrite(recctx, added, False, True, True)
769 kwt.overwrite(recctx, added, False, True, True)
769 kwt.restrict = True
770 kwt.restrict = True
770 return ret
771 return ret
771
772
772
773
773 def kwfilectx_cmp(orig, self, fctx):
774 def kwfilectx_cmp(orig, self, fctx):
774 if fctx._customcmp:
775 if fctx._customcmp:
775 return fctx.cmp(self)
776 return fctx.cmp(self)
776 kwt = getattr(self._repo, '_keywordkwt', None)
777 kwt = getattr(self._repo, '_keywordkwt', None)
777 if kwt is None:
778 if kwt is None:
778 return orig(self, fctx)
779 return orig(self, fctx)
779 # keyword affects data size, comparing wdir and filelog size does
780 # keyword affects data size, comparing wdir and filelog size does
780 # not make sense
781 # not make sense
781 if (
782 if (
782 fctx._filenode is None
783 fctx._filenode is None
783 and (
784 and (
784 self._repo._encodefilterpats
785 self._repo._encodefilterpats
785 or kwt.match(fctx.path())
786 or kwt.match(fctx.path())
786 and b'l' not in fctx.flags()
787 and b'l' not in fctx.flags()
787 or self.size() - 4 == fctx.size()
788 or self.size() - 4 == fctx.size()
788 )
789 )
789 or self.size() == fctx.size()
790 or self.size() == fctx.size()
790 ):
791 ):
791 return self._filelog.cmp(self._filenode, fctx.data())
792 return self._filelog.cmp(self._filenode, fctx.data())
792 return True
793 return True
793
794
794
795
795 def uisetup(ui):
796 def uisetup(ui):
796 """Monkeypatches dispatch._parse to retrieve user command.
797 """Monkeypatches dispatch._parse to retrieve user command.
797 Overrides file method to return kwfilelog instead of filelog
798 Overrides file method to return kwfilelog instead of filelog
798 if file matches user configuration.
799 if file matches user configuration.
799 Wraps commit to overwrite configured files with updated
800 Wraps commit to overwrite configured files with updated
800 keyword substitutions.
801 keyword substitutions.
801 Monkeypatches patch and webcommands."""
802 Monkeypatches patch and webcommands."""
802
803
803 def kwdispatch_parse(orig, ui, args):
804 def kwdispatch_parse(orig, ui, args):
804 '''Monkeypatch dispatch._parse to obtain running hg command.'''
805 '''Monkeypatch dispatch._parse to obtain running hg command.'''
805 cmd, func, args, options, cmdoptions = orig(ui, args)
806 cmd, func, args, options, cmdoptions = orig(ui, args)
806 kwtools[b'hgcmd'] = cmd
807 kwtools[b'hgcmd'] = cmd
807 return cmd, func, args, options, cmdoptions
808 return cmd, func, args, options, cmdoptions
808
809
809 extensions.wrapfunction(dispatch, b'_parse', kwdispatch_parse)
810 extensions.wrapfunction(dispatch, b'_parse', kwdispatch_parse)
810
811
811 extensions.wrapfunction(context.filectx, b'cmp', kwfilectx_cmp)
812 extensions.wrapfunction(context.filectx, b'cmp', kwfilectx_cmp)
812 extensions.wrapfunction(patch.patchfile, b'__init__', kwpatchfile_init)
813 extensions.wrapfunction(patch.patchfile, b'__init__', kwpatchfile_init)
813 extensions.wrapfunction(patch, b'diff', kwdiff)
814 extensions.wrapfunction(patch, b'diff', kwdiff)
814 extensions.wrapfunction(cmdutil, b'amend', kw_amend)
815 extensions.wrapfunction(cmdutil, b'amend', kw_amend)
815 extensions.wrapfunction(cmdutil, b'copy', kw_copy)
816 extensions.wrapfunction(cmdutil, b'copy', kw_copy)
816 extensions.wrapfunction(cmdutil, b'dorecord', kw_dorecord)
817 extensions.wrapfunction(cmdutil, b'dorecord', kw_dorecord)
817 for c in nokwwebcommands.split():
818 for c in nokwwebcommands.split():
818 extensions.wrapfunction(webcommands, c, kwweb_skip)
819 extensions.wrapfunction(webcommands, c, kwweb_skip)
819
820
820
821
821 def reposetup(ui, repo):
822 def reposetup(ui, repo):
822 '''Sets up repo as kwrepo for keyword substitution.'''
823 '''Sets up repo as kwrepo for keyword substitution.'''
823
824
824 try:
825 try:
825 if (
826 if (
826 not repo.local()
827 not repo.local()
827 or kwtools[b'hgcmd'] in nokwcommands.split()
828 or kwtools[b'hgcmd'] in nokwcommands.split()
828 or b'.hg' in util.splitpath(repo.root)
829 or b'.hg' in util.splitpath(repo.root)
829 or repo._url.startswith(b'bundle:')
830 or repo._url.startswith(b'bundle:')
830 ):
831 ):
831 return
832 return
832 except AttributeError:
833 except AttributeError:
833 pass
834 pass
834
835
835 inc, exc = [], [b'.hg*']
836 inc, exc = [], [b'.hg*']
836 for pat, opt in ui.configitems(b'keyword'):
837 for pat, opt in ui.configitems(b'keyword'):
837 if opt != b'ignore':
838 if opt != b'ignore':
838 inc.append(pat)
839 inc.append(pat)
839 else:
840 else:
840 exc.append(pat)
841 exc.append(pat)
841 if not inc:
842 if not inc:
842 return
843 return
843
844
844 kwt = kwtemplater(ui, repo, inc, exc)
845 kwt = kwtemplater(ui, repo, inc, exc)
845
846
846 class kwrepo(repo.__class__):
847 class kwrepo(repo.__class__):
847 def file(self, f):
848 def file(self, f):
848 if f[0] == b'/':
849 if f[0] == b'/':
849 f = f[1:]
850 f = f[1:]
850 return kwfilelog(self.svfs, kwt, f)
851 return kwfilelog(self.svfs, kwt, f)
851
852
852 def wread(self, filename):
853 def wread(self, filename):
853 data = super(kwrepo, self).wread(filename)
854 data = super(kwrepo, self).wread(filename)
854 return kwt.wread(filename, data)
855 return kwt.wread(filename, data)
855
856
856 def commit(self, *args, **opts):
857 def commit(self, *args, **opts):
857 # use custom commitctx for user commands
858 # use custom commitctx for user commands
858 # other extensions can still wrap repo.commitctx directly
859 # other extensions can still wrap repo.commitctx directly
859 self.commitctx = self.kwcommitctx
860 self.commitctx = self.kwcommitctx
860 try:
861 try:
861 return super(kwrepo, self).commit(*args, **opts)
862 return super(kwrepo, self).commit(*args, **opts)
862 finally:
863 finally:
863 del self.commitctx
864 del self.commitctx
864
865
865 def kwcommitctx(self, ctx, error=False, origctx=None):
866 def kwcommitctx(self, ctx, error=False, origctx=None):
866 n = super(kwrepo, self).commitctx(ctx, error, origctx)
867 n = super(kwrepo, self).commitctx(ctx, error, origctx)
867 # no lock needed, only called from repo.commit() which already locks
868 # no lock needed, only called from repo.commit() which already locks
868 if not kwt.postcommit:
869 if not kwt.postcommit:
869 restrict = kwt.restrict
870 restrict = kwt.restrict
870 kwt.restrict = True
871 kwt.restrict = True
871 kwt.overwrite(
872 kwt.overwrite(
872 self[n], sorted(ctx.added() + ctx.modified()), False, True
873 self[n], sorted(ctx.added() + ctx.modified()), False, True
873 )
874 )
874 kwt.restrict = restrict
875 kwt.restrict = restrict
875 return n
876 return n
876
877
877 def rollback(self, dryrun=False, force=False):
878 def rollback(self, dryrun=False, force=False):
878 with self.wlock():
879 with self.wlock():
879 origrestrict = kwt.restrict
880 origrestrict = kwt.restrict
880 try:
881 try:
881 if not dryrun:
882 if not dryrun:
882 changed = self[b'.'].files()
883 changed = self[b'.'].files()
883 ret = super(kwrepo, self).rollback(dryrun, force)
884 ret = super(kwrepo, self).rollback(dryrun, force)
884 if not dryrun:
885 if not dryrun:
885 ctx = self[b'.']
886 ctx = self[b'.']
886 modified, added = _preselect(ctx.status(), changed)
887 modified, added = _preselect(ctx.status(), changed)
887 kwt.restrict = False
888 kwt.restrict = False
888 kwt.overwrite(ctx, modified, True, True)
889 kwt.overwrite(ctx, modified, True, True)
889 kwt.overwrite(ctx, added, True, False)
890 kwt.overwrite(ctx, added, True, False)
890 return ret
891 return ret
891 finally:
892 finally:
892 kwt.restrict = origrestrict
893 kwt.restrict = origrestrict
893
894
894 repo.__class__ = kwrepo
895 repo.__class__ = kwrepo
895 repo._keywordkwt = kwt
896 repo._keywordkwt = kwt
General Comments 0
You need to be logged in to leave comments. Login now