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