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