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