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