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