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