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