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