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