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