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