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