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