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