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