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