##// END OF EJS Templates
keywords: build a new templater to work around caching interaction
Matt Mackall -
r10844:6722ba3b default
parent child Browse files
Show More
@@ -1,521 +1,521 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 Configuration is done in the [keyword] and [keywordmaps] sections of
38 Configuration is done in the [keyword] and [keywordmaps] sections of
39 hgrc files.
39 hgrc files.
40
40
41 Example::
41 Example::
42
42
43 [keyword]
43 [keyword]
44 # expand keywords in every python file except those matching "x*"
44 # expand keywords in every python file except those matching "x*"
45 **.py =
45 **.py =
46 x* = ignore
46 x* = ignore
47
47
48 NOTE: the more specific you are in your filename patterns the less you
48 NOTE: the more specific you are in your filename patterns the less you
49 lose speed in huge repositories.
49 lose speed in huge repositories.
50
50
51 For [keywordmaps] template mapping and expansion demonstration and
51 For [keywordmaps] template mapping and expansion demonstration and
52 control run "hg kwdemo". See "hg help templates" for a list of
52 control run "hg kwdemo". See "hg help templates" for a list of
53 available templates and filters.
53 available templates and filters.
54
54
55 An additional date template filter {date|utcdate} is provided. It
55 An additional date template filter {date|utcdate} is provided. It
56 returns a date like "2006/09/18 15:13:13".
56 returns a date like "2006/09/18 15:13:13".
57
57
58 The default template mappings (view with "hg kwdemo -d") can be
58 The default template mappings (view with "hg kwdemo -d") can be
59 replaced with customized keywords and templates. Again, run "hg
59 replaced with customized keywords and templates. Again, run "hg
60 kwdemo" to control the results of your config changes.
60 kwdemo" to control the results of your config changes.
61
61
62 Before changing/disabling active keywords, run "hg kwshrink" to avoid
62 Before changing/disabling active keywords, run "hg kwshrink" to avoid
63 the risk of inadvertently storing expanded keywords in the change
63 the risk of inadvertently storing expanded keywords in the change
64 history.
64 history.
65
65
66 To force expansion after enabling it, or a configuration change, run
66 To force expansion after enabling it, or a configuration change, run
67 "hg kwexpand".
67 "hg kwexpand".
68
68
69 Also, when committing with the record extension or using mq's qrecord,
69 Also, when committing with the record extension or using mq's qrecord,
70 be aware that keywords cannot be updated. Again, run "hg kwexpand" on
70 be aware that keywords cannot be updated. Again, run "hg kwexpand" on
71 the files in question to update keyword expansions after all changes
71 the files in question to update keyword expansions after all changes
72 have been checked in.
72 have been checked in.
73
73
74 Expansions spanning more than one line and incremental expansions,
74 Expansions spanning more than one line and incremental expansions,
75 like CVS' $Log$, are not supported. A keyword template map "Log =
75 like CVS' $Log$, are not supported. A keyword template map "Log =
76 {desc}" expands to the first line of the changeset description.
76 {desc}" expands to the first line of the changeset description.
77 '''
77 '''
78
78
79 from mercurial import commands, cmdutil, dispatch, filelog, revlog, extensions
79 from mercurial import commands, cmdutil, dispatch, filelog, revlog, extensions
80 from mercurial import patch, localrepo, templater, templatefilters, util, match
80 from mercurial import patch, localrepo, templater, templatefilters, util, match
81 from mercurial.hgweb import webcommands
81 from mercurial.hgweb import webcommands
82 from mercurial.node import nullid
82 from mercurial.node import nullid
83 from mercurial.i18n import _
83 from mercurial.i18n import _
84 import re, shutil, tempfile
84 import re, shutil, tempfile
85
85
86 commands.optionalrepo += ' kwdemo'
86 commands.optionalrepo += ' kwdemo'
87
87
88 # hg commands that do not act on keywords
88 # hg commands that do not act on keywords
89 nokwcommands = ('add addremove annotate bundle copy export grep incoming init'
89 nokwcommands = ('add addremove annotate bundle copy export grep incoming init'
90 ' log outgoing push rename rollback tip verify'
90 ' log outgoing push rename rollback tip verify'
91 ' convert email glog')
91 ' convert email glog')
92
92
93 # hg commands that trigger expansion only when writing to working dir,
93 # hg commands that trigger expansion only when writing to working dir,
94 # not when reading filelog, and unexpand when reading from working dir
94 # not when reading filelog, and unexpand when reading from working dir
95 restricted = ('merge record resolve qfold qimport qnew qpush qrefresh qrecord'
95 restricted = ('merge record resolve qfold qimport qnew qpush qrefresh qrecord'
96 ' transplant')
96 ' transplant')
97
97
98 # provide cvs-like UTC date filter
98 # provide cvs-like UTC date filter
99 utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S')
99 utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S')
100
100
101 # make keyword tools accessible
101 # make keyword tools accessible
102 kwtools = {'templater': None, 'hgcmd': '', 'inc': [], 'exc': ['.hg*']}
102 kwtools = {'templater': None, 'hgcmd': '', 'inc': [], 'exc': ['.hg*']}
103
103
104
104
105 class kwtemplater(object):
105 class kwtemplater(object):
106 '''
106 '''
107 Sets up keyword templates, corresponding keyword regex, and
107 Sets up keyword templates, corresponding keyword regex, and
108 provides keyword substitution functions.
108 provides keyword substitution functions.
109 '''
109 '''
110 templates = {
110 templates = {
111 'Revision': '{node|short}',
111 'Revision': '{node|short}',
112 'Author': '{author|user}',
112 'Author': '{author|user}',
113 'Date': '{date|utcdate}',
113 'Date': '{date|utcdate}',
114 'RCSfile': '{file|basename},v',
114 'RCSfile': '{file|basename},v',
115 'RCSFile': '{file|basename},v', # kept for backwards compatibility
115 'RCSFile': '{file|basename},v', # kept for backwards compatibility
116 # with hg-keyword
116 # with hg-keyword
117 'Source': '{root}/{file},v',
117 'Source': '{root}/{file},v',
118 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
118 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
119 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
119 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
120 }
120 }
121
121
122 def __init__(self, ui, repo):
122 def __init__(self, ui, repo):
123 self.ui = ui
123 self.ui = ui
124 self.repo = repo
124 self.repo = repo
125 self.match = match.match(repo.root, '', [],
125 self.match = match.match(repo.root, '', [],
126 kwtools['inc'], kwtools['exc'])
126 kwtools['inc'], kwtools['exc'])
127 self.restrict = kwtools['hgcmd'] in restricted.split()
127 self.restrict = kwtools['hgcmd'] in restricted.split()
128
128
129 kwmaps = self.ui.configitems('keywordmaps')
129 kwmaps = self.ui.configitems('keywordmaps')
130 if kwmaps: # override default templates
130 if kwmaps: # override default templates
131 self.templates = dict((k, templater.parsestring(v, False))
131 self.templates = dict((k, templater.parsestring(v, False))
132 for k, v in kwmaps)
132 for k, v in kwmaps)
133 escaped = map(re.escape, self.templates.keys())
133 escaped = map(re.escape, self.templates.keys())
134 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
134 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
135 self.re_kw = re.compile(kwpat)
135 self.re_kw = re.compile(kwpat)
136
136
137 templatefilters.filters['utcdate'] = utcdate
137 templatefilters.filters['utcdate'] = utcdate
138 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
139 False, None, '', False)
140
138
141 def substitute(self, data, path, ctx, subfunc):
139 def substitute(self, data, path, ctx, subfunc):
142 '''Replaces keywords in data with expanded template.'''
140 '''Replaces keywords in data with expanded template.'''
143 def kwsub(mobj):
141 def kwsub(mobj):
144 kw = mobj.group(1)
142 kw = mobj.group(1)
143 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
144 False, None, '', False)
145 self.ct.use_template(self.templates[kw])
145 self.ct.use_template(self.templates[kw])
146 self.ui.pushbuffer()
146 self.ui.pushbuffer()
147 self.ct.show(ctx, root=self.repo.root, file=path)
147 self.ct.show(ctx, root=self.repo.root, file=path)
148 ekw = templatefilters.firstline(self.ui.popbuffer())
148 ekw = templatefilters.firstline(self.ui.popbuffer())
149 return '$%s: %s $' % (kw, ekw)
149 return '$%s: %s $' % (kw, ekw)
150 return subfunc(kwsub, data)
150 return subfunc(kwsub, data)
151
151
152 def expand(self, path, node, data):
152 def expand(self, path, node, data):
153 '''Returns data with keywords expanded.'''
153 '''Returns data with keywords expanded.'''
154 if not self.restrict and self.match(path) and not util.binary(data):
154 if not self.restrict and self.match(path) and not util.binary(data):
155 ctx = self.repo.filectx(path, fileid=node).changectx()
155 ctx = self.repo.filectx(path, fileid=node).changectx()
156 return self.substitute(data, path, ctx, self.re_kw.sub)
156 return self.substitute(data, path, ctx, self.re_kw.sub)
157 return data
157 return data
158
158
159 def iskwfile(self, path, flagfunc):
159 def iskwfile(self, path, flagfunc):
160 '''Returns true if path matches [keyword] pattern
160 '''Returns true if path matches [keyword] pattern
161 and is not a symbolic link.
161 and is not a symbolic link.
162 Caveat: localrepository._link fails on Windows.'''
162 Caveat: localrepository._link fails on Windows.'''
163 return self.match(path) and not 'l' in flagfunc(path)
163 return self.match(path) and not 'l' in flagfunc(path)
164
164
165 def overwrite(self, node, expand, candidates):
165 def overwrite(self, node, expand, candidates):
166 '''Overwrites selected files expanding/shrinking keywords.'''
166 '''Overwrites selected files expanding/shrinking keywords.'''
167 ctx = self.repo[node]
167 ctx = self.repo[node]
168 mf = ctx.manifest()
168 mf = ctx.manifest()
169 if node is not None: # commit
169 if node is not None: # commit
170 candidates = [f for f in ctx.files() if f in mf]
170 candidates = [f for f in ctx.files() if f in mf]
171 candidates = [f for f in candidates if self.iskwfile(f, ctx.flags)]
171 candidates = [f for f in candidates if self.iskwfile(f, ctx.flags)]
172 if candidates:
172 if candidates:
173 self.restrict = True # do not expand when reading
173 self.restrict = True # do not expand when reading
174 msg = (expand and _('overwriting %s expanding keywords\n')
174 msg = (expand and _('overwriting %s expanding keywords\n')
175 or _('overwriting %s shrinking keywords\n'))
175 or _('overwriting %s shrinking keywords\n'))
176 for f in candidates:
176 for f in candidates:
177 fp = self.repo.file(f)
177 fp = self.repo.file(f)
178 data = fp.read(mf[f])
178 data = fp.read(mf[f])
179 if util.binary(data):
179 if util.binary(data):
180 continue
180 continue
181 if expand:
181 if expand:
182 if node is None:
182 if node is None:
183 ctx = self.repo.filectx(f, fileid=mf[f]).changectx()
183 ctx = self.repo.filectx(f, fileid=mf[f]).changectx()
184 data, found = self.substitute(data, f, ctx,
184 data, found = self.substitute(data, f, ctx,
185 self.re_kw.subn)
185 self.re_kw.subn)
186 else:
186 else:
187 found = self.re_kw.search(data)
187 found = self.re_kw.search(data)
188 if found:
188 if found:
189 self.ui.note(msg % f)
189 self.ui.note(msg % f)
190 self.repo.wwrite(f, data, mf.flags(f))
190 self.repo.wwrite(f, data, mf.flags(f))
191 if node is None:
191 if node is None:
192 self.repo.dirstate.normal(f)
192 self.repo.dirstate.normal(f)
193 self.restrict = False
193 self.restrict = False
194
194
195 def shrinktext(self, text):
195 def shrinktext(self, text):
196 '''Unconditionally removes all keyword substitutions from text.'''
196 '''Unconditionally removes all keyword substitutions from text.'''
197 return self.re_kw.sub(r'$\1$', text)
197 return self.re_kw.sub(r'$\1$', text)
198
198
199 def shrink(self, fname, text):
199 def shrink(self, fname, text):
200 '''Returns text with all keyword substitutions removed.'''
200 '''Returns text with all keyword substitutions removed.'''
201 if self.match(fname) and not util.binary(text):
201 if self.match(fname) and not util.binary(text):
202 return self.shrinktext(text)
202 return self.shrinktext(text)
203 return text
203 return text
204
204
205 def shrinklines(self, fname, lines):
205 def shrinklines(self, fname, lines):
206 '''Returns lines with keyword substitutions removed.'''
206 '''Returns lines with keyword substitutions removed.'''
207 if self.match(fname):
207 if self.match(fname):
208 text = ''.join(lines)
208 text = ''.join(lines)
209 if not util.binary(text):
209 if not util.binary(text):
210 return self.shrinktext(text).splitlines(True)
210 return self.shrinktext(text).splitlines(True)
211 return lines
211 return lines
212
212
213 def wread(self, fname, data):
213 def wread(self, fname, data):
214 '''If in restricted mode returns data read from wdir with
214 '''If in restricted mode returns data read from wdir with
215 keyword substitutions removed.'''
215 keyword substitutions removed.'''
216 return self.restrict and self.shrink(fname, data) or data
216 return self.restrict and self.shrink(fname, data) or data
217
217
218 class kwfilelog(filelog.filelog):
218 class kwfilelog(filelog.filelog):
219 '''
219 '''
220 Subclass of filelog to hook into its read, add, cmp methods.
220 Subclass of filelog to hook into its read, add, cmp methods.
221 Keywords are "stored" unexpanded, and processed on reading.
221 Keywords are "stored" unexpanded, and processed on reading.
222 '''
222 '''
223 def __init__(self, opener, kwt, path):
223 def __init__(self, opener, kwt, path):
224 super(kwfilelog, self).__init__(opener, path)
224 super(kwfilelog, self).__init__(opener, path)
225 self.kwt = kwt
225 self.kwt = kwt
226 self.path = path
226 self.path = path
227
227
228 def read(self, node):
228 def read(self, node):
229 '''Expands keywords when reading filelog.'''
229 '''Expands keywords when reading filelog.'''
230 data = super(kwfilelog, self).read(node)
230 data = super(kwfilelog, self).read(node)
231 return self.kwt.expand(self.path, node, data)
231 return self.kwt.expand(self.path, node, data)
232
232
233 def add(self, text, meta, tr, link, p1=None, p2=None):
233 def add(self, text, meta, tr, link, p1=None, p2=None):
234 '''Removes keyword substitutions when adding to filelog.'''
234 '''Removes keyword substitutions when adding to filelog.'''
235 text = self.kwt.shrink(self.path, text)
235 text = self.kwt.shrink(self.path, text)
236 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
236 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
237
237
238 def cmp(self, node, text):
238 def cmp(self, node, text):
239 '''Removes keyword substitutions for comparison.'''
239 '''Removes keyword substitutions for comparison.'''
240 text = self.kwt.shrink(self.path, text)
240 text = self.kwt.shrink(self.path, text)
241 if self.renamed(node):
241 if self.renamed(node):
242 t2 = super(kwfilelog, self).read(node)
242 t2 = super(kwfilelog, self).read(node)
243 return t2 != text
243 return t2 != text
244 return revlog.revlog.cmp(self, node, text)
244 return revlog.revlog.cmp(self, node, text)
245
245
246 def _status(ui, repo, kwt, *pats, **opts):
246 def _status(ui, repo, kwt, *pats, **opts):
247 '''Bails out if [keyword] configuration is not active.
247 '''Bails out if [keyword] configuration is not active.
248 Returns status of working directory.'''
248 Returns status of working directory.'''
249 if kwt:
249 if kwt:
250 return repo.status(match=cmdutil.match(repo, pats, opts), clean=True,
250 return repo.status(match=cmdutil.match(repo, pats, opts), clean=True,
251 unknown=opts.get('unknown') or opts.get('all'))
251 unknown=opts.get('unknown') or opts.get('all'))
252 if ui.configitems('keyword'):
252 if ui.configitems('keyword'):
253 raise util.Abort(_('[keyword] patterns cannot match'))
253 raise util.Abort(_('[keyword] patterns cannot match'))
254 raise util.Abort(_('no [keyword] patterns configured'))
254 raise util.Abort(_('no [keyword] patterns configured'))
255
255
256 def _kwfwrite(ui, repo, expand, *pats, **opts):
256 def _kwfwrite(ui, repo, expand, *pats, **opts):
257 '''Selects files and passes them to kwtemplater.overwrite.'''
257 '''Selects files and passes them to kwtemplater.overwrite.'''
258 if repo.dirstate.parents()[1] != nullid:
258 if repo.dirstate.parents()[1] != nullid:
259 raise util.Abort(_('outstanding uncommitted merge'))
259 raise util.Abort(_('outstanding uncommitted merge'))
260 kwt = kwtools['templater']
260 kwt = kwtools['templater']
261 wlock = repo.wlock()
261 wlock = repo.wlock()
262 try:
262 try:
263 status = _status(ui, repo, kwt, *pats, **opts)
263 status = _status(ui, repo, kwt, *pats, **opts)
264 modified, added, removed, deleted, unknown, ignored, clean = status
264 modified, added, removed, deleted, unknown, ignored, clean = status
265 if modified or added or removed or deleted:
265 if modified or added or removed or deleted:
266 raise util.Abort(_('outstanding uncommitted changes'))
266 raise util.Abort(_('outstanding uncommitted changes'))
267 kwt.overwrite(None, expand, clean)
267 kwt.overwrite(None, expand, clean)
268 finally:
268 finally:
269 wlock.release()
269 wlock.release()
270
270
271 def demo(ui, repo, *args, **opts):
271 def demo(ui, repo, *args, **opts):
272 '''print [keywordmaps] configuration and an expansion example
272 '''print [keywordmaps] configuration and an expansion example
273
273
274 Show current, custom, or default keyword template maps and their
274 Show current, custom, or default keyword template maps and their
275 expansions.
275 expansions.
276
276
277 Extend the current configuration by specifying maps as arguments
277 Extend the current configuration by specifying maps as arguments
278 and using -f/--rcfile to source an external hgrc file.
278 and using -f/--rcfile to source an external hgrc file.
279
279
280 Use -d/--default to disable current configuration.
280 Use -d/--default to disable current configuration.
281
281
282 See "hg help templates" for information on templates and filters.
282 See "hg help templates" for information on templates and filters.
283 '''
283 '''
284 def demoitems(section, items):
284 def demoitems(section, items):
285 ui.write('[%s]\n' % section)
285 ui.write('[%s]\n' % section)
286 for k, v in sorted(items):
286 for k, v in sorted(items):
287 ui.write('%s = %s\n' % (k, v))
287 ui.write('%s = %s\n' % (k, v))
288
288
289 fn = 'demo.txt'
289 fn = 'demo.txt'
290 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
290 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
291 ui.note(_('creating temporary repository at %s\n') % tmpdir)
291 ui.note(_('creating temporary repository at %s\n') % tmpdir)
292 repo = localrepo.localrepository(ui, tmpdir, True)
292 repo = localrepo.localrepository(ui, tmpdir, True)
293 ui.setconfig('keyword', fn, '')
293 ui.setconfig('keyword', fn, '')
294
294
295 uikwmaps = ui.configitems('keywordmaps')
295 uikwmaps = ui.configitems('keywordmaps')
296 if args or opts.get('rcfile'):
296 if args or opts.get('rcfile'):
297 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
297 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
298 if uikwmaps:
298 if uikwmaps:
299 ui.status(_('\textending current template maps\n'))
299 ui.status(_('\textending current template maps\n'))
300 if opts.get('default') or not uikwmaps:
300 if opts.get('default') or not uikwmaps:
301 ui.status(_('\toverriding default template maps\n'))
301 ui.status(_('\toverriding default template maps\n'))
302 if opts.get('rcfile'):
302 if opts.get('rcfile'):
303 ui.readconfig(opts.get('rcfile'))
303 ui.readconfig(opts.get('rcfile'))
304 if args:
304 if args:
305 # simulate hgrc parsing
305 # simulate hgrc parsing
306 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
306 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
307 fp = repo.opener('hgrc', 'w')
307 fp = repo.opener('hgrc', 'w')
308 fp.writelines(rcmaps)
308 fp.writelines(rcmaps)
309 fp.close()
309 fp.close()
310 ui.readconfig(repo.join('hgrc'))
310 ui.readconfig(repo.join('hgrc'))
311 kwmaps = dict(ui.configitems('keywordmaps'))
311 kwmaps = dict(ui.configitems('keywordmaps'))
312 elif opts.get('default'):
312 elif opts.get('default'):
313 ui.status(_('\n\tconfiguration using default keyword template maps\n'))
313 ui.status(_('\n\tconfiguration using default keyword template maps\n'))
314 kwmaps = kwtemplater.templates
314 kwmaps = kwtemplater.templates
315 if uikwmaps:
315 if uikwmaps:
316 ui.status(_('\tdisabling current template maps\n'))
316 ui.status(_('\tdisabling current template maps\n'))
317 for k, v in kwmaps.iteritems():
317 for k, v in kwmaps.iteritems():
318 ui.setconfig('keywordmaps', k, v)
318 ui.setconfig('keywordmaps', k, v)
319 else:
319 else:
320 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
320 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
321 kwmaps = dict(uikwmaps) or kwtemplater.templates
321 kwmaps = dict(uikwmaps) or kwtemplater.templates
322
322
323 uisetup(ui)
323 uisetup(ui)
324 reposetup(ui, repo)
324 reposetup(ui, repo)
325 ui.write('[extensions]\nkeyword =\n')
325 ui.write('[extensions]\nkeyword =\n')
326 demoitems('keyword', ui.configitems('keyword'))
326 demoitems('keyword', ui.configitems('keyword'))
327 demoitems('keywordmaps', kwmaps.iteritems())
327 demoitems('keywordmaps', kwmaps.iteritems())
328 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
328 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
329 repo.wopener(fn, 'w').write(keywords)
329 repo.wopener(fn, 'w').write(keywords)
330 repo.add([fn])
330 repo.add([fn])
331 ui.note(_('\nkeywords written to %s:\n') % fn)
331 ui.note(_('\nkeywords written to %s:\n') % fn)
332 ui.note(keywords)
332 ui.note(keywords)
333 repo.dirstate.setbranch('demobranch')
333 repo.dirstate.setbranch('demobranch')
334 for name, cmd in ui.configitems('hooks'):
334 for name, cmd in ui.configitems('hooks'):
335 if name.split('.', 1)[0].find('commit') > -1:
335 if name.split('.', 1)[0].find('commit') > -1:
336 repo.ui.setconfig('hooks', name, '')
336 repo.ui.setconfig('hooks', name, '')
337 msg = _('hg keyword configuration and expansion example')
337 msg = _('hg keyword configuration and expansion example')
338 ui.note("hg ci -m '%s'\n" % msg)
338 ui.note("hg ci -m '%s'\n" % msg)
339 repo.commit(text=msg)
339 repo.commit(text=msg)
340 ui.status(_('\n\tkeywords expanded\n'))
340 ui.status(_('\n\tkeywords expanded\n'))
341 ui.write(repo.wread(fn))
341 ui.write(repo.wread(fn))
342 shutil.rmtree(tmpdir, ignore_errors=True)
342 shutil.rmtree(tmpdir, ignore_errors=True)
343
343
344 def expand(ui, repo, *pats, **opts):
344 def expand(ui, repo, *pats, **opts):
345 '''expand keywords in the working directory
345 '''expand keywords in the working directory
346
346
347 Run after (re)enabling keyword expansion.
347 Run after (re)enabling keyword expansion.
348
348
349 kwexpand refuses to run if given files contain local changes.
349 kwexpand refuses to run if given files contain local changes.
350 '''
350 '''
351 # 3rd argument sets expansion to True
351 # 3rd argument sets expansion to True
352 _kwfwrite(ui, repo, True, *pats, **opts)
352 _kwfwrite(ui, repo, True, *pats, **opts)
353
353
354 def files(ui, repo, *pats, **opts):
354 def files(ui, repo, *pats, **opts):
355 '''show files configured for keyword expansion
355 '''show files configured for keyword expansion
356
356
357 List which files in the working directory are matched by the
357 List which files in the working directory are matched by the
358 [keyword] configuration patterns.
358 [keyword] configuration patterns.
359
359
360 Useful to prevent inadvertent keyword expansion and to speed up
360 Useful to prevent inadvertent keyword expansion and to speed up
361 execution by including only files that are actual candidates for
361 execution by including only files that are actual candidates for
362 expansion.
362 expansion.
363
363
364 See "hg help keyword" on how to construct patterns both for
364 See "hg help keyword" on how to construct patterns both for
365 inclusion and exclusion of files.
365 inclusion and exclusion of files.
366
366
367 With -A/--all and -v/--verbose the codes used to show the status
367 With -A/--all and -v/--verbose the codes used to show the status
368 of files are::
368 of files are::
369
369
370 K = keyword expansion candidate
370 K = keyword expansion candidate
371 k = keyword expansion candidate (not tracked)
371 k = keyword expansion candidate (not tracked)
372 I = ignored
372 I = ignored
373 i = ignored (not tracked)
373 i = ignored (not tracked)
374 '''
374 '''
375 kwt = kwtools['templater']
375 kwt = kwtools['templater']
376 status = _status(ui, repo, kwt, *pats, **opts)
376 status = _status(ui, repo, kwt, *pats, **opts)
377 cwd = pats and repo.getcwd() or ''
377 cwd = pats and repo.getcwd() or ''
378 modified, added, removed, deleted, unknown, ignored, clean = status
378 modified, added, removed, deleted, unknown, ignored, clean = status
379 files = []
379 files = []
380 if not opts.get('unknown') or opts.get('all'):
380 if not opts.get('unknown') or opts.get('all'):
381 files = sorted(modified + added + clean)
381 files = sorted(modified + added + clean)
382 wctx = repo[None]
382 wctx = repo[None]
383 kwfiles = [f for f in files if kwt.iskwfile(f, wctx.flags)]
383 kwfiles = [f for f in files if kwt.iskwfile(f, wctx.flags)]
384 kwunknown = [f for f in unknown if kwt.iskwfile(f, wctx.flags)]
384 kwunknown = [f for f in unknown if kwt.iskwfile(f, wctx.flags)]
385 if not opts.get('ignore') or opts.get('all'):
385 if not opts.get('ignore') or opts.get('all'):
386 showfiles = kwfiles, kwunknown
386 showfiles = kwfiles, kwunknown
387 else:
387 else:
388 showfiles = [], []
388 showfiles = [], []
389 if opts.get('all') or opts.get('ignore'):
389 if opts.get('all') or opts.get('ignore'):
390 showfiles += ([f for f in files if f not in kwfiles],
390 showfiles += ([f for f in files if f not in kwfiles],
391 [f for f in unknown if f not in kwunknown])
391 [f for f in unknown if f not in kwunknown])
392 for char, filenames in zip('KkIi', showfiles):
392 for char, filenames in zip('KkIi', showfiles):
393 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
393 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
394 for f in filenames:
394 for f in filenames:
395 ui.write(fmt % repo.pathto(f, cwd))
395 ui.write(fmt % repo.pathto(f, cwd))
396
396
397 def shrink(ui, repo, *pats, **opts):
397 def shrink(ui, repo, *pats, **opts):
398 '''revert expanded keywords in the working directory
398 '''revert expanded keywords in the working directory
399
399
400 Run before changing/disabling active keywords or if you experience
400 Run before changing/disabling active keywords or if you experience
401 problems with "hg import" or "hg merge".
401 problems with "hg import" or "hg merge".
402
402
403 kwshrink refuses to run if given files contain local changes.
403 kwshrink refuses to run if given files contain local changes.
404 '''
404 '''
405 # 3rd argument sets expansion to False
405 # 3rd argument sets expansion to False
406 _kwfwrite(ui, repo, False, *pats, **opts)
406 _kwfwrite(ui, repo, False, *pats, **opts)
407
407
408
408
409 def uisetup(ui):
409 def uisetup(ui):
410 '''Collects [keyword] config in kwtools.
410 '''Collects [keyword] config in kwtools.
411 Monkeypatches dispatch._parse if needed.'''
411 Monkeypatches dispatch._parse if needed.'''
412
412
413 for pat, opt in ui.configitems('keyword'):
413 for pat, opt in ui.configitems('keyword'):
414 if opt != 'ignore':
414 if opt != 'ignore':
415 kwtools['inc'].append(pat)
415 kwtools['inc'].append(pat)
416 else:
416 else:
417 kwtools['exc'].append(pat)
417 kwtools['exc'].append(pat)
418
418
419 if kwtools['inc']:
419 if kwtools['inc']:
420 def kwdispatch_parse(orig, ui, args):
420 def kwdispatch_parse(orig, ui, args):
421 '''Monkeypatch dispatch._parse to obtain running hg command.'''
421 '''Monkeypatch dispatch._parse to obtain running hg command.'''
422 cmd, func, args, options, cmdoptions = orig(ui, args)
422 cmd, func, args, options, cmdoptions = orig(ui, args)
423 kwtools['hgcmd'] = cmd
423 kwtools['hgcmd'] = cmd
424 return cmd, func, args, options, cmdoptions
424 return cmd, func, args, options, cmdoptions
425
425
426 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
426 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
427
427
428 def reposetup(ui, repo):
428 def reposetup(ui, repo):
429 '''Sets up repo as kwrepo for keyword substitution.
429 '''Sets up repo as kwrepo for keyword substitution.
430 Overrides file method to return kwfilelog instead of filelog
430 Overrides file method to return kwfilelog instead of filelog
431 if file matches user configuration.
431 if file matches user configuration.
432 Wraps commit to overwrite configured files with updated
432 Wraps commit to overwrite configured files with updated
433 keyword substitutions.
433 keyword substitutions.
434 Monkeypatches patch and webcommands.'''
434 Monkeypatches patch and webcommands.'''
435
435
436 try:
436 try:
437 if (not repo.local() or not kwtools['inc']
437 if (not repo.local() or not kwtools['inc']
438 or kwtools['hgcmd'] in nokwcommands.split()
438 or kwtools['hgcmd'] in nokwcommands.split()
439 or '.hg' in util.splitpath(repo.root)
439 or '.hg' in util.splitpath(repo.root)
440 or repo._url.startswith('bundle:')):
440 or repo._url.startswith('bundle:')):
441 return
441 return
442 except AttributeError:
442 except AttributeError:
443 pass
443 pass
444
444
445 kwtools['templater'] = kwt = kwtemplater(ui, repo)
445 kwtools['templater'] = kwt = kwtemplater(ui, repo)
446
446
447 class kwrepo(repo.__class__):
447 class kwrepo(repo.__class__):
448 def file(self, f):
448 def file(self, f):
449 if f[0] == '/':
449 if f[0] == '/':
450 f = f[1:]
450 f = f[1:]
451 return kwfilelog(self.sopener, kwt, f)
451 return kwfilelog(self.sopener, kwt, f)
452
452
453 def wread(self, filename):
453 def wread(self, filename):
454 data = super(kwrepo, self).wread(filename)
454 data = super(kwrepo, self).wread(filename)
455 return kwt.wread(filename, data)
455 return kwt.wread(filename, data)
456
456
457 def commit(self, *args, **opts):
457 def commit(self, *args, **opts):
458 # use custom commitctx for user commands
458 # use custom commitctx for user commands
459 # other extensions can still wrap repo.commitctx directly
459 # other extensions can still wrap repo.commitctx directly
460 self.commitctx = self.kwcommitctx
460 self.commitctx = self.kwcommitctx
461 try:
461 try:
462 return super(kwrepo, self).commit(*args, **opts)
462 return super(kwrepo, self).commit(*args, **opts)
463 finally:
463 finally:
464 del self.commitctx
464 del self.commitctx
465
465
466 def kwcommitctx(self, ctx, error=False):
466 def kwcommitctx(self, ctx, error=False):
467 n = super(kwrepo, self).commitctx(ctx, error)
467 n = super(kwrepo, self).commitctx(ctx, error)
468 # no lock needed, only called from repo.commit() which already locks
468 # no lock needed, only called from repo.commit() which already locks
469 kwt.overwrite(n, True, None)
469 kwt.overwrite(n, True, None)
470 return n
470 return n
471
471
472 # monkeypatches
472 # monkeypatches
473 def kwpatchfile_init(orig, self, ui, fname, opener,
473 def kwpatchfile_init(orig, self, ui, fname, opener,
474 missing=False, eol=None):
474 missing=False, eol=None):
475 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
475 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
476 rejects or conflicts due to expanded keywords in working dir.'''
476 rejects or conflicts due to expanded keywords in working dir.'''
477 orig(self, ui, fname, opener, missing, eol)
477 orig(self, ui, fname, opener, missing, eol)
478 # shrink keywords read from working dir
478 # shrink keywords read from working dir
479 self.lines = kwt.shrinklines(self.fname, self.lines)
479 self.lines = kwt.shrinklines(self.fname, self.lines)
480
480
481 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
481 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
482 opts=None):
482 opts=None):
483 '''Monkeypatch patch.diff to avoid expansion except when
483 '''Monkeypatch patch.diff to avoid expansion except when
484 comparing against working dir.'''
484 comparing against working dir.'''
485 if node2 is not None:
485 if node2 is not None:
486 kwt.match = util.never
486 kwt.match = util.never
487 elif node1 is not None and node1 != repo['.'].node():
487 elif node1 is not None and node1 != repo['.'].node():
488 kwt.restrict = True
488 kwt.restrict = True
489 return orig(repo, node1, node2, match, changes, opts)
489 return orig(repo, node1, node2, match, changes, opts)
490
490
491 def kwweb_skip(orig, web, req, tmpl):
491 def kwweb_skip(orig, web, req, tmpl):
492 '''Wraps webcommands.x turning off keyword expansion.'''
492 '''Wraps webcommands.x turning off keyword expansion.'''
493 kwt.match = util.never
493 kwt.match = util.never
494 return orig(web, req, tmpl)
494 return orig(web, req, tmpl)
495
495
496 repo.__class__ = kwrepo
496 repo.__class__ = kwrepo
497
497
498 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
498 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
499 if not kwt.restrict:
499 if not kwt.restrict:
500 extensions.wrapfunction(patch, 'diff', kw_diff)
500 extensions.wrapfunction(patch, 'diff', kw_diff)
501 for c in 'annotate changeset rev filediff diff'.split():
501 for c in 'annotate changeset rev filediff diff'.split():
502 extensions.wrapfunction(webcommands, c, kwweb_skip)
502 extensions.wrapfunction(webcommands, c, kwweb_skip)
503
503
504 cmdtable = {
504 cmdtable = {
505 'kwdemo':
505 'kwdemo':
506 (demo,
506 (demo,
507 [('d', 'default', None, _('show default keyword template maps')),
507 [('d', 'default', None, _('show default keyword template maps')),
508 ('f', 'rcfile', '', _('read maps from rcfile'))],
508 ('f', 'rcfile', '', _('read maps from rcfile'))],
509 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
509 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
510 'kwexpand': (expand, commands.walkopts,
510 'kwexpand': (expand, commands.walkopts,
511 _('hg kwexpand [OPTION]... [FILE]...')),
511 _('hg kwexpand [OPTION]... [FILE]...')),
512 'kwfiles':
512 'kwfiles':
513 (files,
513 (files,
514 [('A', 'all', None, _('show keyword status flags of all files')),
514 [('A', 'all', None, _('show keyword status flags of all files')),
515 ('i', 'ignore', None, _('show files excluded from expansion')),
515 ('i', 'ignore', None, _('show files excluded from expansion')),
516 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
516 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
517 ] + commands.walkopts,
517 ] + commands.walkopts,
518 _('hg kwfiles [OPTION]... [FILE]...')),
518 _('hg kwfiles [OPTION]... [FILE]...')),
519 'kwshrink': (shrink, commands.walkopts,
519 'kwshrink': (shrink, commands.walkopts,
520 _('hg kwshrink [OPTION]... [FILE]...')),
520 _('hg kwshrink [OPTION]... [FILE]...')),
521 }
521 }
General Comments 0
You need to be logged in to leave comments. Login now