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