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