##// END OF EJS Templates
templates: document missing keywords or filters...
Patrick Mezard -
r13592:ad2ee188 default
parent child Browse files
Show More
@@ -1,682 +1,685
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 configuration changes.
71 :hg:`kwdemo` to control the results of your configuration changes.
72
72
73 Before changing/disabling active keywords, you must run :hg:`kwshrink`
73 Before changing/disabling active keywords, you must run :hg:`kwshrink`
74 to avoid storing expanded keywords in the change history.
74 to avoid storing expanded keywords in the change history.
75
75
76 To force expansion after enabling it, or a configuration change, run
76 To force expansion after enabling it, or a configuration change, run
77 :hg:`kwexpand`.
77 :hg:`kwexpand`.
78
78
79 Expansions spanning more than one line and incremental expansions,
79 Expansions spanning more than one line and incremental expansions,
80 like CVS' $Log$, are not supported. A keyword template map "Log =
80 like CVS' $Log$, are not supported. A keyword template map "Log =
81 {desc}" expands to the first line of the changeset description.
81 {desc}" expands to the first line of the changeset description.
82 '''
82 '''
83
83
84 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
84 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
85 from mercurial import localrepo, match, patch, templatefilters, templater, util
85 from mercurial import localrepo, match, patch, templatefilters, templater, util
86 from mercurial.hgweb import webcommands
86 from mercurial.hgweb import webcommands
87 from mercurial.i18n import _
87 from mercurial.i18n import _
88 import os, re, shutil, tempfile
88 import os, re, shutil, tempfile
89
89
90 commands.optionalrepo += ' kwdemo'
90 commands.optionalrepo += ' kwdemo'
91
91
92 # hg commands that do not act on keywords
92 # hg commands that do not act on keywords
93 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
93 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
94 ' outgoing push tip verify convert email glog')
94 ' outgoing push tip verify convert email glog')
95
95
96 # hg commands that trigger expansion only when writing to working dir,
96 # hg commands that trigger expansion only when writing to working dir,
97 # not when reading filelog, and unexpand when reading from working dir
97 # not when reading filelog, and unexpand when reading from working dir
98 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
98 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
99
99
100 # names of extensions using dorecord
100 # names of extensions using dorecord
101 recordextensions = 'record'
101 recordextensions = 'record'
102
102
103 colortable = {
103 colortable = {
104 'kwfiles.enabled': 'green bold',
104 'kwfiles.enabled': 'green bold',
105 'kwfiles.deleted': 'cyan bold underline',
105 'kwfiles.deleted': 'cyan bold underline',
106 'kwfiles.enabledunknown': 'green',
106 'kwfiles.enabledunknown': 'green',
107 'kwfiles.ignored': 'bold',
107 'kwfiles.ignored': 'bold',
108 'kwfiles.ignoredunknown': 'none'
108 'kwfiles.ignoredunknown': 'none'
109 }
109 }
110
110
111 # date like in cvs' $Date
111 # date like in cvs' $Date
112 utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S')
112 def utcdate(text):
113 return util.datestr((text[0], 0), '%Y/%m/%d %H:%M:%S')
113 # date like in svn's $Date
114 # date like in svn's $Date
114 svnisodate = lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
115 def svnisodate(text):
116 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
115 # date like in svn's $Id
117 # date like in svn's $Id
116 svnutcdate = lambda x: util.datestr((x[0], 0), '%Y-%m-%d %H:%M:%SZ')
118 def svnutcdate(text):
119 return util.datestr((text[0], 0), '%Y-%m-%d %H:%M:%SZ')
117
120
118 # make keyword tools accessible
121 # make keyword tools accessible
119 kwtools = {'templater': None, 'hgcmd': ''}
122 kwtools = {'templater': None, 'hgcmd': ''}
120
123
121 def _defaultkwmaps(ui):
124 def _defaultkwmaps(ui):
122 '''Returns default keywordmaps according to keywordset configuration.'''
125 '''Returns default keywordmaps according to keywordset configuration.'''
123 templates = {
126 templates = {
124 'Revision': '{node|short}',
127 'Revision': '{node|short}',
125 'Author': '{author|user}',
128 'Author': '{author|user}',
126 }
129 }
127 kwsets = ({
130 kwsets = ({
128 'Date': '{date|utcdate}',
131 'Date': '{date|utcdate}',
129 'RCSfile': '{file|basename},v',
132 'RCSfile': '{file|basename},v',
130 'RCSFile': '{file|basename},v', # kept for backwards compatibility
133 'RCSFile': '{file|basename},v', # kept for backwards compatibility
131 # with hg-keyword
134 # with hg-keyword
132 'Source': '{root}/{file},v',
135 'Source': '{root}/{file},v',
133 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
136 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
134 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
137 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
135 }, {
138 }, {
136 'Date': '{date|svnisodate}',
139 'Date': '{date|svnisodate}',
137 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
140 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
138 'LastChangedRevision': '{node|short}',
141 'LastChangedRevision': '{node|short}',
139 'LastChangedBy': '{author|user}',
142 'LastChangedBy': '{author|user}',
140 'LastChangedDate': '{date|svnisodate}',
143 'LastChangedDate': '{date|svnisodate}',
141 })
144 })
142 templates.update(kwsets[ui.configbool('keywordset', 'svn')])
145 templates.update(kwsets[ui.configbool('keywordset', 'svn')])
143 return templates
146 return templates
144
147
145 def _shrinktext(text, subfunc):
148 def _shrinktext(text, subfunc):
146 '''Helper for keyword expansion removal in text.
149 '''Helper for keyword expansion removal in text.
147 Depending on subfunc also returns number of substitutions.'''
150 Depending on subfunc also returns number of substitutions.'''
148 return subfunc(r'$\1$', text)
151 return subfunc(r'$\1$', text)
149
152
150 def _preselect(wstatus, changed):
153 def _preselect(wstatus, changed):
151 '''Retrieves modfied and added files from a working directory state
154 '''Retrieves modfied and added files from a working directory state
152 and returns the subset of each contained in given changed files
155 and returns the subset of each contained in given changed files
153 retrieved from a change context.'''
156 retrieved from a change context.'''
154 modified, added = wstatus[:2]
157 modified, added = wstatus[:2]
155 modified = [f for f in modified if f in changed]
158 modified = [f for f in modified if f in changed]
156 added = [f for f in added if f in changed]
159 added = [f for f in added if f in changed]
157 return modified, added
160 return modified, added
158
161
159
162
160 class kwtemplater(object):
163 class kwtemplater(object):
161 '''
164 '''
162 Sets up keyword templates, corresponding keyword regex, and
165 Sets up keyword templates, corresponding keyword regex, and
163 provides keyword substitution functions.
166 provides keyword substitution functions.
164 '''
167 '''
165
168
166 def __init__(self, ui, repo, inc, exc):
169 def __init__(self, ui, repo, inc, exc):
167 self.ui = ui
170 self.ui = ui
168 self.repo = repo
171 self.repo = repo
169 self.match = match.match(repo.root, '', [], inc, exc)
172 self.match = match.match(repo.root, '', [], inc, exc)
170 self.restrict = kwtools['hgcmd'] in restricted.split()
173 self.restrict = kwtools['hgcmd'] in restricted.split()
171 self.record = False
174 self.record = False
172
175
173 kwmaps = self.ui.configitems('keywordmaps')
176 kwmaps = self.ui.configitems('keywordmaps')
174 if kwmaps: # override default templates
177 if kwmaps: # override default templates
175 self.templates = dict((k, templater.parsestring(v, False))
178 self.templates = dict((k, templater.parsestring(v, False))
176 for k, v in kwmaps)
179 for k, v in kwmaps)
177 else:
180 else:
178 self.templates = _defaultkwmaps(self.ui)
181 self.templates = _defaultkwmaps(self.ui)
179 templatefilters.filters.update({'utcdate': utcdate,
182 templatefilters.filters.update({'utcdate': utcdate,
180 'svnisodate': svnisodate,
183 'svnisodate': svnisodate,
181 'svnutcdate': svnutcdate})
184 'svnutcdate': svnutcdate})
182
185
183 @util.propertycache
186 @util.propertycache
184 def escape(self):
187 def escape(self):
185 '''Returns bar-separated and escaped keywords.'''
188 '''Returns bar-separated and escaped keywords.'''
186 return '|'.join(map(re.escape, self.templates.keys()))
189 return '|'.join(map(re.escape, self.templates.keys()))
187
190
188 @util.propertycache
191 @util.propertycache
189 def rekw(self):
192 def rekw(self):
190 '''Returns regex for unexpanded keywords.'''
193 '''Returns regex for unexpanded keywords.'''
191 return re.compile(r'\$(%s)\$' % self.escape)
194 return re.compile(r'\$(%s)\$' % self.escape)
192
195
193 @util.propertycache
196 @util.propertycache
194 def rekwexp(self):
197 def rekwexp(self):
195 '''Returns regex for expanded keywords.'''
198 '''Returns regex for expanded keywords.'''
196 return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape)
199 return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape)
197
200
198 def substitute(self, data, path, ctx, subfunc):
201 def substitute(self, data, path, ctx, subfunc):
199 '''Replaces keywords in data with expanded template.'''
202 '''Replaces keywords in data with expanded template.'''
200 def kwsub(mobj):
203 def kwsub(mobj):
201 kw = mobj.group(1)
204 kw = mobj.group(1)
202 ct = cmdutil.changeset_templater(self.ui, self.repo,
205 ct = cmdutil.changeset_templater(self.ui, self.repo,
203 False, None, '', False)
206 False, None, '', False)
204 ct.use_template(self.templates[kw])
207 ct.use_template(self.templates[kw])
205 self.ui.pushbuffer()
208 self.ui.pushbuffer()
206 ct.show(ctx, root=self.repo.root, file=path)
209 ct.show(ctx, root=self.repo.root, file=path)
207 ekw = templatefilters.firstline(self.ui.popbuffer())
210 ekw = templatefilters.firstline(self.ui.popbuffer())
208 return '$%s: %s $' % (kw, ekw)
211 return '$%s: %s $' % (kw, ekw)
209 return subfunc(kwsub, data)
212 return subfunc(kwsub, data)
210
213
211 def linkctx(self, path, fileid):
214 def linkctx(self, path, fileid):
212 '''Similar to filelog.linkrev, but returns a changectx.'''
215 '''Similar to filelog.linkrev, but returns a changectx.'''
213 return self.repo.filectx(path, fileid=fileid).changectx()
216 return self.repo.filectx(path, fileid=fileid).changectx()
214
217
215 def expand(self, path, node, data):
218 def expand(self, path, node, data):
216 '''Returns data with keywords expanded.'''
219 '''Returns data with keywords expanded.'''
217 if not self.restrict and self.match(path) and not util.binary(data):
220 if not self.restrict and self.match(path) and not util.binary(data):
218 ctx = self.linkctx(path, node)
221 ctx = self.linkctx(path, node)
219 return self.substitute(data, path, ctx, self.rekw.sub)
222 return self.substitute(data, path, ctx, self.rekw.sub)
220 return data
223 return data
221
224
222 def iskwfile(self, cand, ctx):
225 def iskwfile(self, cand, ctx):
223 '''Returns subset of candidates which are configured for keyword
226 '''Returns subset of candidates which are configured for keyword
224 expansion are not symbolic links.'''
227 expansion are not symbolic links.'''
225 return [f for f in cand if self.match(f) and not 'l' in ctx.flags(f)]
228 return [f for f in cand if self.match(f) and not 'l' in ctx.flags(f)]
226
229
227 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
230 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
228 '''Overwrites selected files expanding/shrinking keywords.'''
231 '''Overwrites selected files expanding/shrinking keywords.'''
229 if self.restrict or lookup or self.record: # exclude kw_copy
232 if self.restrict or lookup or self.record: # exclude kw_copy
230 candidates = self.iskwfile(candidates, ctx)
233 candidates = self.iskwfile(candidates, ctx)
231 if not candidates:
234 if not candidates:
232 return
235 return
233 kwcmd = self.restrict and lookup # kwexpand/kwshrink
236 kwcmd = self.restrict and lookup # kwexpand/kwshrink
234 if self.restrict or expand and lookup:
237 if self.restrict or expand and lookup:
235 mf = ctx.manifest()
238 mf = ctx.manifest()
236 lctx = ctx
239 lctx = ctx
237 re_kw = (self.restrict or rekw) and self.rekw or self.rekwexp
240 re_kw = (self.restrict or rekw) and self.rekw or self.rekwexp
238 msg = (expand and _('overwriting %s expanding keywords\n')
241 msg = (expand and _('overwriting %s expanding keywords\n')
239 or _('overwriting %s shrinking keywords\n'))
242 or _('overwriting %s shrinking keywords\n'))
240 for f in candidates:
243 for f in candidates:
241 if self.restrict:
244 if self.restrict:
242 data = self.repo.file(f).read(mf[f])
245 data = self.repo.file(f).read(mf[f])
243 else:
246 else:
244 data = self.repo.wread(f)
247 data = self.repo.wread(f)
245 if util.binary(data):
248 if util.binary(data):
246 continue
249 continue
247 if expand:
250 if expand:
248 if lookup:
251 if lookup:
249 lctx = self.linkctx(f, mf[f])
252 lctx = self.linkctx(f, mf[f])
250 data, found = self.substitute(data, f, lctx, re_kw.subn)
253 data, found = self.substitute(data, f, lctx, re_kw.subn)
251 elif self.restrict:
254 elif self.restrict:
252 found = re_kw.search(data)
255 found = re_kw.search(data)
253 else:
256 else:
254 data, found = _shrinktext(data, re_kw.subn)
257 data, found = _shrinktext(data, re_kw.subn)
255 if found:
258 if found:
256 self.ui.note(msg % f)
259 self.ui.note(msg % f)
257 self.repo.wwrite(f, data, ctx.flags(f))
260 self.repo.wwrite(f, data, ctx.flags(f))
258 if kwcmd:
261 if kwcmd:
259 self.repo.dirstate.normal(f)
262 self.repo.dirstate.normal(f)
260 elif self.record:
263 elif self.record:
261 self.repo.dirstate.normallookup(f)
264 self.repo.dirstate.normallookup(f)
262
265
263 def shrink(self, fname, text):
266 def shrink(self, fname, text):
264 '''Returns text with all keyword substitutions removed.'''
267 '''Returns text with all keyword substitutions removed.'''
265 if self.match(fname) and not util.binary(text):
268 if self.match(fname) and not util.binary(text):
266 return _shrinktext(text, self.rekwexp.sub)
269 return _shrinktext(text, self.rekwexp.sub)
267 return text
270 return text
268
271
269 def shrinklines(self, fname, lines):
272 def shrinklines(self, fname, lines):
270 '''Returns lines with keyword substitutions removed.'''
273 '''Returns lines with keyword substitutions removed.'''
271 if self.match(fname):
274 if self.match(fname):
272 text = ''.join(lines)
275 text = ''.join(lines)
273 if not util.binary(text):
276 if not util.binary(text):
274 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
277 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
275 return lines
278 return lines
276
279
277 def wread(self, fname, data):
280 def wread(self, fname, data):
278 '''If in restricted mode returns data read from wdir with
281 '''If in restricted mode returns data read from wdir with
279 keyword substitutions removed.'''
282 keyword substitutions removed.'''
280 return self.restrict and self.shrink(fname, data) or data
283 return self.restrict and self.shrink(fname, data) or data
281
284
282 class kwfilelog(filelog.filelog):
285 class kwfilelog(filelog.filelog):
283 '''
286 '''
284 Subclass of filelog to hook into its read, add, cmp methods.
287 Subclass of filelog to hook into its read, add, cmp methods.
285 Keywords are "stored" unexpanded, and processed on reading.
288 Keywords are "stored" unexpanded, and processed on reading.
286 '''
289 '''
287 def __init__(self, opener, kwt, path):
290 def __init__(self, opener, kwt, path):
288 super(kwfilelog, self).__init__(opener, path)
291 super(kwfilelog, self).__init__(opener, path)
289 self.kwt = kwt
292 self.kwt = kwt
290 self.path = path
293 self.path = path
291
294
292 def read(self, node):
295 def read(self, node):
293 '''Expands keywords when reading filelog.'''
296 '''Expands keywords when reading filelog.'''
294 data = super(kwfilelog, self).read(node)
297 data = super(kwfilelog, self).read(node)
295 if self.renamed(node):
298 if self.renamed(node):
296 return data
299 return data
297 return self.kwt.expand(self.path, node, data)
300 return self.kwt.expand(self.path, node, data)
298
301
299 def add(self, text, meta, tr, link, p1=None, p2=None):
302 def add(self, text, meta, tr, link, p1=None, p2=None):
300 '''Removes keyword substitutions when adding to filelog.'''
303 '''Removes keyword substitutions when adding to filelog.'''
301 text = self.kwt.shrink(self.path, text)
304 text = self.kwt.shrink(self.path, text)
302 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
305 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
303
306
304 def cmp(self, node, text):
307 def cmp(self, node, text):
305 '''Removes keyword substitutions for comparison.'''
308 '''Removes keyword substitutions for comparison.'''
306 text = self.kwt.shrink(self.path, text)
309 text = self.kwt.shrink(self.path, text)
307 return super(kwfilelog, self).cmp(node, text)
310 return super(kwfilelog, self).cmp(node, text)
308
311
309 def _status(ui, repo, kwt, *pats, **opts):
312 def _status(ui, repo, kwt, *pats, **opts):
310 '''Bails out if [keyword] configuration is not active.
313 '''Bails out if [keyword] configuration is not active.
311 Returns status of working directory.'''
314 Returns status of working directory.'''
312 if kwt:
315 if kwt:
313 return repo.status(match=cmdutil.match(repo, pats, opts), clean=True,
316 return repo.status(match=cmdutil.match(repo, pats, opts), clean=True,
314 unknown=opts.get('unknown') or opts.get('all'))
317 unknown=opts.get('unknown') or opts.get('all'))
315 if ui.configitems('keyword'):
318 if ui.configitems('keyword'):
316 raise util.Abort(_('[keyword] patterns cannot match'))
319 raise util.Abort(_('[keyword] patterns cannot match'))
317 raise util.Abort(_('no [keyword] patterns configured'))
320 raise util.Abort(_('no [keyword] patterns configured'))
318
321
319 def _kwfwrite(ui, repo, expand, *pats, **opts):
322 def _kwfwrite(ui, repo, expand, *pats, **opts):
320 '''Selects files and passes them to kwtemplater.overwrite.'''
323 '''Selects files and passes them to kwtemplater.overwrite.'''
321 wctx = repo[None]
324 wctx = repo[None]
322 if len(wctx.parents()) > 1:
325 if len(wctx.parents()) > 1:
323 raise util.Abort(_('outstanding uncommitted merge'))
326 raise util.Abort(_('outstanding uncommitted merge'))
324 kwt = kwtools['templater']
327 kwt = kwtools['templater']
325 wlock = repo.wlock()
328 wlock = repo.wlock()
326 try:
329 try:
327 status = _status(ui, repo, kwt, *pats, **opts)
330 status = _status(ui, repo, kwt, *pats, **opts)
328 modified, added, removed, deleted, unknown, ignored, clean = status
331 modified, added, removed, deleted, unknown, ignored, clean = status
329 if modified or added or removed or deleted:
332 if modified or added or removed or deleted:
330 raise util.Abort(_('outstanding uncommitted changes'))
333 raise util.Abort(_('outstanding uncommitted changes'))
331 kwt.overwrite(wctx, clean, True, expand)
334 kwt.overwrite(wctx, clean, True, expand)
332 finally:
335 finally:
333 wlock.release()
336 wlock.release()
334
337
335 def demo(ui, repo, *args, **opts):
338 def demo(ui, repo, *args, **opts):
336 '''print [keywordmaps] configuration and an expansion example
339 '''print [keywordmaps] configuration and an expansion example
337
340
338 Show current, custom, or default keyword template maps and their
341 Show current, custom, or default keyword template maps and their
339 expansions.
342 expansions.
340
343
341 Extend the current configuration by specifying maps as arguments
344 Extend the current configuration by specifying maps as arguments
342 and using -f/--rcfile to source an external hgrc file.
345 and using -f/--rcfile to source an external hgrc file.
343
346
344 Use -d/--default to disable current configuration.
347 Use -d/--default to disable current configuration.
345
348
346 See :hg:`help templates` for information on templates and filters.
349 See :hg:`help templates` for information on templates and filters.
347 '''
350 '''
348 def demoitems(section, items):
351 def demoitems(section, items):
349 ui.write('[%s]\n' % section)
352 ui.write('[%s]\n' % section)
350 for k, v in sorted(items):
353 for k, v in sorted(items):
351 ui.write('%s = %s\n' % (k, v))
354 ui.write('%s = %s\n' % (k, v))
352
355
353 fn = 'demo.txt'
356 fn = 'demo.txt'
354 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
357 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
355 ui.note(_('creating temporary repository at %s\n') % tmpdir)
358 ui.note(_('creating temporary repository at %s\n') % tmpdir)
356 repo = localrepo.localrepository(ui, tmpdir, True)
359 repo = localrepo.localrepository(ui, tmpdir, True)
357 ui.setconfig('keyword', fn, '')
360 ui.setconfig('keyword', fn, '')
358 svn = ui.configbool('keywordset', 'svn')
361 svn = ui.configbool('keywordset', 'svn')
359 # explicitly set keywordset for demo output
362 # explicitly set keywordset for demo output
360 ui.setconfig('keywordset', 'svn', svn)
363 ui.setconfig('keywordset', 'svn', svn)
361
364
362 uikwmaps = ui.configitems('keywordmaps')
365 uikwmaps = ui.configitems('keywordmaps')
363 if args or opts.get('rcfile'):
366 if args or opts.get('rcfile'):
364 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
367 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
365 if uikwmaps:
368 if uikwmaps:
366 ui.status(_('\textending current template maps\n'))
369 ui.status(_('\textending current template maps\n'))
367 if opts.get('default') or not uikwmaps:
370 if opts.get('default') or not uikwmaps:
368 if svn:
371 if svn:
369 ui.status(_('\toverriding default svn keywordset\n'))
372 ui.status(_('\toverriding default svn keywordset\n'))
370 else:
373 else:
371 ui.status(_('\toverriding default cvs keywordset\n'))
374 ui.status(_('\toverriding default cvs keywordset\n'))
372 if opts.get('rcfile'):
375 if opts.get('rcfile'):
373 ui.readconfig(opts.get('rcfile'))
376 ui.readconfig(opts.get('rcfile'))
374 if args:
377 if args:
375 # simulate hgrc parsing
378 # simulate hgrc parsing
376 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
379 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
377 fp = repo.opener('hgrc', 'w')
380 fp = repo.opener('hgrc', 'w')
378 fp.writelines(rcmaps)
381 fp.writelines(rcmaps)
379 fp.close()
382 fp.close()
380 ui.readconfig(repo.join('hgrc'))
383 ui.readconfig(repo.join('hgrc'))
381 kwmaps = dict(ui.configitems('keywordmaps'))
384 kwmaps = dict(ui.configitems('keywordmaps'))
382 elif opts.get('default'):
385 elif opts.get('default'):
383 if svn:
386 if svn:
384 ui.status(_('\n\tconfiguration using default svn keywordset\n'))
387 ui.status(_('\n\tconfiguration using default svn keywordset\n'))
385 else:
388 else:
386 ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
389 ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
387 kwmaps = _defaultkwmaps(ui)
390 kwmaps = _defaultkwmaps(ui)
388 if uikwmaps:
391 if uikwmaps:
389 ui.status(_('\tdisabling current template maps\n'))
392 ui.status(_('\tdisabling current template maps\n'))
390 for k, v in kwmaps.iteritems():
393 for k, v in kwmaps.iteritems():
391 ui.setconfig('keywordmaps', k, v)
394 ui.setconfig('keywordmaps', k, v)
392 else:
395 else:
393 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
396 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
394 kwmaps = dict(uikwmaps) or _defaultkwmaps(ui)
397 kwmaps = dict(uikwmaps) or _defaultkwmaps(ui)
395
398
396 uisetup(ui)
399 uisetup(ui)
397 reposetup(ui, repo)
400 reposetup(ui, repo)
398 ui.write('[extensions]\nkeyword =\n')
401 ui.write('[extensions]\nkeyword =\n')
399 demoitems('keyword', ui.configitems('keyword'))
402 demoitems('keyword', ui.configitems('keyword'))
400 demoitems('keywordset', ui.configitems('keywordset'))
403 demoitems('keywordset', ui.configitems('keywordset'))
401 demoitems('keywordmaps', kwmaps.iteritems())
404 demoitems('keywordmaps', kwmaps.iteritems())
402 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
405 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
403 repo.wopener(fn, 'w').write(keywords)
406 repo.wopener(fn, 'w').write(keywords)
404 repo[None].add([fn])
407 repo[None].add([fn])
405 ui.note(_('\nkeywords written to %s:\n') % fn)
408 ui.note(_('\nkeywords written to %s:\n') % fn)
406 ui.note(keywords)
409 ui.note(keywords)
407 repo.dirstate.setbranch('demobranch')
410 repo.dirstate.setbranch('demobranch')
408 for name, cmd in ui.configitems('hooks'):
411 for name, cmd in ui.configitems('hooks'):
409 if name.split('.', 1)[0].find('commit') > -1:
412 if name.split('.', 1)[0].find('commit') > -1:
410 repo.ui.setconfig('hooks', name, '')
413 repo.ui.setconfig('hooks', name, '')
411 msg = _('hg keyword configuration and expansion example')
414 msg = _('hg keyword configuration and expansion example')
412 ui.note("hg ci -m '%s'\n" % msg)
415 ui.note("hg ci -m '%s'\n" % msg)
413 repo.commit(text=msg)
416 repo.commit(text=msg)
414 ui.status(_('\n\tkeywords expanded\n'))
417 ui.status(_('\n\tkeywords expanded\n'))
415 ui.write(repo.wread(fn))
418 ui.write(repo.wread(fn))
416 shutil.rmtree(tmpdir, ignore_errors=True)
419 shutil.rmtree(tmpdir, ignore_errors=True)
417
420
418 def expand(ui, repo, *pats, **opts):
421 def expand(ui, repo, *pats, **opts):
419 '''expand keywords in the working directory
422 '''expand keywords in the working directory
420
423
421 Run after (re)enabling keyword expansion.
424 Run after (re)enabling keyword expansion.
422
425
423 kwexpand refuses to run if given files contain local changes.
426 kwexpand refuses to run if given files contain local changes.
424 '''
427 '''
425 # 3rd argument sets expansion to True
428 # 3rd argument sets expansion to True
426 _kwfwrite(ui, repo, True, *pats, **opts)
429 _kwfwrite(ui, repo, True, *pats, **opts)
427
430
428 def files(ui, repo, *pats, **opts):
431 def files(ui, repo, *pats, **opts):
429 '''show files configured for keyword expansion
432 '''show files configured for keyword expansion
430
433
431 List which files in the working directory are matched by the
434 List which files in the working directory are matched by the
432 [keyword] configuration patterns.
435 [keyword] configuration patterns.
433
436
434 Useful to prevent inadvertent keyword expansion and to speed up
437 Useful to prevent inadvertent keyword expansion and to speed up
435 execution by including only files that are actual candidates for
438 execution by including only files that are actual candidates for
436 expansion.
439 expansion.
437
440
438 See :hg:`help keyword` on how to construct patterns both for
441 See :hg:`help keyword` on how to construct patterns both for
439 inclusion and exclusion of files.
442 inclusion and exclusion of files.
440
443
441 With -A/--all and -v/--verbose the codes used to show the status
444 With -A/--all and -v/--verbose the codes used to show the status
442 of files are::
445 of files are::
443
446
444 K = keyword expansion candidate
447 K = keyword expansion candidate
445 k = keyword expansion candidate (not tracked)
448 k = keyword expansion candidate (not tracked)
446 I = ignored
449 I = ignored
447 i = ignored (not tracked)
450 i = ignored (not tracked)
448 '''
451 '''
449 kwt = kwtools['templater']
452 kwt = kwtools['templater']
450 status = _status(ui, repo, kwt, *pats, **opts)
453 status = _status(ui, repo, kwt, *pats, **opts)
451 cwd = pats and repo.getcwd() or ''
454 cwd = pats and repo.getcwd() or ''
452 modified, added, removed, deleted, unknown, ignored, clean = status
455 modified, added, removed, deleted, unknown, ignored, clean = status
453 files = []
456 files = []
454 if not opts.get('unknown') or opts.get('all'):
457 if not opts.get('unknown') or opts.get('all'):
455 files = sorted(modified + added + clean)
458 files = sorted(modified + added + clean)
456 wctx = repo[None]
459 wctx = repo[None]
457 kwfiles = kwt.iskwfile(files, wctx)
460 kwfiles = kwt.iskwfile(files, wctx)
458 kwdeleted = kwt.iskwfile(deleted, wctx)
461 kwdeleted = kwt.iskwfile(deleted, wctx)
459 kwunknown = kwt.iskwfile(unknown, wctx)
462 kwunknown = kwt.iskwfile(unknown, wctx)
460 if not opts.get('ignore') or opts.get('all'):
463 if not opts.get('ignore') or opts.get('all'):
461 showfiles = kwfiles, kwdeleted, kwunknown
464 showfiles = kwfiles, kwdeleted, kwunknown
462 else:
465 else:
463 showfiles = [], [], []
466 showfiles = [], [], []
464 if opts.get('all') or opts.get('ignore'):
467 if opts.get('all') or opts.get('ignore'):
465 showfiles += ([f for f in files if f not in kwfiles],
468 showfiles += ([f for f in files if f not in kwfiles],
466 [f for f in unknown if f not in kwunknown])
469 [f for f in unknown if f not in kwunknown])
467 kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
470 kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
468 kwstates = zip('K!kIi', showfiles, kwlabels)
471 kwstates = zip('K!kIi', showfiles, kwlabels)
469 for char, filenames, kwstate in kwstates:
472 for char, filenames, kwstate in kwstates:
470 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
473 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
471 for f in filenames:
474 for f in filenames:
472 ui.write(fmt % repo.pathto(f, cwd), label='kwfiles.' + kwstate)
475 ui.write(fmt % repo.pathto(f, cwd), label='kwfiles.' + kwstate)
473
476
474 def shrink(ui, repo, *pats, **opts):
477 def shrink(ui, repo, *pats, **opts):
475 '''revert expanded keywords in the working directory
478 '''revert expanded keywords in the working directory
476
479
477 Must be run before changing/disabling active keywords.
480 Must be run before changing/disabling active keywords.
478
481
479 kwshrink refuses to run if given files contain local changes.
482 kwshrink refuses to run if given files contain local changes.
480 '''
483 '''
481 # 3rd argument sets expansion to False
484 # 3rd argument sets expansion to False
482 _kwfwrite(ui, repo, False, *pats, **opts)
485 _kwfwrite(ui, repo, False, *pats, **opts)
483
486
484
487
485 def uisetup(ui):
488 def uisetup(ui):
486 ''' Monkeypatches dispatch._parse to retrieve user command.'''
489 ''' Monkeypatches dispatch._parse to retrieve user command.'''
487
490
488 def kwdispatch_parse(orig, ui, args):
491 def kwdispatch_parse(orig, ui, args):
489 '''Monkeypatch dispatch._parse to obtain running hg command.'''
492 '''Monkeypatch dispatch._parse to obtain running hg command.'''
490 cmd, func, args, options, cmdoptions = orig(ui, args)
493 cmd, func, args, options, cmdoptions = orig(ui, args)
491 kwtools['hgcmd'] = cmd
494 kwtools['hgcmd'] = cmd
492 return cmd, func, args, options, cmdoptions
495 return cmd, func, args, options, cmdoptions
493
496
494 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
497 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
495
498
496 def reposetup(ui, repo):
499 def reposetup(ui, repo):
497 '''Sets up repo as kwrepo for keyword substitution.
500 '''Sets up repo as kwrepo for keyword substitution.
498 Overrides file method to return kwfilelog instead of filelog
501 Overrides file method to return kwfilelog instead of filelog
499 if file matches user configuration.
502 if file matches user configuration.
500 Wraps commit to overwrite configured files with updated
503 Wraps commit to overwrite configured files with updated
501 keyword substitutions.
504 keyword substitutions.
502 Monkeypatches patch and webcommands.'''
505 Monkeypatches patch and webcommands.'''
503
506
504 try:
507 try:
505 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
508 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
506 or '.hg' in util.splitpath(repo.root)
509 or '.hg' in util.splitpath(repo.root)
507 or repo._url.startswith('bundle:')):
510 or repo._url.startswith('bundle:')):
508 return
511 return
509 except AttributeError:
512 except AttributeError:
510 pass
513 pass
511
514
512 inc, exc = [], ['.hg*']
515 inc, exc = [], ['.hg*']
513 for pat, opt in ui.configitems('keyword'):
516 for pat, opt in ui.configitems('keyword'):
514 if opt != 'ignore':
517 if opt != 'ignore':
515 inc.append(pat)
518 inc.append(pat)
516 else:
519 else:
517 exc.append(pat)
520 exc.append(pat)
518 if not inc:
521 if not inc:
519 return
522 return
520
523
521 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
524 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
522
525
523 class kwrepo(repo.__class__):
526 class kwrepo(repo.__class__):
524 def file(self, f):
527 def file(self, f):
525 if f[0] == '/':
528 if f[0] == '/':
526 f = f[1:]
529 f = f[1:]
527 return kwfilelog(self.sopener, kwt, f)
530 return kwfilelog(self.sopener, kwt, f)
528
531
529 def wread(self, filename):
532 def wread(self, filename):
530 data = super(kwrepo, self).wread(filename)
533 data = super(kwrepo, self).wread(filename)
531 return kwt.wread(filename, data)
534 return kwt.wread(filename, data)
532
535
533 def commit(self, *args, **opts):
536 def commit(self, *args, **opts):
534 # use custom commitctx for user commands
537 # use custom commitctx for user commands
535 # other extensions can still wrap repo.commitctx directly
538 # other extensions can still wrap repo.commitctx directly
536 self.commitctx = self.kwcommitctx
539 self.commitctx = self.kwcommitctx
537 try:
540 try:
538 return super(kwrepo, self).commit(*args, **opts)
541 return super(kwrepo, self).commit(*args, **opts)
539 finally:
542 finally:
540 del self.commitctx
543 del self.commitctx
541
544
542 def kwcommitctx(self, ctx, error=False):
545 def kwcommitctx(self, ctx, error=False):
543 n = super(kwrepo, self).commitctx(ctx, error)
546 n = super(kwrepo, self).commitctx(ctx, error)
544 # no lock needed, only called from repo.commit() which already locks
547 # no lock needed, only called from repo.commit() which already locks
545 if not kwt.record:
548 if not kwt.record:
546 restrict = kwt.restrict
549 restrict = kwt.restrict
547 kwt.restrict = True
550 kwt.restrict = True
548 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
551 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
549 False, True)
552 False, True)
550 kwt.restrict = restrict
553 kwt.restrict = restrict
551 return n
554 return n
552
555
553 def rollback(self, dryrun=False):
556 def rollback(self, dryrun=False):
554 wlock = self.wlock()
557 wlock = self.wlock()
555 try:
558 try:
556 if not dryrun:
559 if not dryrun:
557 changed = self['.'].files()
560 changed = self['.'].files()
558 ret = super(kwrepo, self).rollback(dryrun)
561 ret = super(kwrepo, self).rollback(dryrun)
559 if not dryrun:
562 if not dryrun:
560 ctx = self['.']
563 ctx = self['.']
561 modified, added = _preselect(self[None].status(), changed)
564 modified, added = _preselect(self[None].status(), changed)
562 kwt.overwrite(ctx, modified, True, True)
565 kwt.overwrite(ctx, modified, True, True)
563 kwt.overwrite(ctx, added, True, False)
566 kwt.overwrite(ctx, added, True, False)
564 return ret
567 return ret
565 finally:
568 finally:
566 wlock.release()
569 wlock.release()
567
570
568 # monkeypatches
571 # monkeypatches
569 def kwpatchfile_init(orig, self, ui, fname, opener,
572 def kwpatchfile_init(orig, self, ui, fname, opener,
570 missing=False, eolmode=None):
573 missing=False, eolmode=None):
571 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
574 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
572 rejects or conflicts due to expanded keywords in working dir.'''
575 rejects or conflicts due to expanded keywords in working dir.'''
573 orig(self, ui, fname, opener, missing, eolmode)
576 orig(self, ui, fname, opener, missing, eolmode)
574 # shrink keywords read from working dir
577 # shrink keywords read from working dir
575 self.lines = kwt.shrinklines(self.fname, self.lines)
578 self.lines = kwt.shrinklines(self.fname, self.lines)
576
579
577 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
580 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
578 opts=None, prefix=''):
581 opts=None, prefix=''):
579 '''Monkeypatch patch.diff to avoid expansion.'''
582 '''Monkeypatch patch.diff to avoid expansion.'''
580 kwt.restrict = True
583 kwt.restrict = True
581 return orig(repo, node1, node2, match, changes, opts, prefix)
584 return orig(repo, node1, node2, match, changes, opts, prefix)
582
585
583 def kwweb_skip(orig, web, req, tmpl):
586 def kwweb_skip(orig, web, req, tmpl):
584 '''Wraps webcommands.x turning off keyword expansion.'''
587 '''Wraps webcommands.x turning off keyword expansion.'''
585 kwt.match = util.never
588 kwt.match = util.never
586 return orig(web, req, tmpl)
589 return orig(web, req, tmpl)
587
590
588 def kw_copy(orig, ui, repo, pats, opts, rename=False):
591 def kw_copy(orig, ui, repo, pats, opts, rename=False):
589 '''Wraps cmdutil.copy so that copy/rename destinations do not
592 '''Wraps cmdutil.copy so that copy/rename destinations do not
590 contain expanded keywords.
593 contain expanded keywords.
591 Note that the source of a regular file destination may also be a
594 Note that the source of a regular file destination may also be a
592 symlink:
595 symlink:
593 hg cp sym x -> x is symlink
596 hg cp sym x -> x is symlink
594 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
597 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
595 For the latter we have to follow the symlink to find out whether its
598 For the latter we have to follow the symlink to find out whether its
596 target is configured for expansion and we therefore must unexpand the
599 target is configured for expansion and we therefore must unexpand the
597 keywords in the destination.'''
600 keywords in the destination.'''
598 orig(ui, repo, pats, opts, rename)
601 orig(ui, repo, pats, opts, rename)
599 if opts.get('dry_run'):
602 if opts.get('dry_run'):
600 return
603 return
601 wctx = repo[None]
604 wctx = repo[None]
602 cwd = repo.getcwd()
605 cwd = repo.getcwd()
603
606
604 def haskwsource(dest):
607 def haskwsource(dest):
605 '''Returns true if dest is a regular file and configured for
608 '''Returns true if dest is a regular file and configured for
606 expansion or a symlink which points to a file configured for
609 expansion or a symlink which points to a file configured for
607 expansion. '''
610 expansion. '''
608 source = repo.dirstate.copied(dest)
611 source = repo.dirstate.copied(dest)
609 if 'l' in wctx.flags(source):
612 if 'l' in wctx.flags(source):
610 source = util.canonpath(repo.root, cwd,
613 source = util.canonpath(repo.root, cwd,
611 os.path.realpath(source))
614 os.path.realpath(source))
612 return kwt.match(source)
615 return kwt.match(source)
613
616
614 candidates = [f for f in repo.dirstate.copies() if
617 candidates = [f for f in repo.dirstate.copies() if
615 not 'l' in wctx.flags(f) and haskwsource(f)]
618 not 'l' in wctx.flags(f) and haskwsource(f)]
616 kwt.overwrite(wctx, candidates, False, False)
619 kwt.overwrite(wctx, candidates, False, False)
617
620
618 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
621 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
619 '''Wraps record.dorecord expanding keywords after recording.'''
622 '''Wraps record.dorecord expanding keywords after recording.'''
620 wlock = repo.wlock()
623 wlock = repo.wlock()
621 try:
624 try:
622 # record returns 0 even when nothing has changed
625 # record returns 0 even when nothing has changed
623 # therefore compare nodes before and after
626 # therefore compare nodes before and after
624 kwt.record = True
627 kwt.record = True
625 ctx = repo['.']
628 ctx = repo['.']
626 wstatus = repo[None].status()
629 wstatus = repo[None].status()
627 ret = orig(ui, repo, commitfunc, *pats, **opts)
630 ret = orig(ui, repo, commitfunc, *pats, **opts)
628 recctx = repo['.']
631 recctx = repo['.']
629 if ctx != recctx:
632 if ctx != recctx:
630 modified, added = _preselect(wstatus, recctx.files())
633 modified, added = _preselect(wstatus, recctx.files())
631 kwt.restrict = False
634 kwt.restrict = False
632 kwt.overwrite(recctx, modified, False, True)
635 kwt.overwrite(recctx, modified, False, True)
633 kwt.overwrite(recctx, added, False, True, True)
636 kwt.overwrite(recctx, added, False, True, True)
634 kwt.restrict = True
637 kwt.restrict = True
635 return ret
638 return ret
636 finally:
639 finally:
637 wlock.release()
640 wlock.release()
638
641
639 def kwfilectx_cmp(orig, self, fctx):
642 def kwfilectx_cmp(orig, self, fctx):
640 # keyword affects data size, comparing wdir and filelog size does
643 # keyword affects data size, comparing wdir and filelog size does
641 # not make sense
644 # not make sense
642 if (fctx._filerev is None and
645 if (fctx._filerev is None and
643 (self._repo._encodefilterpats or
646 (self._repo._encodefilterpats or
644 kwt.match(fctx.path()) and not 'l' in fctx.flags()) or
647 kwt.match(fctx.path()) and not 'l' in fctx.flags()) or
645 self.size() == fctx.size()):
648 self.size() == fctx.size()):
646 return self._filelog.cmp(self._filenode, fctx.data())
649 return self._filelog.cmp(self._filenode, fctx.data())
647 return True
650 return True
648
651
649 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
652 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
650 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
653 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
651 extensions.wrapfunction(patch, 'diff', kw_diff)
654 extensions.wrapfunction(patch, 'diff', kw_diff)
652 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
655 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
653 for c in 'annotate changeset rev filediff diff'.split():
656 for c in 'annotate changeset rev filediff diff'.split():
654 extensions.wrapfunction(webcommands, c, kwweb_skip)
657 extensions.wrapfunction(webcommands, c, kwweb_skip)
655 for name in recordextensions.split():
658 for name in recordextensions.split():
656 try:
659 try:
657 record = extensions.find(name)
660 record = extensions.find(name)
658 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
661 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
659 except KeyError:
662 except KeyError:
660 pass
663 pass
661
664
662 repo.__class__ = kwrepo
665 repo.__class__ = kwrepo
663
666
664 cmdtable = {
667 cmdtable = {
665 'kwdemo':
668 'kwdemo':
666 (demo,
669 (demo,
667 [('d', 'default', None, _('show default keyword template maps')),
670 [('d', 'default', None, _('show default keyword template maps')),
668 ('f', 'rcfile', '',
671 ('f', 'rcfile', '',
669 _('read maps from rcfile'), _('FILE'))],
672 _('read maps from rcfile'), _('FILE'))],
670 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
673 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
671 'kwexpand': (expand, commands.walkopts,
674 'kwexpand': (expand, commands.walkopts,
672 _('hg kwexpand [OPTION]... [FILE]...')),
675 _('hg kwexpand [OPTION]... [FILE]...')),
673 'kwfiles':
676 'kwfiles':
674 (files,
677 (files,
675 [('A', 'all', None, _('show keyword status flags of all files')),
678 [('A', 'all', None, _('show keyword status flags of all files')),
676 ('i', 'ignore', None, _('show files excluded from expansion')),
679 ('i', 'ignore', None, _('show files excluded from expansion')),
677 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
680 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
678 ] + commands.walkopts,
681 ] + commands.walkopts,
679 _('hg kwfiles [OPTION]... [FILE]...')),
682 _('hg kwfiles [OPTION]... [FILE]...')),
680 'kwshrink': (shrink, commands.walkopts,
683 'kwshrink': (shrink, commands.walkopts,
681 _('hg kwshrink [OPTION]... [FILE]...')),
684 _('hg kwshrink [OPTION]... [FILE]...')),
682 }
685 }
@@ -1,331 +1,334
1 # templatekw.py - common changeset template keywords
1 # templatekw.py - common changeset template keywords
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com>
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 from node import hex
8 from node import hex
9 import encoding, patch, util, error
9 import encoding, patch, util, error
10 from i18n import gettext
10 from i18n import gettext
11
11
12 def showlist(name, values, plural=None, **args):
12 def showlist(name, values, plural=None, **args):
13 '''expand set of values.
13 '''expand set of values.
14 name is name of key in template map.
14 name is name of key in template map.
15 values is list of strings or dicts.
15 values is list of strings or dicts.
16 plural is plural of name, if not simply name + 's'.
16 plural is plural of name, if not simply name + 's'.
17
17
18 expansion works like this, given name 'foo'.
18 expansion works like this, given name 'foo'.
19
19
20 if values is empty, expand 'no_foos'.
20 if values is empty, expand 'no_foos'.
21
21
22 if 'foo' not in template map, return values as a string,
22 if 'foo' not in template map, return values as a string,
23 joined by space.
23 joined by space.
24
24
25 expand 'start_foos'.
25 expand 'start_foos'.
26
26
27 for each value, expand 'foo'. if 'last_foo' in template
27 for each value, expand 'foo'. if 'last_foo' in template
28 map, expand it instead of 'foo' for last key.
28 map, expand it instead of 'foo' for last key.
29
29
30 expand 'end_foos'.
30 expand 'end_foos'.
31 '''
31 '''
32 templ = args['templ']
32 templ = args['templ']
33 if plural:
33 if plural:
34 names = plural
34 names = plural
35 else: names = name + 's'
35 else: names = name + 's'
36 if not values:
36 if not values:
37 noname = 'no_' + names
37 noname = 'no_' + names
38 if noname in templ:
38 if noname in templ:
39 yield templ(noname, **args)
39 yield templ(noname, **args)
40 return
40 return
41 if name not in templ:
41 if name not in templ:
42 if isinstance(values[0], str):
42 if isinstance(values[0], str):
43 yield ' '.join(values)
43 yield ' '.join(values)
44 else:
44 else:
45 for v in values:
45 for v in values:
46 yield dict(v, **args)
46 yield dict(v, **args)
47 return
47 return
48 startname = 'start_' + names
48 startname = 'start_' + names
49 if startname in templ:
49 if startname in templ:
50 yield templ(startname, **args)
50 yield templ(startname, **args)
51 vargs = args.copy()
51 vargs = args.copy()
52 def one(v, tag=name):
52 def one(v, tag=name):
53 try:
53 try:
54 vargs.update(v)
54 vargs.update(v)
55 except (AttributeError, ValueError):
55 except (AttributeError, ValueError):
56 try:
56 try:
57 for a, b in v:
57 for a, b in v:
58 vargs[a] = b
58 vargs[a] = b
59 except ValueError:
59 except ValueError:
60 vargs[name] = v
60 vargs[name] = v
61 return templ(tag, **vargs)
61 return templ(tag, **vargs)
62 lastname = 'last_' + name
62 lastname = 'last_' + name
63 if lastname in templ:
63 if lastname in templ:
64 last = values.pop()
64 last = values.pop()
65 else:
65 else:
66 last = None
66 last = None
67 for v in values:
67 for v in values:
68 yield one(v)
68 yield one(v)
69 if last is not None:
69 if last is not None:
70 yield one(last, tag=lastname)
70 yield one(last, tag=lastname)
71 endname = 'end_' + names
71 endname = 'end_' + names
72 if endname in templ:
72 if endname in templ:
73 yield templ(endname, **args)
73 yield templ(endname, **args)
74
74
75 def getfiles(repo, ctx, revcache):
75 def getfiles(repo, ctx, revcache):
76 if 'files' not in revcache:
76 if 'files' not in revcache:
77 revcache['files'] = repo.status(ctx.parents()[0].node(),
77 revcache['files'] = repo.status(ctx.parents()[0].node(),
78 ctx.node())[:3]
78 ctx.node())[:3]
79 return revcache['files']
79 return revcache['files']
80
80
81 def getlatesttags(repo, ctx, cache):
81 def getlatesttags(repo, ctx, cache):
82 '''return date, distance and name for the latest tag of rev'''
82 '''return date, distance and name for the latest tag of rev'''
83
83
84 if 'latesttags' not in cache:
84 if 'latesttags' not in cache:
85 # Cache mapping from rev to a tuple with tag date, tag
85 # Cache mapping from rev to a tuple with tag date, tag
86 # distance and tag name
86 # distance and tag name
87 cache['latesttags'] = {-1: (0, 0, 'null')}
87 cache['latesttags'] = {-1: (0, 0, 'null')}
88 latesttags = cache['latesttags']
88 latesttags = cache['latesttags']
89
89
90 rev = ctx.rev()
90 rev = ctx.rev()
91 todo = [rev]
91 todo = [rev]
92 while todo:
92 while todo:
93 rev = todo.pop()
93 rev = todo.pop()
94 if rev in latesttags:
94 if rev in latesttags:
95 continue
95 continue
96 ctx = repo[rev]
96 ctx = repo[rev]
97 tags = [t for t in ctx.tags() if repo.tagtype(t) == 'global']
97 tags = [t for t in ctx.tags() if repo.tagtype(t) == 'global']
98 if tags:
98 if tags:
99 latesttags[rev] = ctx.date()[0], 0, ':'.join(sorted(tags))
99 latesttags[rev] = ctx.date()[0], 0, ':'.join(sorted(tags))
100 continue
100 continue
101 try:
101 try:
102 # The tuples are laid out so the right one can be found by
102 # The tuples are laid out so the right one can be found by
103 # comparison.
103 # comparison.
104 pdate, pdist, ptag = max(
104 pdate, pdist, ptag = max(
105 latesttags[p.rev()] for p in ctx.parents())
105 latesttags[p.rev()] for p in ctx.parents())
106 except KeyError:
106 except KeyError:
107 # Cache miss - recurse
107 # Cache miss - recurse
108 todo.append(rev)
108 todo.append(rev)
109 todo.extend(p.rev() for p in ctx.parents())
109 todo.extend(p.rev() for p in ctx.parents())
110 continue
110 continue
111 latesttags[rev] = pdate, pdist + 1, ptag
111 latesttags[rev] = pdate, pdist + 1, ptag
112 return latesttags[rev]
112 return latesttags[rev]
113
113
114 def getrenamedfn(repo, endrev=None):
114 def getrenamedfn(repo, endrev=None):
115 rcache = {}
115 rcache = {}
116 if endrev is None:
116 if endrev is None:
117 endrev = len(repo)
117 endrev = len(repo)
118
118
119 def getrenamed(fn, rev):
119 def getrenamed(fn, rev):
120 '''looks up all renames for a file (up to endrev) the first
120 '''looks up all renames for a file (up to endrev) the first
121 time the file is given. It indexes on the changerev and only
121 time the file is given. It indexes on the changerev and only
122 parses the manifest if linkrev != changerev.
122 parses the manifest if linkrev != changerev.
123 Returns rename info for fn at changerev rev.'''
123 Returns rename info for fn at changerev rev.'''
124 if fn not in rcache:
124 if fn not in rcache:
125 rcache[fn] = {}
125 rcache[fn] = {}
126 fl = repo.file(fn)
126 fl = repo.file(fn)
127 for i in fl:
127 for i in fl:
128 lr = fl.linkrev(i)
128 lr = fl.linkrev(i)
129 renamed = fl.renamed(fl.node(i))
129 renamed = fl.renamed(fl.node(i))
130 rcache[fn][lr] = renamed
130 rcache[fn][lr] = renamed
131 if lr >= endrev:
131 if lr >= endrev:
132 break
132 break
133 if rev in rcache[fn]:
133 if rev in rcache[fn]:
134 return rcache[fn][rev]
134 return rcache[fn][rev]
135
135
136 # If linkrev != rev (i.e. rev not found in rcache) fallback to
136 # If linkrev != rev (i.e. rev not found in rcache) fallback to
137 # filectx logic.
137 # filectx logic.
138 try:
138 try:
139 return repo[rev][fn].renamed()
139 return repo[rev][fn].renamed()
140 except error.LookupError:
140 except error.LookupError:
141 return None
141 return None
142
142
143 return getrenamed
143 return getrenamed
144
144
145
145
146 def showauthor(repo, ctx, templ, **args):
146 def showauthor(repo, ctx, templ, **args):
147 """:author: String. The unmodified author of the changeset."""
147 """:author: String. The unmodified author of the changeset."""
148 return ctx.user()
148 return ctx.user()
149
149
150 def showbranch(**args):
150 def showbranch(**args):
151 """:branch: String. The name of the branch on which the changeset was
151 """:branch: String. The name of the branch on which the changeset was
152 committed.
152 committed.
153 """
153 """
154 return args['ctx'].branch()
154 return args['ctx'].branch()
155
155
156 def showbranches(**args):
156 def showbranches(**args):
157 """:branches: List of strings. The name of the branch on which the
157 """:branches: List of strings. The name of the branch on which the
158 changeset was committed. Will be empty if the branch name was
158 changeset was committed. Will be empty if the branch name was
159 default.
159 default.
160 """
160 """
161 branch = args['ctx'].branch()
161 branch = args['ctx'].branch()
162 if branch != 'default':
162 if branch != 'default':
163 return showlist('branch', [branch], plural='branches', **args)
163 return showlist('branch', [branch], plural='branches', **args)
164
164
165 def showbookmarks(**args):
165 def showbookmarks(**args):
166 """:bookmarks: List of strings. Any bookmarks associated with the
167 changeset.
168 """
166 bookmarks = args['ctx'].bookmarks()
169 bookmarks = args['ctx'].bookmarks()
167 return showlist('bookmark', bookmarks, **args)
170 return showlist('bookmark', bookmarks, **args)
168
171
169 def showchildren(**args):
172 def showchildren(**args):
170 """:children: List of strings. The children of the changeset."""
173 """:children: List of strings. The children of the changeset."""
171 ctx = args['ctx']
174 ctx = args['ctx']
172 childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
175 childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
173 return showlist('children', childrevs, **args)
176 return showlist('children', childrevs, **args)
174
177
175 def showdate(repo, ctx, templ, **args):
178 def showdate(repo, ctx, templ, **args):
176 """:date: Date information. The date when the changeset was committed."""
179 """:date: Date information. The date when the changeset was committed."""
177 return ctx.date()
180 return ctx.date()
178
181
179 def showdescription(repo, ctx, templ, **args):
182 def showdescription(repo, ctx, templ, **args):
180 """:desc: String. The text of the changeset description."""
183 """:desc: String. The text of the changeset description."""
181 return ctx.description().strip()
184 return ctx.description().strip()
182
185
183 def showdiffstat(repo, ctx, templ, **args):
186 def showdiffstat(repo, ctx, templ, **args):
184 """:diffstat: String. Statistics of changes with the following format:
187 """:diffstat: String. Statistics of changes with the following format:
185 "modified files: +added/-removed lines"
188 "modified files: +added/-removed lines"
186 """
189 """
187 files, adds, removes = 0, 0, 0
190 files, adds, removes = 0, 0, 0
188 for i in patch.diffstatdata(util.iterlines(ctx.diff())):
191 for i in patch.diffstatdata(util.iterlines(ctx.diff())):
189 files += 1
192 files += 1
190 adds += i[1]
193 adds += i[1]
191 removes += i[2]
194 removes += i[2]
192 return '%s: +%s/-%s' % (files, adds, removes)
195 return '%s: +%s/-%s' % (files, adds, removes)
193
196
194 def showextras(**args):
197 def showextras(**args):
195 templ = args['templ']
198 templ = args['templ']
196 for key, value in sorted(args['ctx'].extra().items()):
199 for key, value in sorted(args['ctx'].extra().items()):
197 args = args.copy()
200 args = args.copy()
198 args.update(dict(key=key, value=value))
201 args.update(dict(key=key, value=value))
199 yield templ('extra', **args)
202 yield templ('extra', **args)
200
203
201 def showfileadds(**args):
204 def showfileadds(**args):
202 """:file_adds: List of strings. Files added by this changeset."""
205 """:file_adds: List of strings. Files added by this changeset."""
203 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
206 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
204 return showlist('file_add', getfiles(repo, ctx, revcache)[1], **args)
207 return showlist('file_add', getfiles(repo, ctx, revcache)[1], **args)
205
208
206 def showfilecopies(**args):
209 def showfilecopies(**args):
207 """:file_copies: List of strings. Files copied in this changeset with
210 """:file_copies: List of strings. Files copied in this changeset with
208 their sources.
211 their sources.
209 """
212 """
210 cache, ctx = args['cache'], args['ctx']
213 cache, ctx = args['cache'], args['ctx']
211 copies = args['revcache'].get('copies')
214 copies = args['revcache'].get('copies')
212 if copies is None:
215 if copies is None:
213 if 'getrenamed' not in cache:
216 if 'getrenamed' not in cache:
214 cache['getrenamed'] = getrenamedfn(args['repo'])
217 cache['getrenamed'] = getrenamedfn(args['repo'])
215 copies = []
218 copies = []
216 getrenamed = cache['getrenamed']
219 getrenamed = cache['getrenamed']
217 for fn in ctx.files():
220 for fn in ctx.files():
218 rename = getrenamed(fn, ctx.rev())
221 rename = getrenamed(fn, ctx.rev())
219 if rename:
222 if rename:
220 copies.append((fn, rename[0]))
223 copies.append((fn, rename[0]))
221
224
222 c = [{'name': x[0], 'source': x[1]} for x in copies]
225 c = [{'name': x[0], 'source': x[1]} for x in copies]
223 return showlist('file_copy', c, plural='file_copies', **args)
226 return showlist('file_copy', c, plural='file_copies', **args)
224
227
225 # showfilecopiesswitch() displays file copies only if copy records are
228 # showfilecopiesswitch() displays file copies only if copy records are
226 # provided before calling the templater, usually with a --copies
229 # provided before calling the templater, usually with a --copies
227 # command line switch.
230 # command line switch.
228 def showfilecopiesswitch(**args):
231 def showfilecopiesswitch(**args):
229 """:file_copies_switch: List of strings. Like "file_copies" but displayed
232 """:file_copies_switch: List of strings. Like "file_copies" but displayed
230 only if the --copied switch is set.
233 only if the --copied switch is set.
231 """
234 """
232 copies = args['revcache'].get('copies') or []
235 copies = args['revcache'].get('copies') or []
233 c = [{'name': x[0], 'source': x[1]} for x in copies]
236 c = [{'name': x[0], 'source': x[1]} for x in copies]
234 return showlist('file_copy', c, plural='file_copies', **args)
237 return showlist('file_copy', c, plural='file_copies', **args)
235
238
236 def showfiledels(**args):
239 def showfiledels(**args):
237 """:file_dels: List of strings. Files removed by this changeset."""
240 """:file_dels: List of strings. Files removed by this changeset."""
238 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
241 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
239 return showlist('file_del', getfiles(repo, ctx, revcache)[2], **args)
242 return showlist('file_del', getfiles(repo, ctx, revcache)[2], **args)
240
243
241 def showfilemods(**args):
244 def showfilemods(**args):
242 """:file_mods: List of strings. Files modified by this changeset."""
245 """:file_mods: List of strings. Files modified by this changeset."""
243 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
246 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
244 return showlist('file_mod', getfiles(repo, ctx, revcache)[0], **args)
247 return showlist('file_mod', getfiles(repo, ctx, revcache)[0], **args)
245
248
246 def showfiles(**args):
249 def showfiles(**args):
247 """:files: List of strings. All files modified, added, or removed by this
250 """:files: List of strings. All files modified, added, or removed by this
248 changeset.
251 changeset.
249 """
252 """
250 return showlist('file', args['ctx'].files(), **args)
253 return showlist('file', args['ctx'].files(), **args)
251
254
252 def showlatesttag(repo, ctx, templ, cache, **args):
255 def showlatesttag(repo, ctx, templ, cache, **args):
253 """:latesttag: String. Most recent global tag in the ancestors of this
256 """:latesttag: String. Most recent global tag in the ancestors of this
254 changeset.
257 changeset.
255 """
258 """
256 return getlatesttags(repo, ctx, cache)[2]
259 return getlatesttags(repo, ctx, cache)[2]
257
260
258 def showlatesttagdistance(repo, ctx, templ, cache, **args):
261 def showlatesttagdistance(repo, ctx, templ, cache, **args):
259 """:latesttagdistance: Integer. Longest path to the latest tag."""
262 """:latesttagdistance: Integer. Longest path to the latest tag."""
260 return getlatesttags(repo, ctx, cache)[1]
263 return getlatesttags(repo, ctx, cache)[1]
261
264
262 def showmanifest(**args):
265 def showmanifest(**args):
263 repo, ctx, templ = args['repo'], args['ctx'], args['templ']
266 repo, ctx, templ = args['repo'], args['ctx'], args['templ']
264 args = args.copy()
267 args = args.copy()
265 args.update(dict(rev=repo.manifest.rev(ctx.changeset()[0]),
268 args.update(dict(rev=repo.manifest.rev(ctx.changeset()[0]),
266 node=hex(ctx.changeset()[0])))
269 node=hex(ctx.changeset()[0])))
267 return templ('manifest', **args)
270 return templ('manifest', **args)
268
271
269 def shownode(repo, ctx, templ, **args):
272 def shownode(repo, ctx, templ, **args):
270 """:node: String. The changeset identification hash, as a 40 hexadecimal
273 """:node: String. The changeset identification hash, as a 40 hexadecimal
271 digit string.
274 digit string.
272 """
275 """
273 return ctx.hex()
276 return ctx.hex()
274
277
275 def showrev(repo, ctx, templ, **args):
278 def showrev(repo, ctx, templ, **args):
276 """:rev: Integer. The repository-local changeset revision number."""
279 """:rev: Integer. The repository-local changeset revision number."""
277 return ctx.rev()
280 return ctx.rev()
278
281
279 def showtags(**args):
282 def showtags(**args):
280 """:tags: List of strings. Any tags associated with the changeset."""
283 """:tags: List of strings. Any tags associated with the changeset."""
281 return showlist('tag', args['ctx'].tags(), **args)
284 return showlist('tag', args['ctx'].tags(), **args)
282
285
283 # keywords are callables like:
286 # keywords are callables like:
284 # fn(repo, ctx, templ, cache, revcache, **args)
287 # fn(repo, ctx, templ, cache, revcache, **args)
285 # with:
288 # with:
286 # repo - current repository instance
289 # repo - current repository instance
287 # ctx - the changectx being displayed
290 # ctx - the changectx being displayed
288 # templ - the templater instance
291 # templ - the templater instance
289 # cache - a cache dictionary for the whole templater run
292 # cache - a cache dictionary for the whole templater run
290 # revcache - a cache dictionary for the current revision
293 # revcache - a cache dictionary for the current revision
291 keywords = {
294 keywords = {
292 'author': showauthor,
295 'author': showauthor,
293 'branch': showbranch,
296 'branch': showbranch,
294 'branches': showbranches,
297 'branches': showbranches,
295 'bookmarks': showbookmarks,
298 'bookmarks': showbookmarks,
296 'children': showchildren,
299 'children': showchildren,
297 'date': showdate,
300 'date': showdate,
298 'desc': showdescription,
301 'desc': showdescription,
299 'diffstat': showdiffstat,
302 'diffstat': showdiffstat,
300 'extras': showextras,
303 'extras': showextras,
301 'file_adds': showfileadds,
304 'file_adds': showfileadds,
302 'file_copies': showfilecopies,
305 'file_copies': showfilecopies,
303 'file_copies_switch': showfilecopiesswitch,
306 'file_copies_switch': showfilecopiesswitch,
304 'file_dels': showfiledels,
307 'file_dels': showfiledels,
305 'file_mods': showfilemods,
308 'file_mods': showfilemods,
306 'files': showfiles,
309 'files': showfiles,
307 'latesttag': showlatesttag,
310 'latesttag': showlatesttag,
308 'latesttagdistance': showlatesttagdistance,
311 'latesttagdistance': showlatesttagdistance,
309 'manifest': showmanifest,
312 'manifest': showmanifest,
310 'node': shownode,
313 'node': shownode,
311 'rev': showrev,
314 'rev': showrev,
312 'tags': showtags,
315 'tags': showtags,
313 }
316 }
314
317
315 def makedoc(topic, doc):
318 def makedoc(topic, doc):
316 """Generate and include keyword help in templating topic."""
319 """Generate and include keyword help in templating topic."""
317 kw = []
320 kw = []
318 for name in sorted(keywords):
321 for name in sorted(keywords):
319 text = (keywords[name].__doc__ or '').rstrip()
322 text = (keywords[name].__doc__ or '').rstrip()
320 if not text:
323 if not text:
321 continue
324 continue
322 text = gettext(text)
325 text = gettext(text)
323 lines = text.splitlines()
326 lines = text.splitlines()
324 lines[1:] = [(' ' + l.strip()) for l in lines[1:]]
327 lines[1:] = [(' ' + l.strip()) for l in lines[1:]]
325 kw.append('\n'.join(lines))
328 kw.append('\n'.join(lines))
326 kw = '\n\n'.join(kw)
329 kw = '\n\n'.join(kw)
327 doc = doc.replace('.. keywordsmarker', kw)
330 doc = doc.replace('.. keywordsmarker', kw)
328 return doc
331 return doc
329
332
330 # tell hggettext to extract docstrings from these functions:
333 # tell hggettext to extract docstrings from these functions:
331 i18nfunctions = keywords.values()
334 i18nfunctions = keywords.values()
General Comments 0
You need to be logged in to leave comments. Login now