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