##// END OF EJS Templates
templatekw: switch most of showlist template keywords to new API (issue5779)...
Yuya Nishihara -
r36609:121a20e5 default
parent child Browse files
Show More
@@ -1,300 +1,299 b''
1 # remotenames.py - extension to display remotenames
1 # remotenames.py - extension to display remotenames
2 #
2 #
3 # Copyright 2017 Augie Fackler <raf@durin42.com>
3 # Copyright 2017 Augie Fackler <raf@durin42.com>
4 # Copyright 2017 Sean Farley <sean@farley.io>
4 # Copyright 2017 Sean Farley <sean@farley.io>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 """ showing remotebookmarks and remotebranches in UI
9 """ showing remotebookmarks and remotebranches in UI
10
10
11 By default both remotebookmarks and remotebranches are turned on. Config knob to
11 By default both remotebookmarks and remotebranches are turned on. Config knob to
12 control the individually are as follows.
12 control the individually are as follows.
13
13
14 Config options to tweak the default behaviour:
14 Config options to tweak the default behaviour:
15
15
16 remotenames.bookmarks
16 remotenames.bookmarks
17 Boolean value to enable or disable showing of remotebookmarks
17 Boolean value to enable or disable showing of remotebookmarks
18
18
19 remotenames.branches
19 remotenames.branches
20 Boolean value to enable or disable showing of remotebranches
20 Boolean value to enable or disable showing of remotebranches
21 """
21 """
22
22
23 from __future__ import absolute_import
23 from __future__ import absolute_import
24
24
25 import collections
25 import collections
26
26
27 from mercurial.i18n import _
27 from mercurial.i18n import _
28
28
29 from mercurial.node import (
29 from mercurial.node import (
30 bin,
30 bin,
31 )
31 )
32 from mercurial import (
32 from mercurial import (
33 logexchange,
33 logexchange,
34 namespaces,
34 namespaces,
35 pycompat,
36 registrar,
35 registrar,
37 revsetlang,
36 revsetlang,
38 smartset,
37 smartset,
39 templatekw,
38 templatekw,
40 )
39 )
41
40
42 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
41 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
43 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
42 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
44 # be specifying the version(s) of Mercurial they are tested with, or
43 # be specifying the version(s) of Mercurial they are tested with, or
45 # leave the attribute unspecified.
44 # leave the attribute unspecified.
46 testedwith = 'ships-with-hg-core'
45 testedwith = 'ships-with-hg-core'
47
46
48 configtable = {}
47 configtable = {}
49 configitem = registrar.configitem(configtable)
48 configitem = registrar.configitem(configtable)
50 templatekeyword = registrar.templatekeyword()
49 templatekeyword = registrar.templatekeyword()
51 revsetpredicate = registrar.revsetpredicate()
50 revsetpredicate = registrar.revsetpredicate()
52
51
53 configitem('remotenames', 'bookmarks',
52 configitem('remotenames', 'bookmarks',
54 default=True,
53 default=True,
55 )
54 )
56 configitem('remotenames', 'branches',
55 configitem('remotenames', 'branches',
57 default=True,
56 default=True,
58 )
57 )
59
58
60 class lazyremotenamedict(collections.MutableMapping):
59 class lazyremotenamedict(collections.MutableMapping):
61 """
60 """
62 Read-only dict-like Class to lazily resolve remotename entries
61 Read-only dict-like Class to lazily resolve remotename entries
63
62
64 We are doing that because remotenames startup was slow.
63 We are doing that because remotenames startup was slow.
65 We lazily read the remotenames file once to figure out the potential entries
64 We lazily read the remotenames file once to figure out the potential entries
66 and store them in self.potentialentries. Then when asked to resolve an
65 and store them in self.potentialentries. Then when asked to resolve an
67 entry, if it is not in self.potentialentries, then it isn't there, if it
66 entry, if it is not in self.potentialentries, then it isn't there, if it
68 is in self.potentialentries we resolve it and store the result in
67 is in self.potentialentries we resolve it and store the result in
69 self.cache. We cannot be lazy is when asked all the entries (keys).
68 self.cache. We cannot be lazy is when asked all the entries (keys).
70 """
69 """
71 def __init__(self, kind, repo):
70 def __init__(self, kind, repo):
72 self.cache = {}
71 self.cache = {}
73 self.potentialentries = {}
72 self.potentialentries = {}
74 self._kind = kind # bookmarks or branches
73 self._kind = kind # bookmarks or branches
75 self._repo = repo
74 self._repo = repo
76 self.loaded = False
75 self.loaded = False
77
76
78 def _load(self):
77 def _load(self):
79 """ Read the remotenames file, store entries matching selected kind """
78 """ Read the remotenames file, store entries matching selected kind """
80 self.loaded = True
79 self.loaded = True
81 repo = self._repo
80 repo = self._repo
82 for node, rpath, rname in logexchange.readremotenamefile(repo,
81 for node, rpath, rname in logexchange.readremotenamefile(repo,
83 self._kind):
82 self._kind):
84 name = rpath + '/' + rname
83 name = rpath + '/' + rname
85 self.potentialentries[name] = (node, rpath, name)
84 self.potentialentries[name] = (node, rpath, name)
86
85
87 def _resolvedata(self, potentialentry):
86 def _resolvedata(self, potentialentry):
88 """ Check that the node for potentialentry exists and return it """
87 """ Check that the node for potentialentry exists and return it """
89 if not potentialentry in self.potentialentries:
88 if not potentialentry in self.potentialentries:
90 return None
89 return None
91 node, remote, name = self.potentialentries[potentialentry]
90 node, remote, name = self.potentialentries[potentialentry]
92 repo = self._repo
91 repo = self._repo
93 binnode = bin(node)
92 binnode = bin(node)
94 # if the node doesn't exist, skip it
93 # if the node doesn't exist, skip it
95 try:
94 try:
96 repo.changelog.rev(binnode)
95 repo.changelog.rev(binnode)
97 except LookupError:
96 except LookupError:
98 return None
97 return None
99 # Skip closed branches
98 # Skip closed branches
100 if (self._kind == 'branches' and repo[binnode].closesbranch()):
99 if (self._kind == 'branches' and repo[binnode].closesbranch()):
101 return None
100 return None
102 return [binnode]
101 return [binnode]
103
102
104 def __getitem__(self, key):
103 def __getitem__(self, key):
105 if not self.loaded:
104 if not self.loaded:
106 self._load()
105 self._load()
107 val = self._fetchandcache(key)
106 val = self._fetchandcache(key)
108 if val is not None:
107 if val is not None:
109 return val
108 return val
110 else:
109 else:
111 raise KeyError()
110 raise KeyError()
112
111
113 def __iter__(self):
112 def __iter__(self):
114 return iter(self.potentialentries)
113 return iter(self.potentialentries)
115
114
116 def __len__(self):
115 def __len__(self):
117 return len(self.potentialentries)
116 return len(self.potentialentries)
118
117
119 def __setitem__(self):
118 def __setitem__(self):
120 raise NotImplementedError
119 raise NotImplementedError
121
120
122 def __delitem__(self):
121 def __delitem__(self):
123 raise NotImplementedError
122 raise NotImplementedError
124
123
125 def _fetchandcache(self, key):
124 def _fetchandcache(self, key):
126 if key in self.cache:
125 if key in self.cache:
127 return self.cache[key]
126 return self.cache[key]
128 val = self._resolvedata(key)
127 val = self._resolvedata(key)
129 if val is not None:
128 if val is not None:
130 self.cache[key] = val
129 self.cache[key] = val
131 return val
130 return val
132 else:
131 else:
133 return None
132 return None
134
133
135 def keys(self):
134 def keys(self):
136 """ Get a list of bookmark or branch names """
135 """ Get a list of bookmark or branch names """
137 if not self.loaded:
136 if not self.loaded:
138 self._load()
137 self._load()
139 return self.potentialentries.keys()
138 return self.potentialentries.keys()
140
139
141 def iteritems(self):
140 def iteritems(self):
142 """ Iterate over (name, node) tuples """
141 """ Iterate over (name, node) tuples """
143
142
144 if not self.loaded:
143 if not self.loaded:
145 self._load()
144 self._load()
146
145
147 for k, vtup in self.potentialentries.iteritems():
146 for k, vtup in self.potentialentries.iteritems():
148 yield (k, [bin(vtup[0])])
147 yield (k, [bin(vtup[0])])
149
148
150 class remotenames(object):
149 class remotenames(object):
151 """
150 """
152 This class encapsulates all the remotenames state. It also contains
151 This class encapsulates all the remotenames state. It also contains
153 methods to access that state in convenient ways. Remotenames are lazy
152 methods to access that state in convenient ways. Remotenames are lazy
154 loaded. Whenever client code needs to ensure the freshest copy of
153 loaded. Whenever client code needs to ensure the freshest copy of
155 remotenames, use the `clearnames` method to force an eventual load.
154 remotenames, use the `clearnames` method to force an eventual load.
156 """
155 """
157
156
158 def __init__(self, repo, *args):
157 def __init__(self, repo, *args):
159 self._repo = repo
158 self._repo = repo
160 self.clearnames()
159 self.clearnames()
161
160
162 def clearnames(self):
161 def clearnames(self):
163 """ Clear all remote names state """
162 """ Clear all remote names state """
164 self.bookmarks = lazyremotenamedict("bookmarks", self._repo)
163 self.bookmarks = lazyremotenamedict("bookmarks", self._repo)
165 self.branches = lazyremotenamedict("branches", self._repo)
164 self.branches = lazyremotenamedict("branches", self._repo)
166 self._invalidatecache()
165 self._invalidatecache()
167
166
168 def _invalidatecache(self):
167 def _invalidatecache(self):
169 self._nodetobmarks = None
168 self._nodetobmarks = None
170 self._nodetobranch = None
169 self._nodetobranch = None
171
170
172 def bmarktonodes(self):
171 def bmarktonodes(self):
173 return self.bookmarks
172 return self.bookmarks
174
173
175 def nodetobmarks(self):
174 def nodetobmarks(self):
176 if not self._nodetobmarks:
175 if not self._nodetobmarks:
177 bmarktonodes = self.bmarktonodes()
176 bmarktonodes = self.bmarktonodes()
178 self._nodetobmarks = {}
177 self._nodetobmarks = {}
179 for name, node in bmarktonodes.iteritems():
178 for name, node in bmarktonodes.iteritems():
180 self._nodetobmarks.setdefault(node[0], []).append(name)
179 self._nodetobmarks.setdefault(node[0], []).append(name)
181 return self._nodetobmarks
180 return self._nodetobmarks
182
181
183 def branchtonodes(self):
182 def branchtonodes(self):
184 return self.branches
183 return self.branches
185
184
186 def nodetobranch(self):
185 def nodetobranch(self):
187 if not self._nodetobranch:
186 if not self._nodetobranch:
188 branchtonodes = self.branchtonodes()
187 branchtonodes = self.branchtonodes()
189 self._nodetobranch = {}
188 self._nodetobranch = {}
190 for name, nodes in branchtonodes.iteritems():
189 for name, nodes in branchtonodes.iteritems():
191 for node in nodes:
190 for node in nodes:
192 self._nodetobranch.setdefault(node, []).append(name)
191 self._nodetobranch.setdefault(node, []).append(name)
193 return self._nodetobranch
192 return self._nodetobranch
194
193
195 def reposetup(ui, repo):
194 def reposetup(ui, repo):
196 if not repo.local():
195 if not repo.local():
197 return
196 return
198
197
199 repo._remotenames = remotenames(repo)
198 repo._remotenames = remotenames(repo)
200 ns = namespaces.namespace
199 ns = namespaces.namespace
201
200
202 if ui.configbool('remotenames', 'bookmarks'):
201 if ui.configbool('remotenames', 'bookmarks'):
203 remotebookmarkns = ns(
202 remotebookmarkns = ns(
204 'remotebookmarks',
203 'remotebookmarks',
205 templatename='remotebookmarks',
204 templatename='remotebookmarks',
206 colorname='remotebookmark',
205 colorname='remotebookmark',
207 logfmt='remote bookmark: %s\n',
206 logfmt='remote bookmark: %s\n',
208 listnames=lambda repo: repo._remotenames.bmarktonodes().keys(),
207 listnames=lambda repo: repo._remotenames.bmarktonodes().keys(),
209 namemap=lambda repo, name:
208 namemap=lambda repo, name:
210 repo._remotenames.bmarktonodes().get(name, []),
209 repo._remotenames.bmarktonodes().get(name, []),
211 nodemap=lambda repo, node:
210 nodemap=lambda repo, node:
212 repo._remotenames.nodetobmarks().get(node, []))
211 repo._remotenames.nodetobmarks().get(node, []))
213 repo.names.addnamespace(remotebookmarkns)
212 repo.names.addnamespace(remotebookmarkns)
214
213
215 if ui.configbool('remotenames', 'branches'):
214 if ui.configbool('remotenames', 'branches'):
216 remotebranchns = ns(
215 remotebranchns = ns(
217 'remotebranches',
216 'remotebranches',
218 templatename='remotebranches',
217 templatename='remotebranches',
219 colorname='remotebranch',
218 colorname='remotebranch',
220 logfmt='remote branch: %s\n',
219 logfmt='remote branch: %s\n',
221 listnames = lambda repo: repo._remotenames.branchtonodes().keys(),
220 listnames = lambda repo: repo._remotenames.branchtonodes().keys(),
222 namemap = lambda repo, name:
221 namemap = lambda repo, name:
223 repo._remotenames.branchtonodes().get(name, []),
222 repo._remotenames.branchtonodes().get(name, []),
224 nodemap = lambda repo, node:
223 nodemap = lambda repo, node:
225 repo._remotenames.nodetobranch().get(node, []))
224 repo._remotenames.nodetobranch().get(node, []))
226 repo.names.addnamespace(remotebranchns)
225 repo.names.addnamespace(remotebranchns)
227
226
228 @templatekeyword('remotenames')
227 @templatekeyword('remotenames', requires={'repo', 'ctx', 'templ'})
229 def remotenameskw(**args):
228 def remotenameskw(context, mapping):
230 """List of strings. Remote names associated with the changeset."""
229 """List of strings. Remote names associated with the changeset."""
231 args = pycompat.byteskwargs(args)
230 repo = context.resource(mapping, 'repo')
232 repo, ctx = args['repo'], args['ctx']
231 ctx = context.resource(mapping, 'ctx')
233
232
234 remotenames = []
233 remotenames = []
235 if 'remotebookmarks' in repo.names:
234 if 'remotebookmarks' in repo.names:
236 remotenames = repo.names['remotebookmarks'].names(repo, ctx.node())
235 remotenames = repo.names['remotebookmarks'].names(repo, ctx.node())
237
236
238 if 'remotebranches' in repo.names:
237 if 'remotebranches' in repo.names:
239 remotenames += repo.names['remotebranches'].names(repo, ctx.node())
238 remotenames += repo.names['remotebranches'].names(repo, ctx.node())
240
239
241 return templatekw.showlist('remotename', remotenames, args,
240 return templatekw.compatlist(context, mapping, 'remotename', remotenames,
242 plural='remotenames')
241 plural='remotenames')
243
242
244 @templatekeyword('remotebookmarks')
243 @templatekeyword('remotebookmarks', requires={'repo', 'ctx', 'templ'})
245 def remotebookmarkskw(**args):
244 def remotebookmarkskw(context, mapping):
246 """List of strings. Remote bookmarks associated with the changeset."""
245 """List of strings. Remote bookmarks associated with the changeset."""
247 args = pycompat.byteskwargs(args)
246 repo = context.resource(mapping, 'repo')
248 repo, ctx = args['repo'], args['ctx']
247 ctx = context.resource(mapping, 'ctx')
249
248
250 remotebmarks = []
249 remotebmarks = []
251 if 'remotebookmarks' in repo.names:
250 if 'remotebookmarks' in repo.names:
252 remotebmarks = repo.names['remotebookmarks'].names(repo, ctx.node())
251 remotebmarks = repo.names['remotebookmarks'].names(repo, ctx.node())
253
252
254 return templatekw.showlist('remotebookmark', remotebmarks, args,
253 return templatekw.compatlist(context, mapping, 'remotebookmark',
255 plural='remotebookmarks')
254 remotebmarks, plural='remotebookmarks')
256
255
257 @templatekeyword('remotebranches')
256 @templatekeyword('remotebranches', requires={'repo', 'ctx', 'templ'})
258 def remotebrancheskw(**args):
257 def remotebrancheskw(context, mapping):
259 """List of strings. Remote branches associated with the changeset."""
258 """List of strings. Remote branches associated with the changeset."""
260 args = pycompat.byteskwargs(args)
259 repo = context.resource(mapping, 'repo')
261 repo, ctx = args['repo'], args['ctx']
260 ctx = context.resource(mapping, 'ctx')
262
261
263 remotebranches = []
262 remotebranches = []
264 if 'remotebranches' in repo.names:
263 if 'remotebranches' in repo.names:
265 remotebranches = repo.names['remotebranches'].names(repo, ctx.node())
264 remotebranches = repo.names['remotebranches'].names(repo, ctx.node())
266
265
267 return templatekw.showlist('remotebranch', remotebranches, args,
266 return templatekw.compatlist(context, mapping, 'remotebranch',
268 plural='remotebranches')
267 remotebranches, plural='remotebranches')
269
268
270 def _revsetutil(repo, subset, x, rtypes):
269 def _revsetutil(repo, subset, x, rtypes):
271 """utility function to return a set of revs based on the rtypes"""
270 """utility function to return a set of revs based on the rtypes"""
272
271
273 revs = set()
272 revs = set()
274 cl = repo.changelog
273 cl = repo.changelog
275 for rtype in rtypes:
274 for rtype in rtypes:
276 if rtype in repo.names:
275 if rtype in repo.names:
277 ns = repo.names[rtype]
276 ns = repo.names[rtype]
278 for name in ns.listnames(repo):
277 for name in ns.listnames(repo):
279 revs.update(ns.nodes(repo, name))
278 revs.update(ns.nodes(repo, name))
280
279
281 results = (cl.rev(n) for n in revs if cl.hasnode(n))
280 results = (cl.rev(n) for n in revs if cl.hasnode(n))
282 return subset & smartset.baseset(sorted(results))
281 return subset & smartset.baseset(sorted(results))
283
282
284 @revsetpredicate('remotenames()')
283 @revsetpredicate('remotenames()')
285 def remotenamesrevset(repo, subset, x):
284 def remotenamesrevset(repo, subset, x):
286 """All changesets which have a remotename on them."""
285 """All changesets which have a remotename on them."""
287 revsetlang.getargs(x, 0, 0, _("remotenames takes no arguments"))
286 revsetlang.getargs(x, 0, 0, _("remotenames takes no arguments"))
288 return _revsetutil(repo, subset, x, ('remotebookmarks', 'remotebranches'))
287 return _revsetutil(repo, subset, x, ('remotebookmarks', 'remotebranches'))
289
288
290 @revsetpredicate('remotebranches()')
289 @revsetpredicate('remotebranches()')
291 def remotebranchesrevset(repo, subset, x):
290 def remotebranchesrevset(repo, subset, x):
292 """All changesets which are branch heads on remotes."""
291 """All changesets which are branch heads on remotes."""
293 revsetlang.getargs(x, 0, 0, _("remotebranches takes no arguments"))
292 revsetlang.getargs(x, 0, 0, _("remotebranches takes no arguments"))
294 return _revsetutil(repo, subset, x, ('remotebranches',))
293 return _revsetutil(repo, subset, x, ('remotebranches',))
295
294
296 @revsetpredicate('remotebookmarks()')
295 @revsetpredicate('remotebookmarks()')
297 def remotebmarksrevset(repo, subset, x):
296 def remotebmarksrevset(repo, subset, x):
298 """All changesets which have bookmarks on remotes."""
297 """All changesets which have bookmarks on remotes."""
299 revsetlang.getargs(x, 0, 0, _("remotebookmarks takes no arguments"))
298 revsetlang.getargs(x, 0, 0, _("remotebookmarks takes no arguments"))
300 return _revsetutil(repo, subset, x, ('remotebookmarks',))
299 return _revsetutil(repo, subset, x, ('remotebookmarks',))
@@ -1,978 +1,977 b''
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 __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 from .i18n import _
10 from .i18n import _
11 from .node import (
11 from .node import (
12 hex,
12 hex,
13 nullid,
13 nullid,
14 )
14 )
15
15
16 from . import (
16 from . import (
17 encoding,
17 encoding,
18 error,
18 error,
19 hbisect,
19 hbisect,
20 i18n,
20 i18n,
21 obsutil,
21 obsutil,
22 patch,
22 patch,
23 pycompat,
23 pycompat,
24 registrar,
24 registrar,
25 scmutil,
25 scmutil,
26 util,
26 util,
27 )
27 )
28
28
29 class _hybrid(object):
29 class _hybrid(object):
30 """Wrapper for list or dict to support legacy template
30 """Wrapper for list or dict to support legacy template
31
31
32 This class allows us to handle both:
32 This class allows us to handle both:
33 - "{files}" (legacy command-line-specific list hack) and
33 - "{files}" (legacy command-line-specific list hack) and
34 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
34 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
35 and to access raw values:
35 and to access raw values:
36 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
36 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
37 - "{get(extras, key)}"
37 - "{get(extras, key)}"
38 - "{files|json}"
38 - "{files|json}"
39 """
39 """
40
40
41 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
41 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
42 if gen is not None:
42 if gen is not None:
43 self.gen = gen # generator or function returning generator
43 self.gen = gen # generator or function returning generator
44 self._values = values
44 self._values = values
45 self._makemap = makemap
45 self._makemap = makemap
46 self.joinfmt = joinfmt
46 self.joinfmt = joinfmt
47 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
47 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
48 def gen(self):
48 def gen(self):
49 """Default generator to stringify this as {join(self, ' ')}"""
49 """Default generator to stringify this as {join(self, ' ')}"""
50 for i, x in enumerate(self._values):
50 for i, x in enumerate(self._values):
51 if i > 0:
51 if i > 0:
52 yield ' '
52 yield ' '
53 yield self.joinfmt(x)
53 yield self.joinfmt(x)
54 def itermaps(self):
54 def itermaps(self):
55 makemap = self._makemap
55 makemap = self._makemap
56 for x in self._values:
56 for x in self._values:
57 yield makemap(x)
57 yield makemap(x)
58 def __contains__(self, x):
58 def __contains__(self, x):
59 return x in self._values
59 return x in self._values
60 def __getitem__(self, key):
60 def __getitem__(self, key):
61 return self._values[key]
61 return self._values[key]
62 def __len__(self):
62 def __len__(self):
63 return len(self._values)
63 return len(self._values)
64 def __iter__(self):
64 def __iter__(self):
65 return iter(self._values)
65 return iter(self._values)
66 def __getattr__(self, name):
66 def __getattr__(self, name):
67 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
67 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
68 r'itervalues', r'keys', r'values'):
68 r'itervalues', r'keys', r'values'):
69 raise AttributeError(name)
69 raise AttributeError(name)
70 return getattr(self._values, name)
70 return getattr(self._values, name)
71
71
72 class _mappable(object):
72 class _mappable(object):
73 """Wrapper for non-list/dict object to support map operation
73 """Wrapper for non-list/dict object to support map operation
74
74
75 This class allows us to handle both:
75 This class allows us to handle both:
76 - "{manifest}"
76 - "{manifest}"
77 - "{manifest % '{rev}:{node}'}"
77 - "{manifest % '{rev}:{node}'}"
78 - "{manifest.rev}"
78 - "{manifest.rev}"
79
79
80 Unlike a _hybrid, this does not simulate the behavior of the underling
80 Unlike a _hybrid, this does not simulate the behavior of the underling
81 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
81 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
82 """
82 """
83
83
84 def __init__(self, gen, key, value, makemap):
84 def __init__(self, gen, key, value, makemap):
85 if gen is not None:
85 if gen is not None:
86 self.gen = gen # generator or function returning generator
86 self.gen = gen # generator or function returning generator
87 self._key = key
87 self._key = key
88 self._value = value # may be generator of strings
88 self._value = value # may be generator of strings
89 self._makemap = makemap
89 self._makemap = makemap
90
90
91 def gen(self):
91 def gen(self):
92 yield pycompat.bytestr(self._value)
92 yield pycompat.bytestr(self._value)
93
93
94 def tomap(self):
94 def tomap(self):
95 return self._makemap(self._key)
95 return self._makemap(self._key)
96
96
97 def itermaps(self):
97 def itermaps(self):
98 yield self.tomap()
98 yield self.tomap()
99
99
100 def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None):
100 def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None):
101 """Wrap data to support both dict-like and string-like operations"""
101 """Wrap data to support both dict-like and string-like operations"""
102 return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
102 return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
103 lambda k: fmt % (k, data[k]))
103 lambda k: fmt % (k, data[k]))
104
104
105 def hybridlist(data, name, fmt='%s', gen=None):
105 def hybridlist(data, name, fmt='%s', gen=None):
106 """Wrap data to support both list-like and string-like operations"""
106 """Wrap data to support both list-like and string-like operations"""
107 return _hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % x)
107 return _hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % x)
108
108
109 def unwraphybrid(thing):
109 def unwraphybrid(thing):
110 """Return an object which can be stringified possibly by using a legacy
110 """Return an object which can be stringified possibly by using a legacy
111 template"""
111 template"""
112 gen = getattr(thing, 'gen', None)
112 gen = getattr(thing, 'gen', None)
113 if gen is None:
113 if gen is None:
114 return thing
114 return thing
115 if callable(gen):
115 if callable(gen):
116 return gen()
116 return gen()
117 return gen
117 return gen
118
118
119 def unwrapvalue(thing):
119 def unwrapvalue(thing):
120 """Move the inner value object out of the wrapper"""
120 """Move the inner value object out of the wrapper"""
121 if not util.safehasattr(thing, '_value'):
121 if not util.safehasattr(thing, '_value'):
122 return thing
122 return thing
123 return thing._value
123 return thing._value
124
124
125 def wraphybridvalue(container, key, value):
125 def wraphybridvalue(container, key, value):
126 """Wrap an element of hybrid container to be mappable
126 """Wrap an element of hybrid container to be mappable
127
127
128 The key is passed to the makemap function of the given container, which
128 The key is passed to the makemap function of the given container, which
129 should be an item generated by iter(container).
129 should be an item generated by iter(container).
130 """
130 """
131 makemap = getattr(container, '_makemap', None)
131 makemap = getattr(container, '_makemap', None)
132 if makemap is None:
132 if makemap is None:
133 return value
133 return value
134 if util.safehasattr(value, '_makemap'):
134 if util.safehasattr(value, '_makemap'):
135 # a nested hybrid list/dict, which has its own way of map operation
135 # a nested hybrid list/dict, which has its own way of map operation
136 return value
136 return value
137 return _mappable(None, key, value, makemap)
137 return _mappable(None, key, value, makemap)
138
138
139 def compatdict(context, mapping, name, data, key='key', value='value',
139 def compatdict(context, mapping, name, data, key='key', value='value',
140 fmt='%s=%s', plural=None, separator=' '):
140 fmt='%s=%s', plural=None, separator=' '):
141 """Wrap data like hybriddict(), but also supports old-style list template
141 """Wrap data like hybriddict(), but also supports old-style list template
142
142
143 This exists for backward compatibility with the old-style template. Use
143 This exists for backward compatibility with the old-style template. Use
144 hybriddict() for new template keywords.
144 hybriddict() for new template keywords.
145 """
145 """
146 c = [{key: k, value: v} for k, v in data.iteritems()]
146 c = [{key: k, value: v} for k, v in data.iteritems()]
147 t = context.resource(mapping, 'templ')
147 t = context.resource(mapping, 'templ')
148 f = _showlist(name, c, t, mapping, plural, separator)
148 f = _showlist(name, c, t, mapping, plural, separator)
149 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
149 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
150
150
151 def compatlist(context, mapping, name, data, element=None, fmt='%s',
151 def compatlist(context, mapping, name, data, element=None, fmt='%s',
152 plural=None, separator=' '):
152 plural=None, separator=' '):
153 """Wrap data like hybridlist(), but also supports old-style list template
153 """Wrap data like hybridlist(), but also supports old-style list template
154
154
155 This exists for backward compatibility with the old-style template. Use
155 This exists for backward compatibility with the old-style template. Use
156 hybridlist() for new template keywords.
156 hybridlist() for new template keywords.
157 """
157 """
158 t = context.resource(mapping, 'templ')
158 t = context.resource(mapping, 'templ')
159 f = _showlist(name, data, t, mapping, plural, separator)
159 f = _showlist(name, data, t, mapping, plural, separator)
160 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
160 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
161
161
162 def showdict(name, data, mapping, plural=None, key='key', value='value',
162 def showdict(name, data, mapping, plural=None, key='key', value='value',
163 fmt='%s=%s', separator=' '):
163 fmt='%s=%s', separator=' '):
164 c = [{key: k, value: v} for k, v in data.iteritems()]
164 c = [{key: k, value: v} for k, v in data.iteritems()]
165 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
165 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
166 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
166 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
167
167
168 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
168 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
169 if not element:
169 if not element:
170 element = name
170 element = name
171 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
171 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
172 return hybridlist(values, name=element, gen=f)
172 return hybridlist(values, name=element, gen=f)
173
173
174 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
174 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
175 '''expand set of values.
175 '''expand set of values.
176 name is name of key in template map.
176 name is name of key in template map.
177 values is list of strings or dicts.
177 values is list of strings or dicts.
178 plural is plural of name, if not simply name + 's'.
178 plural is plural of name, if not simply name + 's'.
179 separator is used to join values as a string
179 separator is used to join values as a string
180
180
181 expansion works like this, given name 'foo'.
181 expansion works like this, given name 'foo'.
182
182
183 if values is empty, expand 'no_foos'.
183 if values is empty, expand 'no_foos'.
184
184
185 if 'foo' not in template map, return values as a string,
185 if 'foo' not in template map, return values as a string,
186 joined by 'separator'.
186 joined by 'separator'.
187
187
188 expand 'start_foos'.
188 expand 'start_foos'.
189
189
190 for each value, expand 'foo'. if 'last_foo' in template
190 for each value, expand 'foo'. if 'last_foo' in template
191 map, expand it instead of 'foo' for last key.
191 map, expand it instead of 'foo' for last key.
192
192
193 expand 'end_foos'.
193 expand 'end_foos'.
194 '''
194 '''
195 strmapping = pycompat.strkwargs(mapping)
195 strmapping = pycompat.strkwargs(mapping)
196 if not plural:
196 if not plural:
197 plural = name + 's'
197 plural = name + 's'
198 if not values:
198 if not values:
199 noname = 'no_' + plural
199 noname = 'no_' + plural
200 if noname in templ:
200 if noname in templ:
201 yield templ(noname, **strmapping)
201 yield templ(noname, **strmapping)
202 return
202 return
203 if name not in templ:
203 if name not in templ:
204 if isinstance(values[0], bytes):
204 if isinstance(values[0], bytes):
205 yield separator.join(values)
205 yield separator.join(values)
206 else:
206 else:
207 for v in values:
207 for v in values:
208 r = dict(v)
208 r = dict(v)
209 r.update(mapping)
209 r.update(mapping)
210 yield r
210 yield r
211 return
211 return
212 startname = 'start_' + plural
212 startname = 'start_' + plural
213 if startname in templ:
213 if startname in templ:
214 yield templ(startname, **strmapping)
214 yield templ(startname, **strmapping)
215 vmapping = mapping.copy()
215 vmapping = mapping.copy()
216 def one(v, tag=name):
216 def one(v, tag=name):
217 try:
217 try:
218 vmapping.update(v)
218 vmapping.update(v)
219 # Python 2 raises ValueError if the type of v is wrong. Python
219 # Python 2 raises ValueError if the type of v is wrong. Python
220 # 3 raises TypeError.
220 # 3 raises TypeError.
221 except (AttributeError, TypeError, ValueError):
221 except (AttributeError, TypeError, ValueError):
222 try:
222 try:
223 # Python 2 raises ValueError trying to destructure an e.g.
223 # Python 2 raises ValueError trying to destructure an e.g.
224 # bytes. Python 3 raises TypeError.
224 # bytes. Python 3 raises TypeError.
225 for a, b in v:
225 for a, b in v:
226 vmapping[a] = b
226 vmapping[a] = b
227 except (TypeError, ValueError):
227 except (TypeError, ValueError):
228 vmapping[name] = v
228 vmapping[name] = v
229 return templ(tag, **pycompat.strkwargs(vmapping))
229 return templ(tag, **pycompat.strkwargs(vmapping))
230 lastname = 'last_' + name
230 lastname = 'last_' + name
231 if lastname in templ:
231 if lastname in templ:
232 last = values.pop()
232 last = values.pop()
233 else:
233 else:
234 last = None
234 last = None
235 for v in values:
235 for v in values:
236 yield one(v)
236 yield one(v)
237 if last is not None:
237 if last is not None:
238 yield one(last, tag=lastname)
238 yield one(last, tag=lastname)
239 endname = 'end_' + plural
239 endname = 'end_' + plural
240 if endname in templ:
240 if endname in templ:
241 yield templ(endname, **strmapping)
241 yield templ(endname, **strmapping)
242
242
243 def getlatesttags(repo, ctx, cache, pattern=None):
243 def getlatesttags(repo, ctx, cache, pattern=None):
244 '''return date, distance and name for the latest tag of rev'''
244 '''return date, distance and name for the latest tag of rev'''
245
245
246 cachename = 'latesttags'
246 cachename = 'latesttags'
247 if pattern is not None:
247 if pattern is not None:
248 cachename += '-' + pattern
248 cachename += '-' + pattern
249 match = util.stringmatcher(pattern)[2]
249 match = util.stringmatcher(pattern)[2]
250 else:
250 else:
251 match = util.always
251 match = util.always
252
252
253 if cachename not in cache:
253 if cachename not in cache:
254 # Cache mapping from rev to a tuple with tag date, tag
254 # Cache mapping from rev to a tuple with tag date, tag
255 # distance and tag name
255 # distance and tag name
256 cache[cachename] = {-1: (0, 0, ['null'])}
256 cache[cachename] = {-1: (0, 0, ['null'])}
257 latesttags = cache[cachename]
257 latesttags = cache[cachename]
258
258
259 rev = ctx.rev()
259 rev = ctx.rev()
260 todo = [rev]
260 todo = [rev]
261 while todo:
261 while todo:
262 rev = todo.pop()
262 rev = todo.pop()
263 if rev in latesttags:
263 if rev in latesttags:
264 continue
264 continue
265 ctx = repo[rev]
265 ctx = repo[rev]
266 tags = [t for t in ctx.tags()
266 tags = [t for t in ctx.tags()
267 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
267 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
268 and match(t))]
268 and match(t))]
269 if tags:
269 if tags:
270 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
270 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
271 continue
271 continue
272 try:
272 try:
273 ptags = [latesttags[p.rev()] for p in ctx.parents()]
273 ptags = [latesttags[p.rev()] for p in ctx.parents()]
274 if len(ptags) > 1:
274 if len(ptags) > 1:
275 if ptags[0][2] == ptags[1][2]:
275 if ptags[0][2] == ptags[1][2]:
276 # The tuples are laid out so the right one can be found by
276 # The tuples are laid out so the right one can be found by
277 # comparison in this case.
277 # comparison in this case.
278 pdate, pdist, ptag = max(ptags)
278 pdate, pdist, ptag = max(ptags)
279 else:
279 else:
280 def key(x):
280 def key(x):
281 changessincetag = len(repo.revs('only(%d, %s)',
281 changessincetag = len(repo.revs('only(%d, %s)',
282 ctx.rev(), x[2][0]))
282 ctx.rev(), x[2][0]))
283 # Smallest number of changes since tag wins. Date is
283 # Smallest number of changes since tag wins. Date is
284 # used as tiebreaker.
284 # used as tiebreaker.
285 return [-changessincetag, x[0]]
285 return [-changessincetag, x[0]]
286 pdate, pdist, ptag = max(ptags, key=key)
286 pdate, pdist, ptag = max(ptags, key=key)
287 else:
287 else:
288 pdate, pdist, ptag = ptags[0]
288 pdate, pdist, ptag = ptags[0]
289 except KeyError:
289 except KeyError:
290 # Cache miss - recurse
290 # Cache miss - recurse
291 todo.append(rev)
291 todo.append(rev)
292 todo.extend(p.rev() for p in ctx.parents())
292 todo.extend(p.rev() for p in ctx.parents())
293 continue
293 continue
294 latesttags[rev] = pdate, pdist + 1, ptag
294 latesttags[rev] = pdate, pdist + 1, ptag
295 return latesttags[rev]
295 return latesttags[rev]
296
296
297 def getrenamedfn(repo, endrev=None):
297 def getrenamedfn(repo, endrev=None):
298 rcache = {}
298 rcache = {}
299 if endrev is None:
299 if endrev is None:
300 endrev = len(repo)
300 endrev = len(repo)
301
301
302 def getrenamed(fn, rev):
302 def getrenamed(fn, rev):
303 '''looks up all renames for a file (up to endrev) the first
303 '''looks up all renames for a file (up to endrev) the first
304 time the file is given. It indexes on the changerev and only
304 time the file is given. It indexes on the changerev and only
305 parses the manifest if linkrev != changerev.
305 parses the manifest if linkrev != changerev.
306 Returns rename info for fn at changerev rev.'''
306 Returns rename info for fn at changerev rev.'''
307 if fn not in rcache:
307 if fn not in rcache:
308 rcache[fn] = {}
308 rcache[fn] = {}
309 fl = repo.file(fn)
309 fl = repo.file(fn)
310 for i in fl:
310 for i in fl:
311 lr = fl.linkrev(i)
311 lr = fl.linkrev(i)
312 renamed = fl.renamed(fl.node(i))
312 renamed = fl.renamed(fl.node(i))
313 rcache[fn][lr] = renamed
313 rcache[fn][lr] = renamed
314 if lr >= endrev:
314 if lr >= endrev:
315 break
315 break
316 if rev in rcache[fn]:
316 if rev in rcache[fn]:
317 return rcache[fn][rev]
317 return rcache[fn][rev]
318
318
319 # If linkrev != rev (i.e. rev not found in rcache) fallback to
319 # If linkrev != rev (i.e. rev not found in rcache) fallback to
320 # filectx logic.
320 # filectx logic.
321 try:
321 try:
322 return repo[rev][fn].renamed()
322 return repo[rev][fn].renamed()
323 except error.LookupError:
323 except error.LookupError:
324 return None
324 return None
325
325
326 return getrenamed
326 return getrenamed
327
327
328 def getlogcolumns():
328 def getlogcolumns():
329 """Return a dict of log column labels"""
329 """Return a dict of log column labels"""
330 _ = pycompat.identity # temporarily disable gettext
330 _ = pycompat.identity # temporarily disable gettext
331 # i18n: column positioning for "hg log"
331 # i18n: column positioning for "hg log"
332 columns = _('bookmark: %s\n'
332 columns = _('bookmark: %s\n'
333 'branch: %s\n'
333 'branch: %s\n'
334 'changeset: %s\n'
334 'changeset: %s\n'
335 'copies: %s\n'
335 'copies: %s\n'
336 'date: %s\n'
336 'date: %s\n'
337 'extra: %s=%s\n'
337 'extra: %s=%s\n'
338 'files+: %s\n'
338 'files+: %s\n'
339 'files-: %s\n'
339 'files-: %s\n'
340 'files: %s\n'
340 'files: %s\n'
341 'instability: %s\n'
341 'instability: %s\n'
342 'manifest: %s\n'
342 'manifest: %s\n'
343 'obsolete: %s\n'
343 'obsolete: %s\n'
344 'parent: %s\n'
344 'parent: %s\n'
345 'phase: %s\n'
345 'phase: %s\n'
346 'summary: %s\n'
346 'summary: %s\n'
347 'tag: %s\n'
347 'tag: %s\n'
348 'user: %s\n')
348 'user: %s\n')
349 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
349 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
350 i18n._(columns).splitlines(True)))
350 i18n._(columns).splitlines(True)))
351
351
352 # default templates internally used for rendering of lists
352 # default templates internally used for rendering of lists
353 defaulttempl = {
353 defaulttempl = {
354 'parent': '{rev}:{node|formatnode} ',
354 'parent': '{rev}:{node|formatnode} ',
355 'manifest': '{rev}:{node|formatnode}',
355 'manifest': '{rev}:{node|formatnode}',
356 'file_copy': '{name} ({source})',
356 'file_copy': '{name} ({source})',
357 'envvar': '{key}={value}',
357 'envvar': '{key}={value}',
358 'extra': '{key}={value|stringescape}'
358 'extra': '{key}={value|stringescape}'
359 }
359 }
360 # filecopy is preserved for compatibility reasons
360 # filecopy is preserved for compatibility reasons
361 defaulttempl['filecopy'] = defaulttempl['file_copy']
361 defaulttempl['filecopy'] = defaulttempl['file_copy']
362
362
363 # keywords are callables (see registrar.templatekeyword for details)
363 # keywords are callables (see registrar.templatekeyword for details)
364 keywords = {}
364 keywords = {}
365 templatekeyword = registrar.templatekeyword(keywords)
365 templatekeyword = registrar.templatekeyword(keywords)
366
366
367 @templatekeyword('author', requires={'ctx'})
367 @templatekeyword('author', requires={'ctx'})
368 def showauthor(context, mapping):
368 def showauthor(context, mapping):
369 """String. The unmodified author of the changeset."""
369 """String. The unmodified author of the changeset."""
370 ctx = context.resource(mapping, 'ctx')
370 ctx = context.resource(mapping, 'ctx')
371 return ctx.user()
371 return ctx.user()
372
372
373 @templatekeyword('bisect', requires={'repo', 'ctx'})
373 @templatekeyword('bisect', requires={'repo', 'ctx'})
374 def showbisect(context, mapping):
374 def showbisect(context, mapping):
375 """String. The changeset bisection status."""
375 """String. The changeset bisection status."""
376 repo = context.resource(mapping, 'repo')
376 repo = context.resource(mapping, 'repo')
377 ctx = context.resource(mapping, 'ctx')
377 ctx = context.resource(mapping, 'ctx')
378 return hbisect.label(repo, ctx.node())
378 return hbisect.label(repo, ctx.node())
379
379
380 @templatekeyword('branch', requires={'ctx'})
380 @templatekeyword('branch', requires={'ctx'})
381 def showbranch(context, mapping):
381 def showbranch(context, mapping):
382 """String. The name of the branch on which the changeset was
382 """String. The name of the branch on which the changeset was
383 committed.
383 committed.
384 """
384 """
385 ctx = context.resource(mapping, 'ctx')
385 ctx = context.resource(mapping, 'ctx')
386 return ctx.branch()
386 return ctx.branch()
387
387
388 @templatekeyword('branches')
388 @templatekeyword('branches', requires={'ctx', 'templ'})
389 def showbranches(**args):
389 def showbranches(context, mapping):
390 """List of strings. The name of the branch on which the
390 """List of strings. The name of the branch on which the
391 changeset was committed. Will be empty if the branch name was
391 changeset was committed. Will be empty if the branch name was
392 default. (DEPRECATED)
392 default. (DEPRECATED)
393 """
393 """
394 args = pycompat.byteskwargs(args)
394 ctx = context.resource(mapping, 'ctx')
395 branch = args['ctx'].branch()
395 branch = ctx.branch()
396 if branch != 'default':
396 if branch != 'default':
397 return showlist('branch', [branch], args, plural='branches')
397 return compatlist(context, mapping, 'branch', [branch],
398 return showlist('branch', [], args, plural='branches')
398 plural='branches')
399 return compatlist(context, mapping, 'branch', [], plural='branches')
399
400
400 @templatekeyword('bookmarks')
401 @templatekeyword('bookmarks')
401 def showbookmarks(**args):
402 def showbookmarks(**args):
402 """List of strings. Any bookmarks associated with the
403 """List of strings. Any bookmarks associated with the
403 changeset. Also sets 'active', the name of the active bookmark.
404 changeset. Also sets 'active', the name of the active bookmark.
404 """
405 """
405 args = pycompat.byteskwargs(args)
406 args = pycompat.byteskwargs(args)
406 repo = args['ctx']._repo
407 repo = args['ctx']._repo
407 bookmarks = args['ctx'].bookmarks()
408 bookmarks = args['ctx'].bookmarks()
408 active = repo._activebookmark
409 active = repo._activebookmark
409 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
410 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
410 f = _showlist('bookmark', bookmarks, args['templ'], args)
411 f = _showlist('bookmark', bookmarks, args['templ'], args)
411 return _hybrid(f, bookmarks, makemap, pycompat.identity)
412 return _hybrid(f, bookmarks, makemap, pycompat.identity)
412
413
413 @templatekeyword('children', requires={'ctx', 'templ'})
414 @templatekeyword('children', requires={'ctx', 'templ'})
414 def showchildren(context, mapping):
415 def showchildren(context, mapping):
415 """List of strings. The children of the changeset."""
416 """List of strings. The children of the changeset."""
416 ctx = context.resource(mapping, 'ctx')
417 ctx = context.resource(mapping, 'ctx')
417 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
418 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
418 return compatlist(context, mapping, 'children', childrevs, element='child')
419 return compatlist(context, mapping, 'children', childrevs, element='child')
419
420
420 # Deprecated, but kept alive for help generation a purpose.
421 # Deprecated, but kept alive for help generation a purpose.
421 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
422 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
422 def showcurrentbookmark(context, mapping):
423 def showcurrentbookmark(context, mapping):
423 """String. The active bookmark, if it is associated with the changeset.
424 """String. The active bookmark, if it is associated with the changeset.
424 (DEPRECATED)"""
425 (DEPRECATED)"""
425 return showactivebookmark(context, mapping)
426 return showactivebookmark(context, mapping)
426
427
427 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
428 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
428 def showactivebookmark(context, mapping):
429 def showactivebookmark(context, mapping):
429 """String. The active bookmark, if it is associated with the changeset."""
430 """String. The active bookmark, if it is associated with the changeset."""
430 repo = context.resource(mapping, 'repo')
431 repo = context.resource(mapping, 'repo')
431 ctx = context.resource(mapping, 'ctx')
432 ctx = context.resource(mapping, 'ctx')
432 active = repo._activebookmark
433 active = repo._activebookmark
433 if active and active in ctx.bookmarks():
434 if active and active in ctx.bookmarks():
434 return active
435 return active
435 return ''
436 return ''
436
437
437 @templatekeyword('date', requires={'ctx'})
438 @templatekeyword('date', requires={'ctx'})
438 def showdate(context, mapping):
439 def showdate(context, mapping):
439 """Date information. The date when the changeset was committed."""
440 """Date information. The date when the changeset was committed."""
440 ctx = context.resource(mapping, 'ctx')
441 ctx = context.resource(mapping, 'ctx')
441 return ctx.date()
442 return ctx.date()
442
443
443 @templatekeyword('desc', requires={'ctx'})
444 @templatekeyword('desc', requires={'ctx'})
444 def showdescription(context, mapping):
445 def showdescription(context, mapping):
445 """String. The text of the changeset description."""
446 """String. The text of the changeset description."""
446 ctx = context.resource(mapping, 'ctx')
447 ctx = context.resource(mapping, 'ctx')
447 s = ctx.description()
448 s = ctx.description()
448 if isinstance(s, encoding.localstr):
449 if isinstance(s, encoding.localstr):
449 # try hard to preserve utf-8 bytes
450 # try hard to preserve utf-8 bytes
450 return encoding.tolocal(encoding.fromlocal(s).strip())
451 return encoding.tolocal(encoding.fromlocal(s).strip())
451 else:
452 else:
452 return s.strip()
453 return s.strip()
453
454
454 @templatekeyword('diffstat', requires={'ctx'})
455 @templatekeyword('diffstat', requires={'ctx'})
455 def showdiffstat(context, mapping):
456 def showdiffstat(context, mapping):
456 """String. Statistics of changes with the following format:
457 """String. Statistics of changes with the following format:
457 "modified files: +added/-removed lines"
458 "modified files: +added/-removed lines"
458 """
459 """
459 ctx = context.resource(mapping, 'ctx')
460 ctx = context.resource(mapping, 'ctx')
460 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
461 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
461 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
462 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
462 return '%d: +%d/-%d' % (len(stats), adds, removes)
463 return '%d: +%d/-%d' % (len(stats), adds, removes)
463
464
464 @templatekeyword('envvars', requires={'ui', 'templ'})
465 @templatekeyword('envvars', requires={'ui', 'templ'})
465 def showenvvars(context, mapping):
466 def showenvvars(context, mapping):
466 """A dictionary of environment variables. (EXPERIMENTAL)"""
467 """A dictionary of environment variables. (EXPERIMENTAL)"""
467 ui = context.resource(mapping, 'ui')
468 ui = context.resource(mapping, 'ui')
468 env = ui.exportableenviron()
469 env = ui.exportableenviron()
469 env = util.sortdict((k, env[k]) for k in sorted(env))
470 env = util.sortdict((k, env[k]) for k in sorted(env))
470 return compatdict(context, mapping, 'envvar', env, plural='envvars')
471 return compatdict(context, mapping, 'envvar', env, plural='envvars')
471
472
472 @templatekeyword('extras')
473 @templatekeyword('extras')
473 def showextras(**args):
474 def showextras(**args):
474 """List of dicts with key, value entries of the 'extras'
475 """List of dicts with key, value entries of the 'extras'
475 field of this changeset."""
476 field of this changeset."""
476 args = pycompat.byteskwargs(args)
477 args = pycompat.byteskwargs(args)
477 extras = args['ctx'].extra()
478 extras = args['ctx'].extra()
478 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
479 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
479 makemap = lambda k: {'key': k, 'value': extras[k]}
480 makemap = lambda k: {'key': k, 'value': extras[k]}
480 c = [makemap(k) for k in extras]
481 c = [makemap(k) for k in extras]
481 f = _showlist('extra', c, args['templ'], args, plural='extras')
482 f = _showlist('extra', c, args['templ'], args, plural='extras')
482 return _hybrid(f, extras, makemap,
483 return _hybrid(f, extras, makemap,
483 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
484 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
484
485
485 def _showfilesbystat(args, name, index):
486 def _showfilesbystat(context, mapping, name, index):
486 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
487 repo = context.resource(mapping, 'repo')
488 ctx = context.resource(mapping, 'ctx')
489 revcache = context.resource(mapping, 'revcache')
487 if 'files' not in revcache:
490 if 'files' not in revcache:
488 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
491 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
489 files = revcache['files'][index]
492 files = revcache['files'][index]
490 return showlist(name, files, args, element='file')
493 return compatlist(context, mapping, name, files, element='file')
491
494
492 @templatekeyword('file_adds')
495 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache', 'templ'})
493 def showfileadds(**args):
496 def showfileadds(context, mapping):
494 """List of strings. Files added by this changeset."""
497 """List of strings. Files added by this changeset."""
495 args = pycompat.byteskwargs(args)
498 return _showfilesbystat(context, mapping, 'file_add', 1)
496 return _showfilesbystat(args, 'file_add', 1)
497
499
498 @templatekeyword('file_copies',
500 @templatekeyword('file_copies',
499 requires={'repo', 'ctx', 'cache', 'revcache', 'templ'})
501 requires={'repo', 'ctx', 'cache', 'revcache', 'templ'})
500 def showfilecopies(context, mapping):
502 def showfilecopies(context, mapping):
501 """List of strings. Files copied in this changeset with
503 """List of strings. Files copied in this changeset with
502 their sources.
504 their sources.
503 """
505 """
504 repo = context.resource(mapping, 'repo')
506 repo = context.resource(mapping, 'repo')
505 ctx = context.resource(mapping, 'ctx')
507 ctx = context.resource(mapping, 'ctx')
506 cache = context.resource(mapping, 'cache')
508 cache = context.resource(mapping, 'cache')
507 copies = context.resource(mapping, 'revcache').get('copies')
509 copies = context.resource(mapping, 'revcache').get('copies')
508 if copies is None:
510 if copies is None:
509 if 'getrenamed' not in cache:
511 if 'getrenamed' not in cache:
510 cache['getrenamed'] = getrenamedfn(repo)
512 cache['getrenamed'] = getrenamedfn(repo)
511 copies = []
513 copies = []
512 getrenamed = cache['getrenamed']
514 getrenamed = cache['getrenamed']
513 for fn in ctx.files():
515 for fn in ctx.files():
514 rename = getrenamed(fn, ctx.rev())
516 rename = getrenamed(fn, ctx.rev())
515 if rename:
517 if rename:
516 copies.append((fn, rename[0]))
518 copies.append((fn, rename[0]))
517
519
518 copies = util.sortdict(copies)
520 copies = util.sortdict(copies)
519 return compatdict(context, mapping, 'file_copy', copies,
521 return compatdict(context, mapping, 'file_copy', copies,
520 key='name', value='source', fmt='%s (%s)',
522 key='name', value='source', fmt='%s (%s)',
521 plural='file_copies')
523 plural='file_copies')
522
524
523 # showfilecopiesswitch() displays file copies only if copy records are
525 # showfilecopiesswitch() displays file copies only if copy records are
524 # provided before calling the templater, usually with a --copies
526 # provided before calling the templater, usually with a --copies
525 # command line switch.
527 # command line switch.
526 @templatekeyword('file_copies_switch', requires={'revcache', 'templ'})
528 @templatekeyword('file_copies_switch', requires={'revcache', 'templ'})
527 def showfilecopiesswitch(context, mapping):
529 def showfilecopiesswitch(context, mapping):
528 """List of strings. Like "file_copies" but displayed
530 """List of strings. Like "file_copies" but displayed
529 only if the --copied switch is set.
531 only if the --copied switch is set.
530 """
532 """
531 copies = context.resource(mapping, 'revcache').get('copies') or []
533 copies = context.resource(mapping, 'revcache').get('copies') or []
532 copies = util.sortdict(copies)
534 copies = util.sortdict(copies)
533 return compatdict(context, mapping, 'file_copy', copies,
535 return compatdict(context, mapping, 'file_copy', copies,
534 key='name', value='source', fmt='%s (%s)',
536 key='name', value='source', fmt='%s (%s)',
535 plural='file_copies')
537 plural='file_copies')
536
538
537 @templatekeyword('file_dels')
539 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache', 'templ'})
538 def showfiledels(**args):
540 def showfiledels(context, mapping):
539 """List of strings. Files removed by this changeset."""
541 """List of strings. Files removed by this changeset."""
540 args = pycompat.byteskwargs(args)
542 return _showfilesbystat(context, mapping, 'file_del', 2)
541 return _showfilesbystat(args, 'file_del', 2)
542
543
543 @templatekeyword('file_mods')
544 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache', 'templ'})
544 def showfilemods(**args):
545 def showfilemods(context, mapping):
545 """List of strings. Files modified by this changeset."""
546 """List of strings. Files modified by this changeset."""
546 args = pycompat.byteskwargs(args)
547 return _showfilesbystat(context, mapping, 'file_mod', 0)
547 return _showfilesbystat(args, 'file_mod', 0)
548
548
549 @templatekeyword('files')
549 @templatekeyword('files', requires={'ctx', 'templ'})
550 def showfiles(**args):
550 def showfiles(context, mapping):
551 """List of strings. All files modified, added, or removed by this
551 """List of strings. All files modified, added, or removed by this
552 changeset.
552 changeset.
553 """
553 """
554 args = pycompat.byteskwargs(args)
554 ctx = context.resource(mapping, 'ctx')
555 return showlist('file', args['ctx'].files(), args)
555 return compatlist(context, mapping, 'file', ctx.files())
556
556
557 @templatekeyword('graphnode', requires={'repo', 'ctx'})
557 @templatekeyword('graphnode', requires={'repo', 'ctx'})
558 def showgraphnode(context, mapping):
558 def showgraphnode(context, mapping):
559 """String. The character representing the changeset node in an ASCII
559 """String. The character representing the changeset node in an ASCII
560 revision graph."""
560 revision graph."""
561 repo = context.resource(mapping, 'repo')
561 repo = context.resource(mapping, 'repo')
562 ctx = context.resource(mapping, 'ctx')
562 ctx = context.resource(mapping, 'ctx')
563 return getgraphnode(repo, ctx)
563 return getgraphnode(repo, ctx)
564
564
565 def getgraphnode(repo, ctx):
565 def getgraphnode(repo, ctx):
566 wpnodes = repo.dirstate.parents()
566 wpnodes = repo.dirstate.parents()
567 if wpnodes[1] == nullid:
567 if wpnodes[1] == nullid:
568 wpnodes = wpnodes[:1]
568 wpnodes = wpnodes[:1]
569 if ctx.node() in wpnodes:
569 if ctx.node() in wpnodes:
570 return '@'
570 return '@'
571 elif ctx.obsolete():
571 elif ctx.obsolete():
572 return 'x'
572 return 'x'
573 elif ctx.isunstable():
573 elif ctx.isunstable():
574 return '*'
574 return '*'
575 elif ctx.closesbranch():
575 elif ctx.closesbranch():
576 return '_'
576 return '_'
577 else:
577 else:
578 return 'o'
578 return 'o'
579
579
580 @templatekeyword('graphwidth', requires=())
580 @templatekeyword('graphwidth', requires=())
581 def showgraphwidth(context, mapping):
581 def showgraphwidth(context, mapping):
582 """Integer. The width of the graph drawn by 'log --graph' or zero."""
582 """Integer. The width of the graph drawn by 'log --graph' or zero."""
583 # just hosts documentation; should be overridden by template mapping
583 # just hosts documentation; should be overridden by template mapping
584 return 0
584 return 0
585
585
586 @templatekeyword('index', requires=())
586 @templatekeyword('index', requires=())
587 def showindex(context, mapping):
587 def showindex(context, mapping):
588 """Integer. The current iteration of the loop. (0 indexed)"""
588 """Integer. The current iteration of the loop. (0 indexed)"""
589 # just hosts documentation; should be overridden by template mapping
589 # just hosts documentation; should be overridden by template mapping
590 raise error.Abort(_("can't use index in this context"))
590 raise error.Abort(_("can't use index in this context"))
591
591
592 @templatekeyword('latesttag')
592 @templatekeyword('latesttag')
593 def showlatesttag(**args):
593 def showlatesttag(**args):
594 """List of strings. The global tags on the most recent globally
594 """List of strings. The global tags on the most recent globally
595 tagged ancestor of this changeset. If no such tags exist, the list
595 tagged ancestor of this changeset. If no such tags exist, the list
596 consists of the single string "null".
596 consists of the single string "null".
597 """
597 """
598 return showlatesttags(None, **args)
598 return showlatesttags(None, **args)
599
599
600 def showlatesttags(pattern, **args):
600 def showlatesttags(pattern, **args):
601 """helper method for the latesttag keyword and function"""
601 """helper method for the latesttag keyword and function"""
602 args = pycompat.byteskwargs(args)
602 args = pycompat.byteskwargs(args)
603 repo, ctx = args['repo'], args['ctx']
603 repo, ctx = args['repo'], args['ctx']
604 cache = args['cache']
604 cache = args['cache']
605 latesttags = getlatesttags(repo, ctx, cache, pattern)
605 latesttags = getlatesttags(repo, ctx, cache, pattern)
606
606
607 # latesttag[0] is an implementation detail for sorting csets on different
607 # latesttag[0] is an implementation detail for sorting csets on different
608 # branches in a stable manner- it is the date the tagged cset was created,
608 # branches in a stable manner- it is the date the tagged cset was created,
609 # not the date the tag was created. Therefore it isn't made visible here.
609 # not the date the tag was created. Therefore it isn't made visible here.
610 makemap = lambda v: {
610 makemap = lambda v: {
611 'changes': _showchangessincetag,
611 'changes': _showchangessincetag,
612 'distance': latesttags[1],
612 'distance': latesttags[1],
613 'latesttag': v, # BC with {latesttag % '{latesttag}'}
613 'latesttag': v, # BC with {latesttag % '{latesttag}'}
614 'tag': v
614 'tag': v
615 }
615 }
616
616
617 tags = latesttags[2]
617 tags = latesttags[2]
618 f = _showlist('latesttag', tags, args['templ'], args, separator=':')
618 f = _showlist('latesttag', tags, args['templ'], args, separator=':')
619 return _hybrid(f, tags, makemap, pycompat.identity)
619 return _hybrid(f, tags, makemap, pycompat.identity)
620
620
621 @templatekeyword('latesttagdistance')
621 @templatekeyword('latesttagdistance')
622 def showlatesttagdistance(repo, ctx, templ, cache, **args):
622 def showlatesttagdistance(repo, ctx, templ, cache, **args):
623 """Integer. Longest path to the latest tag."""
623 """Integer. Longest path to the latest tag."""
624 return getlatesttags(repo, ctx, cache)[1]
624 return getlatesttags(repo, ctx, cache)[1]
625
625
626 @templatekeyword('changessincelatesttag')
626 @templatekeyword('changessincelatesttag')
627 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
627 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
628 """Integer. All ancestors not in the latest tag."""
628 """Integer. All ancestors not in the latest tag."""
629 latesttag = getlatesttags(repo, ctx, cache)[2][0]
629 latesttag = getlatesttags(repo, ctx, cache)[2][0]
630
630
631 return _showchangessincetag(repo, ctx, tag=latesttag, **args)
631 return _showchangessincetag(repo, ctx, tag=latesttag, **args)
632
632
633 def _showchangessincetag(repo, ctx, **args):
633 def _showchangessincetag(repo, ctx, **args):
634 offset = 0
634 offset = 0
635 revs = [ctx.rev()]
635 revs = [ctx.rev()]
636 tag = args[r'tag']
636 tag = args[r'tag']
637
637
638 # The only() revset doesn't currently support wdir()
638 # The only() revset doesn't currently support wdir()
639 if ctx.rev() is None:
639 if ctx.rev() is None:
640 offset = 1
640 offset = 1
641 revs = [p.rev() for p in ctx.parents()]
641 revs = [p.rev() for p in ctx.parents()]
642
642
643 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
643 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
644
644
645 @templatekeyword('manifest')
645 @templatekeyword('manifest')
646 def showmanifest(**args):
646 def showmanifest(**args):
647 repo, ctx, templ = args[r'repo'], args[r'ctx'], args[r'templ']
647 repo, ctx, templ = args[r'repo'], args[r'ctx'], args[r'templ']
648 mnode = ctx.manifestnode()
648 mnode = ctx.manifestnode()
649 if mnode is None:
649 if mnode is None:
650 # just avoid crash, we might want to use the 'ff...' hash in future
650 # just avoid crash, we might want to use the 'ff...' hash in future
651 return
651 return
652 mrev = repo.manifestlog._revlog.rev(mnode)
652 mrev = repo.manifestlog._revlog.rev(mnode)
653 mhex = hex(mnode)
653 mhex = hex(mnode)
654 args = args.copy()
654 args = args.copy()
655 args.update({r'rev': mrev, r'node': mhex})
655 args.update({r'rev': mrev, r'node': mhex})
656 f = templ('manifest', **args)
656 f = templ('manifest', **args)
657 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
657 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
658 # rev and node are completely different from changeset's.
658 # rev and node are completely different from changeset's.
659 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
659 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
660
660
661 @templatekeyword('obsfate')
661 @templatekeyword('obsfate')
662 def showobsfate(**args):
662 def showobsfate(**args):
663 # this function returns a list containing pre-formatted obsfate strings.
663 # this function returns a list containing pre-formatted obsfate strings.
664 #
664 #
665 # This function will be replaced by templates fragments when we will have
665 # This function will be replaced by templates fragments when we will have
666 # the verbosity templatekw available.
666 # the verbosity templatekw available.
667 succsandmarkers = showsuccsandmarkers(**args)
667 succsandmarkers = showsuccsandmarkers(**args)
668
668
669 args = pycompat.byteskwargs(args)
669 args = pycompat.byteskwargs(args)
670 ui = args['ui']
670 ui = args['ui']
671
671
672 values = []
672 values = []
673
673
674 for x in succsandmarkers:
674 for x in succsandmarkers:
675 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
675 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
676
676
677 return showlist("fate", values, args)
677 return showlist("fate", values, args)
678
678
679 def shownames(namespace, **args):
679 def shownames(namespace, **args):
680 """helper method to generate a template keyword for a namespace"""
680 """helper method to generate a template keyword for a namespace"""
681 args = pycompat.byteskwargs(args)
681 args = pycompat.byteskwargs(args)
682 ctx = args['ctx']
682 ctx = args['ctx']
683 repo = ctx.repo()
683 repo = ctx.repo()
684 ns = repo.names[namespace]
684 ns = repo.names[namespace]
685 names = ns.names(repo, ctx.node())
685 names = ns.names(repo, ctx.node())
686 return showlist(ns.templatename, names, args, plural=namespace)
686 return showlist(ns.templatename, names, args, plural=namespace)
687
687
688 @templatekeyword('namespaces')
688 @templatekeyword('namespaces')
689 def shownamespaces(**args):
689 def shownamespaces(**args):
690 """Dict of lists. Names attached to this changeset per
690 """Dict of lists. Names attached to this changeset per
691 namespace."""
691 namespace."""
692 args = pycompat.byteskwargs(args)
692 args = pycompat.byteskwargs(args)
693 ctx = args['ctx']
693 ctx = args['ctx']
694 repo = ctx.repo()
694 repo = ctx.repo()
695
695
696 namespaces = util.sortdict()
696 namespaces = util.sortdict()
697 def makensmapfn(ns):
697 def makensmapfn(ns):
698 # 'name' for iterating over namespaces, templatename for local reference
698 # 'name' for iterating over namespaces, templatename for local reference
699 return lambda v: {'name': v, ns.templatename: v}
699 return lambda v: {'name': v, ns.templatename: v}
700
700
701 for k, ns in repo.names.iteritems():
701 for k, ns in repo.names.iteritems():
702 names = ns.names(repo, ctx.node())
702 names = ns.names(repo, ctx.node())
703 f = _showlist('name', names, args['templ'], args)
703 f = _showlist('name', names, args['templ'], args)
704 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
704 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
705
705
706 f = _showlist('namespace', list(namespaces), args['templ'], args)
706 f = _showlist('namespace', list(namespaces), args['templ'], args)
707
707
708 def makemap(ns):
708 def makemap(ns):
709 return {
709 return {
710 'namespace': ns,
710 'namespace': ns,
711 'names': namespaces[ns],
711 'names': namespaces[ns],
712 'builtin': repo.names[ns].builtin,
712 'builtin': repo.names[ns].builtin,
713 'colorname': repo.names[ns].colorname,
713 'colorname': repo.names[ns].colorname,
714 }
714 }
715
715
716 return _hybrid(f, namespaces, makemap, pycompat.identity)
716 return _hybrid(f, namespaces, makemap, pycompat.identity)
717
717
718 @templatekeyword('node', requires={'ctx'})
718 @templatekeyword('node', requires={'ctx'})
719 def shownode(context, mapping):
719 def shownode(context, mapping):
720 """String. The changeset identification hash, as a 40 hexadecimal
720 """String. The changeset identification hash, as a 40 hexadecimal
721 digit string.
721 digit string.
722 """
722 """
723 ctx = context.resource(mapping, 'ctx')
723 ctx = context.resource(mapping, 'ctx')
724 return ctx.hex()
724 return ctx.hex()
725
725
726 @templatekeyword('obsolete', requires={'ctx'})
726 @templatekeyword('obsolete', requires={'ctx'})
727 def showobsolete(context, mapping):
727 def showobsolete(context, mapping):
728 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
728 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
729 ctx = context.resource(mapping, 'ctx')
729 ctx = context.resource(mapping, 'ctx')
730 if ctx.obsolete():
730 if ctx.obsolete():
731 return 'obsolete'
731 return 'obsolete'
732 return ''
732 return ''
733
733
734 @templatekeyword('peerurls', requires={'repo'})
734 @templatekeyword('peerurls', requires={'repo'})
735 def showpeerurls(context, mapping):
735 def showpeerurls(context, mapping):
736 """A dictionary of repository locations defined in the [paths] section
736 """A dictionary of repository locations defined in the [paths] section
737 of your configuration file."""
737 of your configuration file."""
738 repo = context.resource(mapping, 'repo')
738 repo = context.resource(mapping, 'repo')
739 # see commands.paths() for naming of dictionary keys
739 # see commands.paths() for naming of dictionary keys
740 paths = repo.ui.paths
740 paths = repo.ui.paths
741 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
741 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
742 def makemap(k):
742 def makemap(k):
743 p = paths[k]
743 p = paths[k]
744 d = {'name': k, 'url': p.rawloc}
744 d = {'name': k, 'url': p.rawloc}
745 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
745 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
746 return d
746 return d
747 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
747 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
748
748
749 @templatekeyword("predecessors", requires={'repo', 'ctx'})
749 @templatekeyword("predecessors", requires={'repo', 'ctx'})
750 def showpredecessors(context, mapping):
750 def showpredecessors(context, mapping):
751 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
751 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
752 repo = context.resource(mapping, 'repo')
752 repo = context.resource(mapping, 'repo')
753 ctx = context.resource(mapping, 'ctx')
753 ctx = context.resource(mapping, 'ctx')
754 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
754 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
755 predecessors = map(hex, predecessors)
755 predecessors = map(hex, predecessors)
756
756
757 return _hybrid(None, predecessors,
757 return _hybrid(None, predecessors,
758 lambda x: {'ctx': repo[x], 'revcache': {}},
758 lambda x: {'ctx': repo[x], 'revcache': {}},
759 lambda x: scmutil.formatchangeid(repo[x]))
759 lambda x: scmutil.formatchangeid(repo[x]))
760
760
761 @templatekeyword('reporoot', requires={'repo'})
761 @templatekeyword('reporoot', requires={'repo'})
762 def showreporoot(context, mapping):
762 def showreporoot(context, mapping):
763 """String. The root directory of the current repository."""
763 """String. The root directory of the current repository."""
764 repo = context.resource(mapping, 'repo')
764 repo = context.resource(mapping, 'repo')
765 return repo.root
765 return repo.root
766
766
767 @templatekeyword("successorssets", requires={'repo', 'ctx'})
767 @templatekeyword("successorssets", requires={'repo', 'ctx'})
768 def showsuccessorssets(context, mapping):
768 def showsuccessorssets(context, mapping):
769 """Returns a string of sets of successors for a changectx. Format used
769 """Returns a string of sets of successors for a changectx. Format used
770 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
770 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
771 while also diverged into ctx3. (EXPERIMENTAL)"""
771 while also diverged into ctx3. (EXPERIMENTAL)"""
772 repo = context.resource(mapping, 'repo')
772 repo = context.resource(mapping, 'repo')
773 ctx = context.resource(mapping, 'ctx')
773 ctx = context.resource(mapping, 'ctx')
774 if not ctx.obsolete():
774 if not ctx.obsolete():
775 return ''
775 return ''
776
776
777 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
777 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
778 ssets = [[hex(n) for n in ss] for ss in ssets]
778 ssets = [[hex(n) for n in ss] for ss in ssets]
779
779
780 data = []
780 data = []
781 for ss in ssets:
781 for ss in ssets:
782 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
782 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
783 lambda x: scmutil.formatchangeid(repo[x]))
783 lambda x: scmutil.formatchangeid(repo[x]))
784 data.append(h)
784 data.append(h)
785
785
786 # Format the successorssets
786 # Format the successorssets
787 def render(d):
787 def render(d):
788 t = []
788 t = []
789 for i in d.gen():
789 for i in d.gen():
790 t.append(i)
790 t.append(i)
791 return "".join(t)
791 return "".join(t)
792
792
793 def gen(data):
793 def gen(data):
794 yield "; ".join(render(d) for d in data)
794 yield "; ".join(render(d) for d in data)
795
795
796 return _hybrid(gen(data), data, lambda x: {'successorset': x},
796 return _hybrid(gen(data), data, lambda x: {'successorset': x},
797 pycompat.identity)
797 pycompat.identity)
798
798
799 @templatekeyword("succsandmarkers")
799 @templatekeyword("succsandmarkers")
800 def showsuccsandmarkers(repo, ctx, **args):
800 def showsuccsandmarkers(repo, ctx, **args):
801 """Returns a list of dict for each final successor of ctx. The dict
801 """Returns a list of dict for each final successor of ctx. The dict
802 contains successors node id in "successors" keys and the list of
802 contains successors node id in "successors" keys and the list of
803 obs-markers from ctx to the set of successors in "markers".
803 obs-markers from ctx to the set of successors in "markers".
804 (EXPERIMENTAL)
804 (EXPERIMENTAL)
805 """
805 """
806
806
807 values = obsutil.successorsandmarkers(repo, ctx)
807 values = obsutil.successorsandmarkers(repo, ctx)
808
808
809 if values is None:
809 if values is None:
810 values = []
810 values = []
811
811
812 # Format successors and markers to avoid exposing binary to templates
812 # Format successors and markers to avoid exposing binary to templates
813 data = []
813 data = []
814 for i in values:
814 for i in values:
815 # Format successors
815 # Format successors
816 successors = i['successors']
816 successors = i['successors']
817
817
818 successors = [hex(n) for n in successors]
818 successors = [hex(n) for n in successors]
819 successors = _hybrid(None, successors,
819 successors = _hybrid(None, successors,
820 lambda x: {'ctx': repo[x], 'revcache': {}},
820 lambda x: {'ctx': repo[x], 'revcache': {}},
821 lambda x: scmutil.formatchangeid(repo[x]))
821 lambda x: scmutil.formatchangeid(repo[x]))
822
822
823 # Format markers
823 # Format markers
824 finalmarkers = []
824 finalmarkers = []
825 for m in i['markers']:
825 for m in i['markers']:
826 hexprec = hex(m[0])
826 hexprec = hex(m[0])
827 hexsucs = tuple(hex(n) for n in m[1])
827 hexsucs = tuple(hex(n) for n in m[1])
828 hexparents = None
828 hexparents = None
829 if m[5] is not None:
829 if m[5] is not None:
830 hexparents = tuple(hex(n) for n in m[5])
830 hexparents = tuple(hex(n) for n in m[5])
831 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
831 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
832 finalmarkers.append(newmarker)
832 finalmarkers.append(newmarker)
833
833
834 data.append({'successors': successors, 'markers': finalmarkers})
834 data.append({'successors': successors, 'markers': finalmarkers})
835
835
836 args = pycompat.byteskwargs(args)
836 args = pycompat.byteskwargs(args)
837 f = _showlist('succsandmarkers', data, args['templ'], args)
837 f = _showlist('succsandmarkers', data, args['templ'], args)
838 return _hybrid(f, data, lambda x: x, pycompat.identity)
838 return _hybrid(f, data, lambda x: x, pycompat.identity)
839
839
840 @templatekeyword('p1rev', requires={'ctx'})
840 @templatekeyword('p1rev', requires={'ctx'})
841 def showp1rev(context, mapping):
841 def showp1rev(context, mapping):
842 """Integer. The repository-local revision number of the changeset's
842 """Integer. The repository-local revision number of the changeset's
843 first parent, or -1 if the changeset has no parents."""
843 first parent, or -1 if the changeset has no parents."""
844 ctx = context.resource(mapping, 'ctx')
844 ctx = context.resource(mapping, 'ctx')
845 return ctx.p1().rev()
845 return ctx.p1().rev()
846
846
847 @templatekeyword('p2rev', requires={'ctx'})
847 @templatekeyword('p2rev', requires={'ctx'})
848 def showp2rev(context, mapping):
848 def showp2rev(context, mapping):
849 """Integer. The repository-local revision number of the changeset's
849 """Integer. The repository-local revision number of the changeset's
850 second parent, or -1 if the changeset has no second parent."""
850 second parent, or -1 if the changeset has no second parent."""
851 ctx = context.resource(mapping, 'ctx')
851 ctx = context.resource(mapping, 'ctx')
852 return ctx.p2().rev()
852 return ctx.p2().rev()
853
853
854 @templatekeyword('p1node', requires={'ctx'})
854 @templatekeyword('p1node', requires={'ctx'})
855 def showp1node(context, mapping):
855 def showp1node(context, mapping):
856 """String. The identification hash of the changeset's first parent,
856 """String. The identification hash of the changeset's first parent,
857 as a 40 digit hexadecimal string. If the changeset has no parents, all
857 as a 40 digit hexadecimal string. If the changeset has no parents, all
858 digits are 0."""
858 digits are 0."""
859 ctx = context.resource(mapping, 'ctx')
859 ctx = context.resource(mapping, 'ctx')
860 return ctx.p1().hex()
860 return ctx.p1().hex()
861
861
862 @templatekeyword('p2node', requires={'ctx'})
862 @templatekeyword('p2node', requires={'ctx'})
863 def showp2node(context, mapping):
863 def showp2node(context, mapping):
864 """String. The identification hash of the changeset's second
864 """String. The identification hash of the changeset's second
865 parent, as a 40 digit hexadecimal string. If the changeset has no second
865 parent, as a 40 digit hexadecimal string. If the changeset has no second
866 parent, all digits are 0."""
866 parent, all digits are 0."""
867 ctx = context.resource(mapping, 'ctx')
867 ctx = context.resource(mapping, 'ctx')
868 return ctx.p2().hex()
868 return ctx.p2().hex()
869
869
870 @templatekeyword('parents')
870 @templatekeyword('parents')
871 def showparents(**args):
871 def showparents(**args):
872 """List of strings. The parents of the changeset in "rev:node"
872 """List of strings. The parents of the changeset in "rev:node"
873 format. If the changeset has only one "natural" parent (the predecessor
873 format. If the changeset has only one "natural" parent (the predecessor
874 revision) nothing is shown."""
874 revision) nothing is shown."""
875 args = pycompat.byteskwargs(args)
875 args = pycompat.byteskwargs(args)
876 repo = args['repo']
876 repo = args['repo']
877 ctx = args['ctx']
877 ctx = args['ctx']
878 pctxs = scmutil.meaningfulparents(repo, ctx)
878 pctxs = scmutil.meaningfulparents(repo, ctx)
879 prevs = [p.rev() for p in pctxs]
879 prevs = [p.rev() for p in pctxs]
880 parents = [[('rev', p.rev()),
880 parents = [[('rev', p.rev()),
881 ('node', p.hex()),
881 ('node', p.hex()),
882 ('phase', p.phasestr())]
882 ('phase', p.phasestr())]
883 for p in pctxs]
883 for p in pctxs]
884 f = _showlist('parent', parents, args['templ'], args)
884 f = _showlist('parent', parents, args['templ'], args)
885 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
885 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
886 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
886 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
887
887
888 @templatekeyword('phase', requires={'ctx'})
888 @templatekeyword('phase', requires={'ctx'})
889 def showphase(context, mapping):
889 def showphase(context, mapping):
890 """String. The changeset phase name."""
890 """String. The changeset phase name."""
891 ctx = context.resource(mapping, 'ctx')
891 ctx = context.resource(mapping, 'ctx')
892 return ctx.phasestr()
892 return ctx.phasestr()
893
893
894 @templatekeyword('phaseidx', requires={'ctx'})
894 @templatekeyword('phaseidx', requires={'ctx'})
895 def showphaseidx(context, mapping):
895 def showphaseidx(context, mapping):
896 """Integer. The changeset phase index. (ADVANCED)"""
896 """Integer. The changeset phase index. (ADVANCED)"""
897 ctx = context.resource(mapping, 'ctx')
897 ctx = context.resource(mapping, 'ctx')
898 return ctx.phase()
898 return ctx.phase()
899
899
900 @templatekeyword('rev', requires={'ctx'})
900 @templatekeyword('rev', requires={'ctx'})
901 def showrev(context, mapping):
901 def showrev(context, mapping):
902 """Integer. The repository-local changeset revision number."""
902 """Integer. The repository-local changeset revision number."""
903 ctx = context.resource(mapping, 'ctx')
903 ctx = context.resource(mapping, 'ctx')
904 return scmutil.intrev(ctx)
904 return scmutil.intrev(ctx)
905
905
906 def showrevslist(name, revs, **args):
906 def showrevslist(name, revs, **args):
907 """helper to generate a list of revisions in which a mapped template will
907 """helper to generate a list of revisions in which a mapped template will
908 be evaluated"""
908 be evaluated"""
909 args = pycompat.byteskwargs(args)
909 args = pycompat.byteskwargs(args)
910 repo = args['ctx'].repo()
910 repo = args['ctx'].repo()
911 f = _showlist(name, ['%d' % r for r in revs], args['templ'], args)
911 f = _showlist(name, ['%d' % r for r in revs], args['templ'], args)
912 return _hybrid(f, revs,
912 return _hybrid(f, revs,
913 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
913 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
914 pycompat.identity, keytype=int)
914 pycompat.identity, keytype=int)
915
915
916 @templatekeyword('subrepos')
916 @templatekeyword('subrepos', requires={'ctx', 'templ'})
917 def showsubrepos(**args):
917 def showsubrepos(context, mapping):
918 """List of strings. Updated subrepositories in the changeset."""
918 """List of strings. Updated subrepositories in the changeset."""
919 args = pycompat.byteskwargs(args)
919 ctx = context.resource(mapping, 'ctx')
920 ctx = args['ctx']
921 substate = ctx.substate
920 substate = ctx.substate
922 if not substate:
921 if not substate:
923 return showlist('subrepo', [], args)
922 return compatlist(context, mapping, 'subrepo', [])
924 psubstate = ctx.parents()[0].substate or {}
923 psubstate = ctx.parents()[0].substate or {}
925 subrepos = []
924 subrepos = []
926 for sub in substate:
925 for sub in substate:
927 if sub not in psubstate or substate[sub] != psubstate[sub]:
926 if sub not in psubstate or substate[sub] != psubstate[sub]:
928 subrepos.append(sub) # modified or newly added in ctx
927 subrepos.append(sub) # modified or newly added in ctx
929 for sub in psubstate:
928 for sub in psubstate:
930 if sub not in substate:
929 if sub not in substate:
931 subrepos.append(sub) # removed in ctx
930 subrepos.append(sub) # removed in ctx
932 return showlist('subrepo', sorted(subrepos), args)
931 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
933
932
934 # don't remove "showtags" definition, even though namespaces will put
933 # don't remove "showtags" definition, even though namespaces will put
935 # a helper function for "tags" keyword into "keywords" map automatically,
934 # a helper function for "tags" keyword into "keywords" map automatically,
936 # because online help text is built without namespaces initialization
935 # because online help text is built without namespaces initialization
937 @templatekeyword('tags')
936 @templatekeyword('tags')
938 def showtags(**args):
937 def showtags(**args):
939 """List of strings. Any tags associated with the changeset."""
938 """List of strings. Any tags associated with the changeset."""
940 return shownames('tags', **args)
939 return shownames('tags', **args)
941
940
942 @templatekeyword('termwidth', requires={'ui'})
941 @templatekeyword('termwidth', requires={'ui'})
943 def showtermwidth(context, mapping):
942 def showtermwidth(context, mapping):
944 """Integer. The width of the current terminal."""
943 """Integer. The width of the current terminal."""
945 ui = context.resource(mapping, 'ui')
944 ui = context.resource(mapping, 'ui')
946 return ui.termwidth()
945 return ui.termwidth()
947
946
948 @templatekeyword('instabilities')
947 @templatekeyword('instabilities', requires={'ctx', 'templ'})
949 def showinstabilities(**args):
948 def showinstabilities(context, mapping):
950 """List of strings. Evolution instabilities affecting the changeset.
949 """List of strings. Evolution instabilities affecting the changeset.
951 (EXPERIMENTAL)
950 (EXPERIMENTAL)
952 """
951 """
953 args = pycompat.byteskwargs(args)
952 ctx = context.resource(mapping, 'ctx')
954 return showlist('instability', args['ctx'].instabilities(), args,
953 return compatlist(context, mapping, 'instability', ctx.instabilities(),
955 plural='instabilities')
954 plural='instabilities')
956
955
957 @templatekeyword('verbosity', requires={'ui'})
956 @templatekeyword('verbosity', requires={'ui'})
958 def showverbosity(context, mapping):
957 def showverbosity(context, mapping):
959 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
958 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
960 or ''."""
959 or ''."""
961 ui = context.resource(mapping, 'ui')
960 ui = context.resource(mapping, 'ui')
962 # see logcmdutil.changesettemplater for priority of these flags
961 # see logcmdutil.changesettemplater for priority of these flags
963 if ui.debugflag:
962 if ui.debugflag:
964 return 'debug'
963 return 'debug'
965 elif ui.quiet:
964 elif ui.quiet:
966 return 'quiet'
965 return 'quiet'
967 elif ui.verbose:
966 elif ui.verbose:
968 return 'verbose'
967 return 'verbose'
969 return ''
968 return ''
970
969
971 def loadkeyword(ui, extname, registrarobj):
970 def loadkeyword(ui, extname, registrarobj):
972 """Load template keyword from specified registrarobj
971 """Load template keyword from specified registrarobj
973 """
972 """
974 for name, func in registrarobj._table.iteritems():
973 for name, func in registrarobj._table.iteritems():
975 keywords[name] = func
974 keywords[name] = func
976
975
977 # tell hggettext to extract docstrings from these functions:
976 # tell hggettext to extract docstrings from these functions:
978 i18nfunctions = keywords.values()
977 i18nfunctions = keywords.values()
@@ -1,1636 +1,1633 b''
1 # templater.py - template expansion for output
1 # templater.py - template expansion for output
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 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 __future__ import absolute_import, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import os
10 import os
11 import re
11 import re
12 import types
12 import types
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import (
15 from . import (
16 color,
16 color,
17 config,
17 config,
18 encoding,
18 encoding,
19 error,
19 error,
20 minirst,
20 minirst,
21 obsutil,
21 obsutil,
22 parser,
22 parser,
23 pycompat,
23 pycompat,
24 registrar,
24 registrar,
25 revset as revsetmod,
25 revset as revsetmod,
26 revsetlang,
26 revsetlang,
27 scmutil,
27 scmutil,
28 templatefilters,
28 templatefilters,
29 templatekw,
29 templatekw,
30 util,
30 util,
31 )
31 )
32
32
33 class ResourceUnavailable(error.Abort):
33 class ResourceUnavailable(error.Abort):
34 pass
34 pass
35
35
36 class TemplateNotFound(error.Abort):
36 class TemplateNotFound(error.Abort):
37 pass
37 pass
38
38
39 # template parsing
39 # template parsing
40
40
41 elements = {
41 elements = {
42 # token-type: binding-strength, primary, prefix, infix, suffix
42 # token-type: binding-strength, primary, prefix, infix, suffix
43 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
43 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
44 ".": (18, None, None, (".", 18), None),
44 ".": (18, None, None, (".", 18), None),
45 "%": (15, None, None, ("%", 15), None),
45 "%": (15, None, None, ("%", 15), None),
46 "|": (15, None, None, ("|", 15), None),
46 "|": (15, None, None, ("|", 15), None),
47 "*": (5, None, None, ("*", 5), None),
47 "*": (5, None, None, ("*", 5), None),
48 "/": (5, None, None, ("/", 5), None),
48 "/": (5, None, None, ("/", 5), None),
49 "+": (4, None, None, ("+", 4), None),
49 "+": (4, None, None, ("+", 4), None),
50 "-": (4, None, ("negate", 19), ("-", 4), None),
50 "-": (4, None, ("negate", 19), ("-", 4), None),
51 "=": (3, None, None, ("keyvalue", 3), None),
51 "=": (3, None, None, ("keyvalue", 3), None),
52 ",": (2, None, None, ("list", 2), None),
52 ",": (2, None, None, ("list", 2), None),
53 ")": (0, None, None, None, None),
53 ")": (0, None, None, None, None),
54 "integer": (0, "integer", None, None, None),
54 "integer": (0, "integer", None, None, None),
55 "symbol": (0, "symbol", None, None, None),
55 "symbol": (0, "symbol", None, None, None),
56 "string": (0, "string", None, None, None),
56 "string": (0, "string", None, None, None),
57 "template": (0, "template", None, None, None),
57 "template": (0, "template", None, None, None),
58 "end": (0, None, None, None, None),
58 "end": (0, None, None, None, None),
59 }
59 }
60
60
61 def tokenize(program, start, end, term=None):
61 def tokenize(program, start, end, term=None):
62 """Parse a template expression into a stream of tokens, which must end
62 """Parse a template expression into a stream of tokens, which must end
63 with term if specified"""
63 with term if specified"""
64 pos = start
64 pos = start
65 program = pycompat.bytestr(program)
65 program = pycompat.bytestr(program)
66 while pos < end:
66 while pos < end:
67 c = program[pos]
67 c = program[pos]
68 if c.isspace(): # skip inter-token whitespace
68 if c.isspace(): # skip inter-token whitespace
69 pass
69 pass
70 elif c in "(=,).%|+-*/": # handle simple operators
70 elif c in "(=,).%|+-*/": # handle simple operators
71 yield (c, None, pos)
71 yield (c, None, pos)
72 elif c in '"\'': # handle quoted templates
72 elif c in '"\'': # handle quoted templates
73 s = pos + 1
73 s = pos + 1
74 data, pos = _parsetemplate(program, s, end, c)
74 data, pos = _parsetemplate(program, s, end, c)
75 yield ('template', data, s)
75 yield ('template', data, s)
76 pos -= 1
76 pos -= 1
77 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
77 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
78 # handle quoted strings
78 # handle quoted strings
79 c = program[pos + 1]
79 c = program[pos + 1]
80 s = pos = pos + 2
80 s = pos = pos + 2
81 while pos < end: # find closing quote
81 while pos < end: # find closing quote
82 d = program[pos]
82 d = program[pos]
83 if d == '\\': # skip over escaped characters
83 if d == '\\': # skip over escaped characters
84 pos += 2
84 pos += 2
85 continue
85 continue
86 if d == c:
86 if d == c:
87 yield ('string', program[s:pos], s)
87 yield ('string', program[s:pos], s)
88 break
88 break
89 pos += 1
89 pos += 1
90 else:
90 else:
91 raise error.ParseError(_("unterminated string"), s)
91 raise error.ParseError(_("unterminated string"), s)
92 elif c.isdigit():
92 elif c.isdigit():
93 s = pos
93 s = pos
94 while pos < end:
94 while pos < end:
95 d = program[pos]
95 d = program[pos]
96 if not d.isdigit():
96 if not d.isdigit():
97 break
97 break
98 pos += 1
98 pos += 1
99 yield ('integer', program[s:pos], s)
99 yield ('integer', program[s:pos], s)
100 pos -= 1
100 pos -= 1
101 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
101 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
102 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
102 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
103 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
103 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
104 # where some of nested templates were preprocessed as strings and
104 # where some of nested templates were preprocessed as strings and
105 # then compiled. therefore, \"...\" was allowed. (issue4733)
105 # then compiled. therefore, \"...\" was allowed. (issue4733)
106 #
106 #
107 # processing flow of _evalifliteral() at 5ab28a2e9962:
107 # processing flow of _evalifliteral() at 5ab28a2e9962:
108 # outer template string -> stringify() -> compiletemplate()
108 # outer template string -> stringify() -> compiletemplate()
109 # ------------------------ ------------ ------------------
109 # ------------------------ ------------ ------------------
110 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
110 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
111 # ~~~~~~~~
111 # ~~~~~~~~
112 # escaped quoted string
112 # escaped quoted string
113 if c == 'r':
113 if c == 'r':
114 pos += 1
114 pos += 1
115 token = 'string'
115 token = 'string'
116 else:
116 else:
117 token = 'template'
117 token = 'template'
118 quote = program[pos:pos + 2]
118 quote = program[pos:pos + 2]
119 s = pos = pos + 2
119 s = pos = pos + 2
120 while pos < end: # find closing escaped quote
120 while pos < end: # find closing escaped quote
121 if program.startswith('\\\\\\', pos, end):
121 if program.startswith('\\\\\\', pos, end):
122 pos += 4 # skip over double escaped characters
122 pos += 4 # skip over double escaped characters
123 continue
123 continue
124 if program.startswith(quote, pos, end):
124 if program.startswith(quote, pos, end):
125 # interpret as if it were a part of an outer string
125 # interpret as if it were a part of an outer string
126 data = parser.unescapestr(program[s:pos])
126 data = parser.unescapestr(program[s:pos])
127 if token == 'template':
127 if token == 'template':
128 data = _parsetemplate(data, 0, len(data))[0]
128 data = _parsetemplate(data, 0, len(data))[0]
129 yield (token, data, s)
129 yield (token, data, s)
130 pos += 1
130 pos += 1
131 break
131 break
132 pos += 1
132 pos += 1
133 else:
133 else:
134 raise error.ParseError(_("unterminated string"), s)
134 raise error.ParseError(_("unterminated string"), s)
135 elif c.isalnum() or c in '_':
135 elif c.isalnum() or c in '_':
136 s = pos
136 s = pos
137 pos += 1
137 pos += 1
138 while pos < end: # find end of symbol
138 while pos < end: # find end of symbol
139 d = program[pos]
139 d = program[pos]
140 if not (d.isalnum() or d == "_"):
140 if not (d.isalnum() or d == "_"):
141 break
141 break
142 pos += 1
142 pos += 1
143 sym = program[s:pos]
143 sym = program[s:pos]
144 yield ('symbol', sym, s)
144 yield ('symbol', sym, s)
145 pos -= 1
145 pos -= 1
146 elif c == term:
146 elif c == term:
147 yield ('end', None, pos + 1)
147 yield ('end', None, pos + 1)
148 return
148 return
149 else:
149 else:
150 raise error.ParseError(_("syntax error"), pos)
150 raise error.ParseError(_("syntax error"), pos)
151 pos += 1
151 pos += 1
152 if term:
152 if term:
153 raise error.ParseError(_("unterminated template expansion"), start)
153 raise error.ParseError(_("unterminated template expansion"), start)
154 yield ('end', None, pos)
154 yield ('end', None, pos)
155
155
156 def _parsetemplate(tmpl, start, stop, quote=''):
156 def _parsetemplate(tmpl, start, stop, quote=''):
157 r"""
157 r"""
158 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
158 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
159 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
159 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
160 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
160 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
161 ([('string', 'foo'), ('symbol', 'bar')], 9)
161 ([('string', 'foo'), ('symbol', 'bar')], 9)
162 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
162 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
163 ([('string', 'foo')], 4)
163 ([('string', 'foo')], 4)
164 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
164 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
165 ([('string', 'foo"'), ('string', 'bar')], 9)
165 ([('string', 'foo"'), ('string', 'bar')], 9)
166 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
166 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
167 ([('string', 'foo\\')], 6)
167 ([('string', 'foo\\')], 6)
168 """
168 """
169 parsed = []
169 parsed = []
170 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
170 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
171 if typ == 'string':
171 if typ == 'string':
172 parsed.append((typ, val))
172 parsed.append((typ, val))
173 elif typ == 'template':
173 elif typ == 'template':
174 parsed.append(val)
174 parsed.append(val)
175 elif typ == 'end':
175 elif typ == 'end':
176 return parsed, pos
176 return parsed, pos
177 else:
177 else:
178 raise error.ProgrammingError('unexpected type: %s' % typ)
178 raise error.ProgrammingError('unexpected type: %s' % typ)
179 raise error.ProgrammingError('unterminated scanning of template')
179 raise error.ProgrammingError('unterminated scanning of template')
180
180
181 def scantemplate(tmpl, raw=False):
181 def scantemplate(tmpl, raw=False):
182 r"""Scan (type, start, end) positions of outermost elements in template
182 r"""Scan (type, start, end) positions of outermost elements in template
183
183
184 If raw=True, a backslash is not taken as an escape character just like
184 If raw=True, a backslash is not taken as an escape character just like
185 r'' string in Python. Note that this is different from r'' literal in
185 r'' string in Python. Note that this is different from r'' literal in
186 template in that no template fragment can appear in r'', e.g. r'{foo}'
186 template in that no template fragment can appear in r'', e.g. r'{foo}'
187 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
187 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
188 'foo'.
188 'foo'.
189
189
190 >>> list(scantemplate(b'foo{bar}"baz'))
190 >>> list(scantemplate(b'foo{bar}"baz'))
191 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
191 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
192 >>> list(scantemplate(b'outer{"inner"}outer'))
192 >>> list(scantemplate(b'outer{"inner"}outer'))
193 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
193 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
194 >>> list(scantemplate(b'foo\\{escaped}'))
194 >>> list(scantemplate(b'foo\\{escaped}'))
195 [('string', 0, 5), ('string', 5, 13)]
195 [('string', 0, 5), ('string', 5, 13)]
196 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
196 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
197 [('string', 0, 4), ('template', 4, 13)]
197 [('string', 0, 4), ('template', 4, 13)]
198 """
198 """
199 last = None
199 last = None
200 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
200 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
201 if last:
201 if last:
202 yield last + (pos,)
202 yield last + (pos,)
203 if typ == 'end':
203 if typ == 'end':
204 return
204 return
205 else:
205 else:
206 last = (typ, pos)
206 last = (typ, pos)
207 raise error.ProgrammingError('unterminated scanning of template')
207 raise error.ProgrammingError('unterminated scanning of template')
208
208
209 def _scantemplate(tmpl, start, stop, quote='', raw=False):
209 def _scantemplate(tmpl, start, stop, quote='', raw=False):
210 """Parse template string into chunks of strings and template expressions"""
210 """Parse template string into chunks of strings and template expressions"""
211 sepchars = '{' + quote
211 sepchars = '{' + quote
212 unescape = [parser.unescapestr, pycompat.identity][raw]
212 unescape = [parser.unescapestr, pycompat.identity][raw]
213 pos = start
213 pos = start
214 p = parser.parser(elements)
214 p = parser.parser(elements)
215 while pos < stop:
215 while pos < stop:
216 n = min((tmpl.find(c, pos, stop) for c in sepchars),
216 n = min((tmpl.find(c, pos, stop) for c in sepchars),
217 key=lambda n: (n < 0, n))
217 key=lambda n: (n < 0, n))
218 if n < 0:
218 if n < 0:
219 yield ('string', unescape(tmpl[pos:stop]), pos)
219 yield ('string', unescape(tmpl[pos:stop]), pos)
220 pos = stop
220 pos = stop
221 break
221 break
222 c = tmpl[n:n + 1]
222 c = tmpl[n:n + 1]
223 bs = 0 # count leading backslashes
223 bs = 0 # count leading backslashes
224 if not raw:
224 if not raw:
225 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
225 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
226 if bs % 2 == 1:
226 if bs % 2 == 1:
227 # escaped (e.g. '\{', '\\\{', but not '\\{')
227 # escaped (e.g. '\{', '\\\{', but not '\\{')
228 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
228 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
229 pos = n + 1
229 pos = n + 1
230 continue
230 continue
231 if n > pos:
231 if n > pos:
232 yield ('string', unescape(tmpl[pos:n]), pos)
232 yield ('string', unescape(tmpl[pos:n]), pos)
233 if c == quote:
233 if c == quote:
234 yield ('end', None, n + 1)
234 yield ('end', None, n + 1)
235 return
235 return
236
236
237 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
237 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
238 if not tmpl.endswith('}', n + 1, pos):
238 if not tmpl.endswith('}', n + 1, pos):
239 raise error.ParseError(_("invalid token"), pos)
239 raise error.ParseError(_("invalid token"), pos)
240 yield ('template', parseres, n)
240 yield ('template', parseres, n)
241
241
242 if quote:
242 if quote:
243 raise error.ParseError(_("unterminated string"), start)
243 raise error.ParseError(_("unterminated string"), start)
244 yield ('end', None, pos)
244 yield ('end', None, pos)
245
245
246 def _unnesttemplatelist(tree):
246 def _unnesttemplatelist(tree):
247 """Expand list of templates to node tuple
247 """Expand list of templates to node tuple
248
248
249 >>> def f(tree):
249 >>> def f(tree):
250 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
250 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
251 >>> f((b'template', []))
251 >>> f((b'template', []))
252 (string '')
252 (string '')
253 >>> f((b'template', [(b'string', b'foo')]))
253 >>> f((b'template', [(b'string', b'foo')]))
254 (string 'foo')
254 (string 'foo')
255 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
255 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
256 (template
256 (template
257 (string 'foo')
257 (string 'foo')
258 (symbol 'rev'))
258 (symbol 'rev'))
259 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
259 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
260 (template
260 (template
261 (symbol 'rev'))
261 (symbol 'rev'))
262 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
262 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
263 (string 'foo')
263 (string 'foo')
264 """
264 """
265 if not isinstance(tree, tuple):
265 if not isinstance(tree, tuple):
266 return tree
266 return tree
267 op = tree[0]
267 op = tree[0]
268 if op != 'template':
268 if op != 'template':
269 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
269 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
270
270
271 assert len(tree) == 2
271 assert len(tree) == 2
272 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
272 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
273 if not xs:
273 if not xs:
274 return ('string', '') # empty template ""
274 return ('string', '') # empty template ""
275 elif len(xs) == 1 and xs[0][0] == 'string':
275 elif len(xs) == 1 and xs[0][0] == 'string':
276 return xs[0] # fast path for string with no template fragment "x"
276 return xs[0] # fast path for string with no template fragment "x"
277 else:
277 else:
278 return (op,) + xs
278 return (op,) + xs
279
279
280 def parse(tmpl):
280 def parse(tmpl):
281 """Parse template string into tree"""
281 """Parse template string into tree"""
282 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
282 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
283 assert pos == len(tmpl), 'unquoted template should be consumed'
283 assert pos == len(tmpl), 'unquoted template should be consumed'
284 return _unnesttemplatelist(('template', parsed))
284 return _unnesttemplatelist(('template', parsed))
285
285
286 def _parseexpr(expr):
286 def _parseexpr(expr):
287 """Parse a template expression into tree
287 """Parse a template expression into tree
288
288
289 >>> _parseexpr(b'"foo"')
289 >>> _parseexpr(b'"foo"')
290 ('string', 'foo')
290 ('string', 'foo')
291 >>> _parseexpr(b'foo(bar)')
291 >>> _parseexpr(b'foo(bar)')
292 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
292 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
293 >>> _parseexpr(b'foo(')
293 >>> _parseexpr(b'foo(')
294 Traceback (most recent call last):
294 Traceback (most recent call last):
295 ...
295 ...
296 ParseError: ('not a prefix: end', 4)
296 ParseError: ('not a prefix: end', 4)
297 >>> _parseexpr(b'"foo" "bar"')
297 >>> _parseexpr(b'"foo" "bar"')
298 Traceback (most recent call last):
298 Traceback (most recent call last):
299 ...
299 ...
300 ParseError: ('invalid token', 7)
300 ParseError: ('invalid token', 7)
301 """
301 """
302 p = parser.parser(elements)
302 p = parser.parser(elements)
303 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
303 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
304 if pos != len(expr):
304 if pos != len(expr):
305 raise error.ParseError(_('invalid token'), pos)
305 raise error.ParseError(_('invalid token'), pos)
306 return _unnesttemplatelist(tree)
306 return _unnesttemplatelist(tree)
307
307
308 def prettyformat(tree):
308 def prettyformat(tree):
309 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
309 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
310
310
311 def compileexp(exp, context, curmethods):
311 def compileexp(exp, context, curmethods):
312 """Compile parsed template tree to (func, data) pair"""
312 """Compile parsed template tree to (func, data) pair"""
313 if not exp:
313 if not exp:
314 raise error.ParseError(_("missing argument"))
314 raise error.ParseError(_("missing argument"))
315 t = exp[0]
315 t = exp[0]
316 if t in curmethods:
316 if t in curmethods:
317 return curmethods[t](exp, context)
317 return curmethods[t](exp, context)
318 raise error.ParseError(_("unknown method '%s'") % t)
318 raise error.ParseError(_("unknown method '%s'") % t)
319
319
320 # template evaluation
320 # template evaluation
321
321
322 def getsymbol(exp):
322 def getsymbol(exp):
323 if exp[0] == 'symbol':
323 if exp[0] == 'symbol':
324 return exp[1]
324 return exp[1]
325 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
325 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
326
326
327 def getlist(x):
327 def getlist(x):
328 if not x:
328 if not x:
329 return []
329 return []
330 if x[0] == 'list':
330 if x[0] == 'list':
331 return getlist(x[1]) + [x[2]]
331 return getlist(x[1]) + [x[2]]
332 return [x]
332 return [x]
333
333
334 def gettemplate(exp, context):
334 def gettemplate(exp, context):
335 """Compile given template tree or load named template from map file;
335 """Compile given template tree or load named template from map file;
336 returns (func, data) pair"""
336 returns (func, data) pair"""
337 if exp[0] in ('template', 'string'):
337 if exp[0] in ('template', 'string'):
338 return compileexp(exp, context, methods)
338 return compileexp(exp, context, methods)
339 if exp[0] == 'symbol':
339 if exp[0] == 'symbol':
340 # unlike runsymbol(), here 'symbol' is always taken as template name
340 # unlike runsymbol(), here 'symbol' is always taken as template name
341 # even if it exists in mapping. this allows us to override mapping
341 # even if it exists in mapping. this allows us to override mapping
342 # by web templates, e.g. 'changelogtag' is redefined in map file.
342 # by web templates, e.g. 'changelogtag' is redefined in map file.
343 return context._load(exp[1])
343 return context._load(exp[1])
344 raise error.ParseError(_("expected template specifier"))
344 raise error.ParseError(_("expected template specifier"))
345
345
346 def findsymbolicname(arg):
346 def findsymbolicname(arg):
347 """Find symbolic name for the given compiled expression; returns None
347 """Find symbolic name for the given compiled expression; returns None
348 if nothing found reliably"""
348 if nothing found reliably"""
349 while True:
349 while True:
350 func, data = arg
350 func, data = arg
351 if func is runsymbol:
351 if func is runsymbol:
352 return data
352 return data
353 elif func is runfilter:
353 elif func is runfilter:
354 arg = data[0]
354 arg = data[0]
355 else:
355 else:
356 return None
356 return None
357
357
358 def evalrawexp(context, mapping, arg):
358 def evalrawexp(context, mapping, arg):
359 """Evaluate given argument as a bare template object which may require
359 """Evaluate given argument as a bare template object which may require
360 further processing (such as folding generator of strings)"""
360 further processing (such as folding generator of strings)"""
361 func, data = arg
361 func, data = arg
362 return func(context, mapping, data)
362 return func(context, mapping, data)
363
363
364 def evalfuncarg(context, mapping, arg):
364 def evalfuncarg(context, mapping, arg):
365 """Evaluate given argument as value type"""
365 """Evaluate given argument as value type"""
366 thing = evalrawexp(context, mapping, arg)
366 thing = evalrawexp(context, mapping, arg)
367 thing = templatekw.unwrapvalue(thing)
367 thing = templatekw.unwrapvalue(thing)
368 # evalrawexp() may return string, generator of strings or arbitrary object
368 # evalrawexp() may return string, generator of strings or arbitrary object
369 # such as date tuple, but filter does not want generator.
369 # such as date tuple, but filter does not want generator.
370 if isinstance(thing, types.GeneratorType):
370 if isinstance(thing, types.GeneratorType):
371 thing = stringify(thing)
371 thing = stringify(thing)
372 return thing
372 return thing
373
373
374 def evalboolean(context, mapping, arg):
374 def evalboolean(context, mapping, arg):
375 """Evaluate given argument as boolean, but also takes boolean literals"""
375 """Evaluate given argument as boolean, but also takes boolean literals"""
376 func, data = arg
376 func, data = arg
377 if func is runsymbol:
377 if func is runsymbol:
378 thing = func(context, mapping, data, default=None)
378 thing = func(context, mapping, data, default=None)
379 if thing is None:
379 if thing is None:
380 # not a template keyword, takes as a boolean literal
380 # not a template keyword, takes as a boolean literal
381 thing = util.parsebool(data)
381 thing = util.parsebool(data)
382 else:
382 else:
383 thing = func(context, mapping, data)
383 thing = func(context, mapping, data)
384 thing = templatekw.unwrapvalue(thing)
384 thing = templatekw.unwrapvalue(thing)
385 if isinstance(thing, bool):
385 if isinstance(thing, bool):
386 return thing
386 return thing
387 # other objects are evaluated as strings, which means 0 is True, but
387 # other objects are evaluated as strings, which means 0 is True, but
388 # empty dict/list should be False as they are expected to be ''
388 # empty dict/list should be False as they are expected to be ''
389 return bool(stringify(thing))
389 return bool(stringify(thing))
390
390
391 def evalinteger(context, mapping, arg, err=None):
391 def evalinteger(context, mapping, arg, err=None):
392 v = evalfuncarg(context, mapping, arg)
392 v = evalfuncarg(context, mapping, arg)
393 try:
393 try:
394 return int(v)
394 return int(v)
395 except (TypeError, ValueError):
395 except (TypeError, ValueError):
396 raise error.ParseError(err or _('not an integer'))
396 raise error.ParseError(err or _('not an integer'))
397
397
398 def evalstring(context, mapping, arg):
398 def evalstring(context, mapping, arg):
399 return stringify(evalrawexp(context, mapping, arg))
399 return stringify(evalrawexp(context, mapping, arg))
400
400
401 def evalstringliteral(context, mapping, arg):
401 def evalstringliteral(context, mapping, arg):
402 """Evaluate given argument as string template, but returns symbol name
402 """Evaluate given argument as string template, but returns symbol name
403 if it is unknown"""
403 if it is unknown"""
404 func, data = arg
404 func, data = arg
405 if func is runsymbol:
405 if func is runsymbol:
406 thing = func(context, mapping, data, default=data)
406 thing = func(context, mapping, data, default=data)
407 else:
407 else:
408 thing = func(context, mapping, data)
408 thing = func(context, mapping, data)
409 return stringify(thing)
409 return stringify(thing)
410
410
411 _evalfuncbytype = {
411 _evalfuncbytype = {
412 bool: evalboolean,
412 bool: evalboolean,
413 bytes: evalstring,
413 bytes: evalstring,
414 int: evalinteger,
414 int: evalinteger,
415 }
415 }
416
416
417 def evalastype(context, mapping, arg, typ):
417 def evalastype(context, mapping, arg, typ):
418 """Evaluate given argument and coerce its type"""
418 """Evaluate given argument and coerce its type"""
419 try:
419 try:
420 f = _evalfuncbytype[typ]
420 f = _evalfuncbytype[typ]
421 except KeyError:
421 except KeyError:
422 raise error.ProgrammingError('invalid type specified: %r' % typ)
422 raise error.ProgrammingError('invalid type specified: %r' % typ)
423 return f(context, mapping, arg)
423 return f(context, mapping, arg)
424
424
425 def runinteger(context, mapping, data):
425 def runinteger(context, mapping, data):
426 return int(data)
426 return int(data)
427
427
428 def runstring(context, mapping, data):
428 def runstring(context, mapping, data):
429 return data
429 return data
430
430
431 def _recursivesymbolblocker(key):
431 def _recursivesymbolblocker(key):
432 def showrecursion(**args):
432 def showrecursion(**args):
433 raise error.Abort(_("recursive reference '%s' in template") % key)
433 raise error.Abort(_("recursive reference '%s' in template") % key)
434 return showrecursion
434 return showrecursion
435
435
436 def _runrecursivesymbol(context, mapping, key):
436 def _runrecursivesymbol(context, mapping, key):
437 raise error.Abort(_("recursive reference '%s' in template") % key)
437 raise error.Abort(_("recursive reference '%s' in template") % key)
438
438
439 def runsymbol(context, mapping, key, default=''):
439 def runsymbol(context, mapping, key, default=''):
440 v = context.symbol(mapping, key)
440 v = context.symbol(mapping, key)
441 if v is None:
441 if v is None:
442 # put poison to cut recursion. we can't move this to parsing phase
442 # put poison to cut recursion. we can't move this to parsing phase
443 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
443 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
444 safemapping = mapping.copy()
444 safemapping = mapping.copy()
445 safemapping[key] = _recursivesymbolblocker(key)
445 safemapping[key] = _recursivesymbolblocker(key)
446 try:
446 try:
447 v = context.process(key, safemapping)
447 v = context.process(key, safemapping)
448 except TemplateNotFound:
448 except TemplateNotFound:
449 v = default
449 v = default
450 if callable(v) and getattr(v, '_requires', None) is None:
450 if callable(v) and getattr(v, '_requires', None) is None:
451 # old templatekw: expand all keywords and resources
451 # old templatekw: expand all keywords and resources
452 props = context._resources.copy()
452 props = context._resources.copy()
453 props.update(mapping)
453 props.update(mapping)
454 return v(**pycompat.strkwargs(props))
454 return v(**pycompat.strkwargs(props))
455 if callable(v):
455 if callable(v):
456 # new templatekw
456 # new templatekw
457 try:
457 try:
458 return v(context, mapping)
458 return v(context, mapping)
459 except ResourceUnavailable:
459 except ResourceUnavailable:
460 # unsupported keyword is mapped to empty just like unknown keyword
460 # unsupported keyword is mapped to empty just like unknown keyword
461 return None
461 return None
462 return v
462 return v
463
463
464 def buildtemplate(exp, context):
464 def buildtemplate(exp, context):
465 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
465 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
466 return (runtemplate, ctmpl)
466 return (runtemplate, ctmpl)
467
467
468 def runtemplate(context, mapping, template):
468 def runtemplate(context, mapping, template):
469 for arg in template:
469 for arg in template:
470 yield evalrawexp(context, mapping, arg)
470 yield evalrawexp(context, mapping, arg)
471
471
472 def buildfilter(exp, context):
472 def buildfilter(exp, context):
473 n = getsymbol(exp[2])
473 n = getsymbol(exp[2])
474 if n in context._filters:
474 if n in context._filters:
475 filt = context._filters[n]
475 filt = context._filters[n]
476 arg = compileexp(exp[1], context, methods)
476 arg = compileexp(exp[1], context, methods)
477 return (runfilter, (arg, filt))
477 return (runfilter, (arg, filt))
478 if n in funcs:
478 if n in funcs:
479 f = funcs[n]
479 f = funcs[n]
480 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
480 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
481 return (f, args)
481 return (f, args)
482 raise error.ParseError(_("unknown function '%s'") % n)
482 raise error.ParseError(_("unknown function '%s'") % n)
483
483
484 def runfilter(context, mapping, data):
484 def runfilter(context, mapping, data):
485 arg, filt = data
485 arg, filt = data
486 thing = evalfuncarg(context, mapping, arg)
486 thing = evalfuncarg(context, mapping, arg)
487 try:
487 try:
488 return filt(thing)
488 return filt(thing)
489 except (ValueError, AttributeError, TypeError):
489 except (ValueError, AttributeError, TypeError):
490 sym = findsymbolicname(arg)
490 sym = findsymbolicname(arg)
491 if sym:
491 if sym:
492 msg = (_("template filter '%s' is not compatible with keyword '%s'")
492 msg = (_("template filter '%s' is not compatible with keyword '%s'")
493 % (pycompat.sysbytes(filt.__name__), sym))
493 % (pycompat.sysbytes(filt.__name__), sym))
494 else:
494 else:
495 msg = (_("incompatible use of template filter '%s'")
495 msg = (_("incompatible use of template filter '%s'")
496 % pycompat.sysbytes(filt.__name__))
496 % pycompat.sysbytes(filt.__name__))
497 raise error.Abort(msg)
497 raise error.Abort(msg)
498
498
499 def buildmap(exp, context):
499 def buildmap(exp, context):
500 darg = compileexp(exp[1], context, methods)
500 darg = compileexp(exp[1], context, methods)
501 targ = gettemplate(exp[2], context)
501 targ = gettemplate(exp[2], context)
502 return (runmap, (darg, targ))
502 return (runmap, (darg, targ))
503
503
504 def runmap(context, mapping, data):
504 def runmap(context, mapping, data):
505 darg, targ = data
505 darg, targ = data
506 d = evalrawexp(context, mapping, darg)
506 d = evalrawexp(context, mapping, darg)
507 if util.safehasattr(d, 'itermaps'):
507 if util.safehasattr(d, 'itermaps'):
508 diter = d.itermaps()
508 diter = d.itermaps()
509 else:
509 else:
510 try:
510 try:
511 diter = iter(d)
511 diter = iter(d)
512 except TypeError:
512 except TypeError:
513 sym = findsymbolicname(darg)
513 sym = findsymbolicname(darg)
514 if sym:
514 if sym:
515 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
515 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
516 else:
516 else:
517 raise error.ParseError(_("%r is not iterable") % d)
517 raise error.ParseError(_("%r is not iterable") % d)
518
518
519 for i, v in enumerate(diter):
519 for i, v in enumerate(diter):
520 lm = mapping.copy()
520 lm = mapping.copy()
521 lm['index'] = i
521 lm['index'] = i
522 if isinstance(v, dict):
522 if isinstance(v, dict):
523 lm.update(v)
523 lm.update(v)
524 lm['originalnode'] = mapping.get('node')
524 lm['originalnode'] = mapping.get('node')
525 yield evalrawexp(context, lm, targ)
525 yield evalrawexp(context, lm, targ)
526 else:
526 else:
527 # v is not an iterable of dicts, this happen when 'key'
527 # v is not an iterable of dicts, this happen when 'key'
528 # has been fully expanded already and format is useless.
528 # has been fully expanded already and format is useless.
529 # If so, return the expanded value.
529 # If so, return the expanded value.
530 yield v
530 yield v
531
531
532 def buildmember(exp, context):
532 def buildmember(exp, context):
533 darg = compileexp(exp[1], context, methods)
533 darg = compileexp(exp[1], context, methods)
534 memb = getsymbol(exp[2])
534 memb = getsymbol(exp[2])
535 return (runmember, (darg, memb))
535 return (runmember, (darg, memb))
536
536
537 def runmember(context, mapping, data):
537 def runmember(context, mapping, data):
538 darg, memb = data
538 darg, memb = data
539 d = evalrawexp(context, mapping, darg)
539 d = evalrawexp(context, mapping, darg)
540 if util.safehasattr(d, 'tomap'):
540 if util.safehasattr(d, 'tomap'):
541 lm = mapping.copy()
541 lm = mapping.copy()
542 lm.update(d.tomap())
542 lm.update(d.tomap())
543 return runsymbol(context, lm, memb)
543 return runsymbol(context, lm, memb)
544 if util.safehasattr(d, 'get'):
544 if util.safehasattr(d, 'get'):
545 return _getdictitem(d, memb)
545 return _getdictitem(d, memb)
546
546
547 sym = findsymbolicname(darg)
547 sym = findsymbolicname(darg)
548 if sym:
548 if sym:
549 raise error.ParseError(_("keyword '%s' has no member") % sym)
549 raise error.ParseError(_("keyword '%s' has no member") % sym)
550 else:
550 else:
551 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
551 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
552
552
553 def buildnegate(exp, context):
553 def buildnegate(exp, context):
554 arg = compileexp(exp[1], context, exprmethods)
554 arg = compileexp(exp[1], context, exprmethods)
555 return (runnegate, arg)
555 return (runnegate, arg)
556
556
557 def runnegate(context, mapping, data):
557 def runnegate(context, mapping, data):
558 data = evalinteger(context, mapping, data,
558 data = evalinteger(context, mapping, data,
559 _('negation needs an integer argument'))
559 _('negation needs an integer argument'))
560 return -data
560 return -data
561
561
562 def buildarithmetic(exp, context, func):
562 def buildarithmetic(exp, context, func):
563 left = compileexp(exp[1], context, exprmethods)
563 left = compileexp(exp[1], context, exprmethods)
564 right = compileexp(exp[2], context, exprmethods)
564 right = compileexp(exp[2], context, exprmethods)
565 return (runarithmetic, (func, left, right))
565 return (runarithmetic, (func, left, right))
566
566
567 def runarithmetic(context, mapping, data):
567 def runarithmetic(context, mapping, data):
568 func, left, right = data
568 func, left, right = data
569 left = evalinteger(context, mapping, left,
569 left = evalinteger(context, mapping, left,
570 _('arithmetic only defined on integers'))
570 _('arithmetic only defined on integers'))
571 right = evalinteger(context, mapping, right,
571 right = evalinteger(context, mapping, right,
572 _('arithmetic only defined on integers'))
572 _('arithmetic only defined on integers'))
573 try:
573 try:
574 return func(left, right)
574 return func(left, right)
575 except ZeroDivisionError:
575 except ZeroDivisionError:
576 raise error.Abort(_('division by zero is not defined'))
576 raise error.Abort(_('division by zero is not defined'))
577
577
578 def buildfunc(exp, context):
578 def buildfunc(exp, context):
579 n = getsymbol(exp[1])
579 n = getsymbol(exp[1])
580 if n in funcs:
580 if n in funcs:
581 f = funcs[n]
581 f = funcs[n]
582 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
582 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
583 return (f, args)
583 return (f, args)
584 if n in context._filters:
584 if n in context._filters:
585 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
585 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
586 if len(args) != 1:
586 if len(args) != 1:
587 raise error.ParseError(_("filter %s expects one argument") % n)
587 raise error.ParseError(_("filter %s expects one argument") % n)
588 f = context._filters[n]
588 f = context._filters[n]
589 return (runfilter, (args[0], f))
589 return (runfilter, (args[0], f))
590 raise error.ParseError(_("unknown function '%s'") % n)
590 raise error.ParseError(_("unknown function '%s'") % n)
591
591
592 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
592 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
593 """Compile parsed tree of function arguments into list or dict of
593 """Compile parsed tree of function arguments into list or dict of
594 (func, data) pairs
594 (func, data) pairs
595
595
596 >>> context = engine(lambda t: (runsymbol, t))
596 >>> context = engine(lambda t: (runsymbol, t))
597 >>> def fargs(expr, argspec):
597 >>> def fargs(expr, argspec):
598 ... x = _parseexpr(expr)
598 ... x = _parseexpr(expr)
599 ... n = getsymbol(x[1])
599 ... n = getsymbol(x[1])
600 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
600 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
601 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
601 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
602 ['l', 'k']
602 ['l', 'k']
603 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
603 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
604 >>> list(args.keys()), list(args[b'opts'].keys())
604 >>> list(args.keys()), list(args[b'opts'].keys())
605 (['opts'], ['opts', 'k'])
605 (['opts'], ['opts', 'k'])
606 """
606 """
607 def compiledict(xs):
607 def compiledict(xs):
608 return util.sortdict((k, compileexp(x, context, curmethods))
608 return util.sortdict((k, compileexp(x, context, curmethods))
609 for k, x in xs.iteritems())
609 for k, x in xs.iteritems())
610 def compilelist(xs):
610 def compilelist(xs):
611 return [compileexp(x, context, curmethods) for x in xs]
611 return [compileexp(x, context, curmethods) for x in xs]
612
612
613 if not argspec:
613 if not argspec:
614 # filter or function with no argspec: return list of positional args
614 # filter or function with no argspec: return list of positional args
615 return compilelist(getlist(exp))
615 return compilelist(getlist(exp))
616
616
617 # function with argspec: return dict of named args
617 # function with argspec: return dict of named args
618 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
618 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
619 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
619 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
620 keyvaluenode='keyvalue', keynode='symbol')
620 keyvaluenode='keyvalue', keynode='symbol')
621 compargs = util.sortdict()
621 compargs = util.sortdict()
622 if varkey:
622 if varkey:
623 compargs[varkey] = compilelist(treeargs.pop(varkey))
623 compargs[varkey] = compilelist(treeargs.pop(varkey))
624 if optkey:
624 if optkey:
625 compargs[optkey] = compiledict(treeargs.pop(optkey))
625 compargs[optkey] = compiledict(treeargs.pop(optkey))
626 compargs.update(compiledict(treeargs))
626 compargs.update(compiledict(treeargs))
627 return compargs
627 return compargs
628
628
629 def buildkeyvaluepair(exp, content):
629 def buildkeyvaluepair(exp, content):
630 raise error.ParseError(_("can't use a key-value pair in this context"))
630 raise error.ParseError(_("can't use a key-value pair in this context"))
631
631
632 # dict of template built-in functions
632 # dict of template built-in functions
633 funcs = {}
633 funcs = {}
634
634
635 templatefunc = registrar.templatefunc(funcs)
635 templatefunc = registrar.templatefunc(funcs)
636
636
637 @templatefunc('date(date[, fmt])')
637 @templatefunc('date(date[, fmt])')
638 def date(context, mapping, args):
638 def date(context, mapping, args):
639 """Format a date. See :hg:`help dates` for formatting
639 """Format a date. See :hg:`help dates` for formatting
640 strings. The default is a Unix date format, including the timezone:
640 strings. The default is a Unix date format, including the timezone:
641 "Mon Sep 04 15:13:13 2006 0700"."""
641 "Mon Sep 04 15:13:13 2006 0700"."""
642 if not (1 <= len(args) <= 2):
642 if not (1 <= len(args) <= 2):
643 # i18n: "date" is a keyword
643 # i18n: "date" is a keyword
644 raise error.ParseError(_("date expects one or two arguments"))
644 raise error.ParseError(_("date expects one or two arguments"))
645
645
646 date = evalfuncarg(context, mapping, args[0])
646 date = evalfuncarg(context, mapping, args[0])
647 fmt = None
647 fmt = None
648 if len(args) == 2:
648 if len(args) == 2:
649 fmt = evalstring(context, mapping, args[1])
649 fmt = evalstring(context, mapping, args[1])
650 try:
650 try:
651 if fmt is None:
651 if fmt is None:
652 return util.datestr(date)
652 return util.datestr(date)
653 else:
653 else:
654 return util.datestr(date, fmt)
654 return util.datestr(date, fmt)
655 except (TypeError, ValueError):
655 except (TypeError, ValueError):
656 # i18n: "date" is a keyword
656 # i18n: "date" is a keyword
657 raise error.ParseError(_("date expects a date information"))
657 raise error.ParseError(_("date expects a date information"))
658
658
659 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
659 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
660 def dict_(context, mapping, args):
660 def dict_(context, mapping, args):
661 """Construct a dict from key-value pairs. A key may be omitted if
661 """Construct a dict from key-value pairs. A key may be omitted if
662 a value expression can provide an unambiguous name."""
662 a value expression can provide an unambiguous name."""
663 data = util.sortdict()
663 data = util.sortdict()
664
664
665 for v in args['args']:
665 for v in args['args']:
666 k = findsymbolicname(v)
666 k = findsymbolicname(v)
667 if not k:
667 if not k:
668 raise error.ParseError(_('dict key cannot be inferred'))
668 raise error.ParseError(_('dict key cannot be inferred'))
669 if k in data or k in args['kwargs']:
669 if k in data or k in args['kwargs']:
670 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
670 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
671 data[k] = evalfuncarg(context, mapping, v)
671 data[k] = evalfuncarg(context, mapping, v)
672
672
673 data.update((k, evalfuncarg(context, mapping, v))
673 data.update((k, evalfuncarg(context, mapping, v))
674 for k, v in args['kwargs'].iteritems())
674 for k, v in args['kwargs'].iteritems())
675 return templatekw.hybriddict(data)
675 return templatekw.hybriddict(data)
676
676
677 @templatefunc('diff([includepattern [, excludepattern]])')
677 @templatefunc('diff([includepattern [, excludepattern]])')
678 def diff(context, mapping, args):
678 def diff(context, mapping, args):
679 """Show a diff, optionally
679 """Show a diff, optionally
680 specifying files to include or exclude."""
680 specifying files to include or exclude."""
681 if len(args) > 2:
681 if len(args) > 2:
682 # i18n: "diff" is a keyword
682 # i18n: "diff" is a keyword
683 raise error.ParseError(_("diff expects zero, one, or two arguments"))
683 raise error.ParseError(_("diff expects zero, one, or two arguments"))
684
684
685 def getpatterns(i):
685 def getpatterns(i):
686 if i < len(args):
686 if i < len(args):
687 s = evalstring(context, mapping, args[i]).strip()
687 s = evalstring(context, mapping, args[i]).strip()
688 if s:
688 if s:
689 return [s]
689 return [s]
690 return []
690 return []
691
691
692 ctx = context.resource(mapping, 'ctx')
692 ctx = context.resource(mapping, 'ctx')
693 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
693 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
694
694
695 return ''.join(chunks)
695 return ''.join(chunks)
696
696
697 @templatefunc('extdata(source)', argspec='source')
697 @templatefunc('extdata(source)', argspec='source')
698 def extdata(context, mapping, args):
698 def extdata(context, mapping, args):
699 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
699 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
700 if 'source' not in args:
700 if 'source' not in args:
701 # i18n: "extdata" is a keyword
701 # i18n: "extdata" is a keyword
702 raise error.ParseError(_('extdata expects one argument'))
702 raise error.ParseError(_('extdata expects one argument'))
703
703
704 source = evalstring(context, mapping, args['source'])
704 source = evalstring(context, mapping, args['source'])
705 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
705 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
706 ctx = context.resource(mapping, 'ctx')
706 ctx = context.resource(mapping, 'ctx')
707 if source in cache:
707 if source in cache:
708 data = cache[source]
708 data = cache[source]
709 else:
709 else:
710 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
710 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
711 return data.get(ctx.rev(), '')
711 return data.get(ctx.rev(), '')
712
712
713 @templatefunc('files(pattern)')
713 @templatefunc('files(pattern)')
714 def files(context, mapping, args):
714 def files(context, mapping, args):
715 """All files of the current changeset matching the pattern. See
715 """All files of the current changeset matching the pattern. See
716 :hg:`help patterns`."""
716 :hg:`help patterns`."""
717 if not len(args) == 1:
717 if not len(args) == 1:
718 # i18n: "files" is a keyword
718 # i18n: "files" is a keyword
719 raise error.ParseError(_("files expects one argument"))
719 raise error.ParseError(_("files expects one argument"))
720
720
721 raw = evalstring(context, mapping, args[0])
721 raw = evalstring(context, mapping, args[0])
722 ctx = context.resource(mapping, 'ctx')
722 ctx = context.resource(mapping, 'ctx')
723 m = ctx.match([raw])
723 m = ctx.match([raw])
724 files = list(ctx.matches(m))
724 files = list(ctx.matches(m))
725 # TODO: pass (context, mapping) pair to keyword function
725 return templatekw.compatlist(context, mapping, "file", files)
726 props = context._resources.copy()
727 props.update(mapping)
728 return templatekw.showlist("file", files, props)
729
726
730 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
727 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
731 def fill(context, mapping, args):
728 def fill(context, mapping, args):
732 """Fill many
729 """Fill many
733 paragraphs with optional indentation. See the "fill" filter."""
730 paragraphs with optional indentation. See the "fill" filter."""
734 if not (1 <= len(args) <= 4):
731 if not (1 <= len(args) <= 4):
735 # i18n: "fill" is a keyword
732 # i18n: "fill" is a keyword
736 raise error.ParseError(_("fill expects one to four arguments"))
733 raise error.ParseError(_("fill expects one to four arguments"))
737
734
738 text = evalstring(context, mapping, args[0])
735 text = evalstring(context, mapping, args[0])
739 width = 76
736 width = 76
740 initindent = ''
737 initindent = ''
741 hangindent = ''
738 hangindent = ''
742 if 2 <= len(args) <= 4:
739 if 2 <= len(args) <= 4:
743 width = evalinteger(context, mapping, args[1],
740 width = evalinteger(context, mapping, args[1],
744 # i18n: "fill" is a keyword
741 # i18n: "fill" is a keyword
745 _("fill expects an integer width"))
742 _("fill expects an integer width"))
746 try:
743 try:
747 initindent = evalstring(context, mapping, args[2])
744 initindent = evalstring(context, mapping, args[2])
748 hangindent = evalstring(context, mapping, args[3])
745 hangindent = evalstring(context, mapping, args[3])
749 except IndexError:
746 except IndexError:
750 pass
747 pass
751
748
752 return templatefilters.fill(text, width, initindent, hangindent)
749 return templatefilters.fill(text, width, initindent, hangindent)
753
750
754 @templatefunc('formatnode(node)')
751 @templatefunc('formatnode(node)')
755 def formatnode(context, mapping, args):
752 def formatnode(context, mapping, args):
756 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
753 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
757 if len(args) != 1:
754 if len(args) != 1:
758 # i18n: "formatnode" is a keyword
755 # i18n: "formatnode" is a keyword
759 raise error.ParseError(_("formatnode expects one argument"))
756 raise error.ParseError(_("formatnode expects one argument"))
760
757
761 ui = context.resource(mapping, 'ui')
758 ui = context.resource(mapping, 'ui')
762 node = evalstring(context, mapping, args[0])
759 node = evalstring(context, mapping, args[0])
763 if ui.debugflag:
760 if ui.debugflag:
764 return node
761 return node
765 return templatefilters.short(node)
762 return templatefilters.short(node)
766
763
767 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
764 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
768 argspec='text width fillchar left')
765 argspec='text width fillchar left')
769 def pad(context, mapping, args):
766 def pad(context, mapping, args):
770 """Pad text with a
767 """Pad text with a
771 fill character."""
768 fill character."""
772 if 'text' not in args or 'width' not in args:
769 if 'text' not in args or 'width' not in args:
773 # i18n: "pad" is a keyword
770 # i18n: "pad" is a keyword
774 raise error.ParseError(_("pad() expects two to four arguments"))
771 raise error.ParseError(_("pad() expects two to four arguments"))
775
772
776 width = evalinteger(context, mapping, args['width'],
773 width = evalinteger(context, mapping, args['width'],
777 # i18n: "pad" is a keyword
774 # i18n: "pad" is a keyword
778 _("pad() expects an integer width"))
775 _("pad() expects an integer width"))
779
776
780 text = evalstring(context, mapping, args['text'])
777 text = evalstring(context, mapping, args['text'])
781
778
782 left = False
779 left = False
783 fillchar = ' '
780 fillchar = ' '
784 if 'fillchar' in args:
781 if 'fillchar' in args:
785 fillchar = evalstring(context, mapping, args['fillchar'])
782 fillchar = evalstring(context, mapping, args['fillchar'])
786 if len(color.stripeffects(fillchar)) != 1:
783 if len(color.stripeffects(fillchar)) != 1:
787 # i18n: "pad" is a keyword
784 # i18n: "pad" is a keyword
788 raise error.ParseError(_("pad() expects a single fill character"))
785 raise error.ParseError(_("pad() expects a single fill character"))
789 if 'left' in args:
786 if 'left' in args:
790 left = evalboolean(context, mapping, args['left'])
787 left = evalboolean(context, mapping, args['left'])
791
788
792 fillwidth = width - encoding.colwidth(color.stripeffects(text))
789 fillwidth = width - encoding.colwidth(color.stripeffects(text))
793 if fillwidth <= 0:
790 if fillwidth <= 0:
794 return text
791 return text
795 if left:
792 if left:
796 return fillchar * fillwidth + text
793 return fillchar * fillwidth + text
797 else:
794 else:
798 return text + fillchar * fillwidth
795 return text + fillchar * fillwidth
799
796
800 @templatefunc('indent(text, indentchars[, firstline])')
797 @templatefunc('indent(text, indentchars[, firstline])')
801 def indent(context, mapping, args):
798 def indent(context, mapping, args):
802 """Indents all non-empty lines
799 """Indents all non-empty lines
803 with the characters given in the indentchars string. An optional
800 with the characters given in the indentchars string. An optional
804 third parameter will override the indent for the first line only
801 third parameter will override the indent for the first line only
805 if present."""
802 if present."""
806 if not (2 <= len(args) <= 3):
803 if not (2 <= len(args) <= 3):
807 # i18n: "indent" is a keyword
804 # i18n: "indent" is a keyword
808 raise error.ParseError(_("indent() expects two or three arguments"))
805 raise error.ParseError(_("indent() expects two or three arguments"))
809
806
810 text = evalstring(context, mapping, args[0])
807 text = evalstring(context, mapping, args[0])
811 indent = evalstring(context, mapping, args[1])
808 indent = evalstring(context, mapping, args[1])
812
809
813 if len(args) == 3:
810 if len(args) == 3:
814 firstline = evalstring(context, mapping, args[2])
811 firstline = evalstring(context, mapping, args[2])
815 else:
812 else:
816 firstline = indent
813 firstline = indent
817
814
818 # the indent function doesn't indent the first line, so we do it here
815 # the indent function doesn't indent the first line, so we do it here
819 return templatefilters.indent(firstline + text, indent)
816 return templatefilters.indent(firstline + text, indent)
820
817
821 @templatefunc('get(dict, key)')
818 @templatefunc('get(dict, key)')
822 def get(context, mapping, args):
819 def get(context, mapping, args):
823 """Get an attribute/key from an object. Some keywords
820 """Get an attribute/key from an object. Some keywords
824 are complex types. This function allows you to obtain the value of an
821 are complex types. This function allows you to obtain the value of an
825 attribute on these types."""
822 attribute on these types."""
826 if len(args) != 2:
823 if len(args) != 2:
827 # i18n: "get" is a keyword
824 # i18n: "get" is a keyword
828 raise error.ParseError(_("get() expects two arguments"))
825 raise error.ParseError(_("get() expects two arguments"))
829
826
830 dictarg = evalfuncarg(context, mapping, args[0])
827 dictarg = evalfuncarg(context, mapping, args[0])
831 if not util.safehasattr(dictarg, 'get'):
828 if not util.safehasattr(dictarg, 'get'):
832 # i18n: "get" is a keyword
829 # i18n: "get" is a keyword
833 raise error.ParseError(_("get() expects a dict as first argument"))
830 raise error.ParseError(_("get() expects a dict as first argument"))
834
831
835 key = evalfuncarg(context, mapping, args[1])
832 key = evalfuncarg(context, mapping, args[1])
836 return _getdictitem(dictarg, key)
833 return _getdictitem(dictarg, key)
837
834
838 def _getdictitem(dictarg, key):
835 def _getdictitem(dictarg, key):
839 val = dictarg.get(key)
836 val = dictarg.get(key)
840 if val is None:
837 if val is None:
841 return
838 return
842 return templatekw.wraphybridvalue(dictarg, key, val)
839 return templatekw.wraphybridvalue(dictarg, key, val)
843
840
844 @templatefunc('if(expr, then[, else])')
841 @templatefunc('if(expr, then[, else])')
845 def if_(context, mapping, args):
842 def if_(context, mapping, args):
846 """Conditionally execute based on the result of
843 """Conditionally execute based on the result of
847 an expression."""
844 an expression."""
848 if not (2 <= len(args) <= 3):
845 if not (2 <= len(args) <= 3):
849 # i18n: "if" is a keyword
846 # i18n: "if" is a keyword
850 raise error.ParseError(_("if expects two or three arguments"))
847 raise error.ParseError(_("if expects two or three arguments"))
851
848
852 test = evalboolean(context, mapping, args[0])
849 test = evalboolean(context, mapping, args[0])
853 if test:
850 if test:
854 yield evalrawexp(context, mapping, args[1])
851 yield evalrawexp(context, mapping, args[1])
855 elif len(args) == 3:
852 elif len(args) == 3:
856 yield evalrawexp(context, mapping, args[2])
853 yield evalrawexp(context, mapping, args[2])
857
854
858 @templatefunc('ifcontains(needle, haystack, then[, else])')
855 @templatefunc('ifcontains(needle, haystack, then[, else])')
859 def ifcontains(context, mapping, args):
856 def ifcontains(context, mapping, args):
860 """Conditionally execute based
857 """Conditionally execute based
861 on whether the item "needle" is in "haystack"."""
858 on whether the item "needle" is in "haystack"."""
862 if not (3 <= len(args) <= 4):
859 if not (3 <= len(args) <= 4):
863 # i18n: "ifcontains" is a keyword
860 # i18n: "ifcontains" is a keyword
864 raise error.ParseError(_("ifcontains expects three or four arguments"))
861 raise error.ParseError(_("ifcontains expects three or four arguments"))
865
862
866 haystack = evalfuncarg(context, mapping, args[1])
863 haystack = evalfuncarg(context, mapping, args[1])
867 try:
864 try:
868 needle = evalastype(context, mapping, args[0],
865 needle = evalastype(context, mapping, args[0],
869 getattr(haystack, 'keytype', None) or bytes)
866 getattr(haystack, 'keytype', None) or bytes)
870 found = (needle in haystack)
867 found = (needle in haystack)
871 except error.ParseError:
868 except error.ParseError:
872 found = False
869 found = False
873
870
874 if found:
871 if found:
875 yield evalrawexp(context, mapping, args[2])
872 yield evalrawexp(context, mapping, args[2])
876 elif len(args) == 4:
873 elif len(args) == 4:
877 yield evalrawexp(context, mapping, args[3])
874 yield evalrawexp(context, mapping, args[3])
878
875
879 @templatefunc('ifeq(expr1, expr2, then[, else])')
876 @templatefunc('ifeq(expr1, expr2, then[, else])')
880 def ifeq(context, mapping, args):
877 def ifeq(context, mapping, args):
881 """Conditionally execute based on
878 """Conditionally execute based on
882 whether 2 items are equivalent."""
879 whether 2 items are equivalent."""
883 if not (3 <= len(args) <= 4):
880 if not (3 <= len(args) <= 4):
884 # i18n: "ifeq" is a keyword
881 # i18n: "ifeq" is a keyword
885 raise error.ParseError(_("ifeq expects three or four arguments"))
882 raise error.ParseError(_("ifeq expects three or four arguments"))
886
883
887 test = evalstring(context, mapping, args[0])
884 test = evalstring(context, mapping, args[0])
888 match = evalstring(context, mapping, args[1])
885 match = evalstring(context, mapping, args[1])
889 if test == match:
886 if test == match:
890 yield evalrawexp(context, mapping, args[2])
887 yield evalrawexp(context, mapping, args[2])
891 elif len(args) == 4:
888 elif len(args) == 4:
892 yield evalrawexp(context, mapping, args[3])
889 yield evalrawexp(context, mapping, args[3])
893
890
894 @templatefunc('join(list, sep)')
891 @templatefunc('join(list, sep)')
895 def join(context, mapping, args):
892 def join(context, mapping, args):
896 """Join items in a list with a delimiter."""
893 """Join items in a list with a delimiter."""
897 if not (1 <= len(args) <= 2):
894 if not (1 <= len(args) <= 2):
898 # i18n: "join" is a keyword
895 # i18n: "join" is a keyword
899 raise error.ParseError(_("join expects one or two arguments"))
896 raise error.ParseError(_("join expects one or two arguments"))
900
897
901 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
898 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
902 # abuses generator as a keyword that returns a list of dicts.
899 # abuses generator as a keyword that returns a list of dicts.
903 joinset = evalrawexp(context, mapping, args[0])
900 joinset = evalrawexp(context, mapping, args[0])
904 joinset = templatekw.unwrapvalue(joinset)
901 joinset = templatekw.unwrapvalue(joinset)
905 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
902 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
906 joiner = " "
903 joiner = " "
907 if len(args) > 1:
904 if len(args) > 1:
908 joiner = evalstring(context, mapping, args[1])
905 joiner = evalstring(context, mapping, args[1])
909
906
910 first = True
907 first = True
911 for x in pycompat.maybebytestr(joinset):
908 for x in pycompat.maybebytestr(joinset):
912 if first:
909 if first:
913 first = False
910 first = False
914 else:
911 else:
915 yield joiner
912 yield joiner
916 yield joinfmt(x)
913 yield joinfmt(x)
917
914
918 @templatefunc('label(label, expr)')
915 @templatefunc('label(label, expr)')
919 def label(context, mapping, args):
916 def label(context, mapping, args):
920 """Apply a label to generated content. Content with
917 """Apply a label to generated content. Content with
921 a label applied can result in additional post-processing, such as
918 a label applied can result in additional post-processing, such as
922 automatic colorization."""
919 automatic colorization."""
923 if len(args) != 2:
920 if len(args) != 2:
924 # i18n: "label" is a keyword
921 # i18n: "label" is a keyword
925 raise error.ParseError(_("label expects two arguments"))
922 raise error.ParseError(_("label expects two arguments"))
926
923
927 ui = context.resource(mapping, 'ui')
924 ui = context.resource(mapping, 'ui')
928 thing = evalstring(context, mapping, args[1])
925 thing = evalstring(context, mapping, args[1])
929 # preserve unknown symbol as literal so effects like 'red', 'bold',
926 # preserve unknown symbol as literal so effects like 'red', 'bold',
930 # etc. don't need to be quoted
927 # etc. don't need to be quoted
931 label = evalstringliteral(context, mapping, args[0])
928 label = evalstringliteral(context, mapping, args[0])
932
929
933 return ui.label(thing, label)
930 return ui.label(thing, label)
934
931
935 @templatefunc('latesttag([pattern])')
932 @templatefunc('latesttag([pattern])')
936 def latesttag(context, mapping, args):
933 def latesttag(context, mapping, args):
937 """The global tags matching the given pattern on the
934 """The global tags matching the given pattern on the
938 most recent globally tagged ancestor of this changeset.
935 most recent globally tagged ancestor of this changeset.
939 If no such tags exist, the "{tag}" template resolves to
936 If no such tags exist, the "{tag}" template resolves to
940 the string "null"."""
937 the string "null"."""
941 if len(args) > 1:
938 if len(args) > 1:
942 # i18n: "latesttag" is a keyword
939 # i18n: "latesttag" is a keyword
943 raise error.ParseError(_("latesttag expects at most one argument"))
940 raise error.ParseError(_("latesttag expects at most one argument"))
944
941
945 pattern = None
942 pattern = None
946 if len(args) == 1:
943 if len(args) == 1:
947 pattern = evalstring(context, mapping, args[0])
944 pattern = evalstring(context, mapping, args[0])
948
945
949 # TODO: pass (context, mapping) pair to keyword function
946 # TODO: pass (context, mapping) pair to keyword function
950 props = context._resources.copy()
947 props = context._resources.copy()
951 props.update(mapping)
948 props.update(mapping)
952 return templatekw.showlatesttags(pattern, **pycompat.strkwargs(props))
949 return templatekw.showlatesttags(pattern, **pycompat.strkwargs(props))
953
950
954 @templatefunc('localdate(date[, tz])')
951 @templatefunc('localdate(date[, tz])')
955 def localdate(context, mapping, args):
952 def localdate(context, mapping, args):
956 """Converts a date to the specified timezone.
953 """Converts a date to the specified timezone.
957 The default is local date."""
954 The default is local date."""
958 if not (1 <= len(args) <= 2):
955 if not (1 <= len(args) <= 2):
959 # i18n: "localdate" is a keyword
956 # i18n: "localdate" is a keyword
960 raise error.ParseError(_("localdate expects one or two arguments"))
957 raise error.ParseError(_("localdate expects one or two arguments"))
961
958
962 date = evalfuncarg(context, mapping, args[0])
959 date = evalfuncarg(context, mapping, args[0])
963 try:
960 try:
964 date = util.parsedate(date)
961 date = util.parsedate(date)
965 except AttributeError: # not str nor date tuple
962 except AttributeError: # not str nor date tuple
966 # i18n: "localdate" is a keyword
963 # i18n: "localdate" is a keyword
967 raise error.ParseError(_("localdate expects a date information"))
964 raise error.ParseError(_("localdate expects a date information"))
968 if len(args) >= 2:
965 if len(args) >= 2:
969 tzoffset = None
966 tzoffset = None
970 tz = evalfuncarg(context, mapping, args[1])
967 tz = evalfuncarg(context, mapping, args[1])
971 if isinstance(tz, bytes):
968 if isinstance(tz, bytes):
972 tzoffset, remainder = util.parsetimezone(tz)
969 tzoffset, remainder = util.parsetimezone(tz)
973 if remainder:
970 if remainder:
974 tzoffset = None
971 tzoffset = None
975 if tzoffset is None:
972 if tzoffset is None:
976 try:
973 try:
977 tzoffset = int(tz)
974 tzoffset = int(tz)
978 except (TypeError, ValueError):
975 except (TypeError, ValueError):
979 # i18n: "localdate" is a keyword
976 # i18n: "localdate" is a keyword
980 raise error.ParseError(_("localdate expects a timezone"))
977 raise error.ParseError(_("localdate expects a timezone"))
981 else:
978 else:
982 tzoffset = util.makedate()[1]
979 tzoffset = util.makedate()[1]
983 return (date[0], tzoffset)
980 return (date[0], tzoffset)
984
981
985 @templatefunc('max(iterable)')
982 @templatefunc('max(iterable)')
986 def max_(context, mapping, args, **kwargs):
983 def max_(context, mapping, args, **kwargs):
987 """Return the max of an iterable"""
984 """Return the max of an iterable"""
988 if len(args) != 1:
985 if len(args) != 1:
989 # i18n: "max" is a keyword
986 # i18n: "max" is a keyword
990 raise error.ParseError(_("max expects one argument"))
987 raise error.ParseError(_("max expects one argument"))
991
988
992 iterable = evalfuncarg(context, mapping, args[0])
989 iterable = evalfuncarg(context, mapping, args[0])
993 try:
990 try:
994 x = max(pycompat.maybebytestr(iterable))
991 x = max(pycompat.maybebytestr(iterable))
995 except (TypeError, ValueError):
992 except (TypeError, ValueError):
996 # i18n: "max" is a keyword
993 # i18n: "max" is a keyword
997 raise error.ParseError(_("max first argument should be an iterable"))
994 raise error.ParseError(_("max first argument should be an iterable"))
998 return templatekw.wraphybridvalue(iterable, x, x)
995 return templatekw.wraphybridvalue(iterable, x, x)
999
996
1000 @templatefunc('min(iterable)')
997 @templatefunc('min(iterable)')
1001 def min_(context, mapping, args, **kwargs):
998 def min_(context, mapping, args, **kwargs):
1002 """Return the min of an iterable"""
999 """Return the min of an iterable"""
1003 if len(args) != 1:
1000 if len(args) != 1:
1004 # i18n: "min" is a keyword
1001 # i18n: "min" is a keyword
1005 raise error.ParseError(_("min expects one argument"))
1002 raise error.ParseError(_("min expects one argument"))
1006
1003
1007 iterable = evalfuncarg(context, mapping, args[0])
1004 iterable = evalfuncarg(context, mapping, args[0])
1008 try:
1005 try:
1009 x = min(pycompat.maybebytestr(iterable))
1006 x = min(pycompat.maybebytestr(iterable))
1010 except (TypeError, ValueError):
1007 except (TypeError, ValueError):
1011 # i18n: "min" is a keyword
1008 # i18n: "min" is a keyword
1012 raise error.ParseError(_("min first argument should be an iterable"))
1009 raise error.ParseError(_("min first argument should be an iterable"))
1013 return templatekw.wraphybridvalue(iterable, x, x)
1010 return templatekw.wraphybridvalue(iterable, x, x)
1014
1011
1015 @templatefunc('mod(a, b)')
1012 @templatefunc('mod(a, b)')
1016 def mod(context, mapping, args):
1013 def mod(context, mapping, args):
1017 """Calculate a mod b such that a / b + a mod b == a"""
1014 """Calculate a mod b such that a / b + a mod b == a"""
1018 if not len(args) == 2:
1015 if not len(args) == 2:
1019 # i18n: "mod" is a keyword
1016 # i18n: "mod" is a keyword
1020 raise error.ParseError(_("mod expects two arguments"))
1017 raise error.ParseError(_("mod expects two arguments"))
1021
1018
1022 func = lambda a, b: a % b
1019 func = lambda a, b: a % b
1023 return runarithmetic(context, mapping, (func, args[0], args[1]))
1020 return runarithmetic(context, mapping, (func, args[0], args[1]))
1024
1021
1025 @templatefunc('obsfateoperations(markers)')
1022 @templatefunc('obsfateoperations(markers)')
1026 def obsfateoperations(context, mapping, args):
1023 def obsfateoperations(context, mapping, args):
1027 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1024 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1028 if len(args) != 1:
1025 if len(args) != 1:
1029 # i18n: "obsfateoperations" is a keyword
1026 # i18n: "obsfateoperations" is a keyword
1030 raise error.ParseError(_("obsfateoperations expects one argument"))
1027 raise error.ParseError(_("obsfateoperations expects one argument"))
1031
1028
1032 markers = evalfuncarg(context, mapping, args[0])
1029 markers = evalfuncarg(context, mapping, args[0])
1033
1030
1034 try:
1031 try:
1035 data = obsutil.markersoperations(markers)
1032 data = obsutil.markersoperations(markers)
1036 return templatekw.hybridlist(data, name='operation')
1033 return templatekw.hybridlist(data, name='operation')
1037 except (TypeError, KeyError):
1034 except (TypeError, KeyError):
1038 # i18n: "obsfateoperations" is a keyword
1035 # i18n: "obsfateoperations" is a keyword
1039 errmsg = _("obsfateoperations first argument should be an iterable")
1036 errmsg = _("obsfateoperations first argument should be an iterable")
1040 raise error.ParseError(errmsg)
1037 raise error.ParseError(errmsg)
1041
1038
1042 @templatefunc('obsfatedate(markers)')
1039 @templatefunc('obsfatedate(markers)')
1043 def obsfatedate(context, mapping, args):
1040 def obsfatedate(context, mapping, args):
1044 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1041 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1045 if len(args) != 1:
1042 if len(args) != 1:
1046 # i18n: "obsfatedate" is a keyword
1043 # i18n: "obsfatedate" is a keyword
1047 raise error.ParseError(_("obsfatedate expects one argument"))
1044 raise error.ParseError(_("obsfatedate expects one argument"))
1048
1045
1049 markers = evalfuncarg(context, mapping, args[0])
1046 markers = evalfuncarg(context, mapping, args[0])
1050
1047
1051 try:
1048 try:
1052 data = obsutil.markersdates(markers)
1049 data = obsutil.markersdates(markers)
1053 return templatekw.hybridlist(data, name='date', fmt='%d %d')
1050 return templatekw.hybridlist(data, name='date', fmt='%d %d')
1054 except (TypeError, KeyError):
1051 except (TypeError, KeyError):
1055 # i18n: "obsfatedate" is a keyword
1052 # i18n: "obsfatedate" is a keyword
1056 errmsg = _("obsfatedate first argument should be an iterable")
1053 errmsg = _("obsfatedate first argument should be an iterable")
1057 raise error.ParseError(errmsg)
1054 raise error.ParseError(errmsg)
1058
1055
1059 @templatefunc('obsfateusers(markers)')
1056 @templatefunc('obsfateusers(markers)')
1060 def obsfateusers(context, mapping, args):
1057 def obsfateusers(context, mapping, args):
1061 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1058 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
1062 if len(args) != 1:
1059 if len(args) != 1:
1063 # i18n: "obsfateusers" is a keyword
1060 # i18n: "obsfateusers" is a keyword
1064 raise error.ParseError(_("obsfateusers expects one argument"))
1061 raise error.ParseError(_("obsfateusers expects one argument"))
1065
1062
1066 markers = evalfuncarg(context, mapping, args[0])
1063 markers = evalfuncarg(context, mapping, args[0])
1067
1064
1068 try:
1065 try:
1069 data = obsutil.markersusers(markers)
1066 data = obsutil.markersusers(markers)
1070 return templatekw.hybridlist(data, name='user')
1067 return templatekw.hybridlist(data, name='user')
1071 except (TypeError, KeyError, ValueError):
1068 except (TypeError, KeyError, ValueError):
1072 # i18n: "obsfateusers" is a keyword
1069 # i18n: "obsfateusers" is a keyword
1073 msg = _("obsfateusers first argument should be an iterable of "
1070 msg = _("obsfateusers first argument should be an iterable of "
1074 "obsmakers")
1071 "obsmakers")
1075 raise error.ParseError(msg)
1072 raise error.ParseError(msg)
1076
1073
1077 @templatefunc('obsfateverb(successors, markers)')
1074 @templatefunc('obsfateverb(successors, markers)')
1078 def obsfateverb(context, mapping, args):
1075 def obsfateverb(context, mapping, args):
1079 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
1076 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
1080 if len(args) != 2:
1077 if len(args) != 2:
1081 # i18n: "obsfateverb" is a keyword
1078 # i18n: "obsfateverb" is a keyword
1082 raise error.ParseError(_("obsfateverb expects two arguments"))
1079 raise error.ParseError(_("obsfateverb expects two arguments"))
1083
1080
1084 successors = evalfuncarg(context, mapping, args[0])
1081 successors = evalfuncarg(context, mapping, args[0])
1085 markers = evalfuncarg(context, mapping, args[1])
1082 markers = evalfuncarg(context, mapping, args[1])
1086
1083
1087 try:
1084 try:
1088 return obsutil.obsfateverb(successors, markers)
1085 return obsutil.obsfateverb(successors, markers)
1089 except TypeError:
1086 except TypeError:
1090 # i18n: "obsfateverb" is a keyword
1087 # i18n: "obsfateverb" is a keyword
1091 errmsg = _("obsfateverb first argument should be countable")
1088 errmsg = _("obsfateverb first argument should be countable")
1092 raise error.ParseError(errmsg)
1089 raise error.ParseError(errmsg)
1093
1090
1094 @templatefunc('relpath(path)')
1091 @templatefunc('relpath(path)')
1095 def relpath(context, mapping, args):
1092 def relpath(context, mapping, args):
1096 """Convert a repository-absolute path into a filesystem path relative to
1093 """Convert a repository-absolute path into a filesystem path relative to
1097 the current working directory."""
1094 the current working directory."""
1098 if len(args) != 1:
1095 if len(args) != 1:
1099 # i18n: "relpath" is a keyword
1096 # i18n: "relpath" is a keyword
1100 raise error.ParseError(_("relpath expects one argument"))
1097 raise error.ParseError(_("relpath expects one argument"))
1101
1098
1102 repo = context.resource(mapping, 'ctx').repo()
1099 repo = context.resource(mapping, 'ctx').repo()
1103 path = evalstring(context, mapping, args[0])
1100 path = evalstring(context, mapping, args[0])
1104 return repo.pathto(path)
1101 return repo.pathto(path)
1105
1102
1106 @templatefunc('revset(query[, formatargs...])')
1103 @templatefunc('revset(query[, formatargs...])')
1107 def revset(context, mapping, args):
1104 def revset(context, mapping, args):
1108 """Execute a revision set query. See
1105 """Execute a revision set query. See
1109 :hg:`help revset`."""
1106 :hg:`help revset`."""
1110 if not len(args) > 0:
1107 if not len(args) > 0:
1111 # i18n: "revset" is a keyword
1108 # i18n: "revset" is a keyword
1112 raise error.ParseError(_("revset expects one or more arguments"))
1109 raise error.ParseError(_("revset expects one or more arguments"))
1113
1110
1114 raw = evalstring(context, mapping, args[0])
1111 raw = evalstring(context, mapping, args[0])
1115 ctx = context.resource(mapping, 'ctx')
1112 ctx = context.resource(mapping, 'ctx')
1116 repo = ctx.repo()
1113 repo = ctx.repo()
1117
1114
1118 def query(expr):
1115 def query(expr):
1119 m = revsetmod.match(repo.ui, expr, repo=repo)
1116 m = revsetmod.match(repo.ui, expr, repo=repo)
1120 return m(repo)
1117 return m(repo)
1121
1118
1122 if len(args) > 1:
1119 if len(args) > 1:
1123 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
1120 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
1124 revs = query(revsetlang.formatspec(raw, *formatargs))
1121 revs = query(revsetlang.formatspec(raw, *formatargs))
1125 revs = list(revs)
1122 revs = list(revs)
1126 else:
1123 else:
1127 cache = context.resource(mapping, 'cache')
1124 cache = context.resource(mapping, 'cache')
1128 revsetcache = cache.setdefault("revsetcache", {})
1125 revsetcache = cache.setdefault("revsetcache", {})
1129 if raw in revsetcache:
1126 if raw in revsetcache:
1130 revs = revsetcache[raw]
1127 revs = revsetcache[raw]
1131 else:
1128 else:
1132 revs = query(raw)
1129 revs = query(raw)
1133 revs = list(revs)
1130 revs = list(revs)
1134 revsetcache[raw] = revs
1131 revsetcache[raw] = revs
1135
1132
1136 # TODO: pass (context, mapping) pair to keyword function
1133 # TODO: pass (context, mapping) pair to keyword function
1137 props = context._resources.copy()
1134 props = context._resources.copy()
1138 props.update(mapping)
1135 props.update(mapping)
1139 return templatekw.showrevslist("revision", revs,
1136 return templatekw.showrevslist("revision", revs,
1140 **pycompat.strkwargs(props))
1137 **pycompat.strkwargs(props))
1141
1138
1142 @templatefunc('rstdoc(text, style)')
1139 @templatefunc('rstdoc(text, style)')
1143 def rstdoc(context, mapping, args):
1140 def rstdoc(context, mapping, args):
1144 """Format reStructuredText."""
1141 """Format reStructuredText."""
1145 if len(args) != 2:
1142 if len(args) != 2:
1146 # i18n: "rstdoc" is a keyword
1143 # i18n: "rstdoc" is a keyword
1147 raise error.ParseError(_("rstdoc expects two arguments"))
1144 raise error.ParseError(_("rstdoc expects two arguments"))
1148
1145
1149 text = evalstring(context, mapping, args[0])
1146 text = evalstring(context, mapping, args[0])
1150 style = evalstring(context, mapping, args[1])
1147 style = evalstring(context, mapping, args[1])
1151
1148
1152 return minirst.format(text, style=style, keep=['verbose'])
1149 return minirst.format(text, style=style, keep=['verbose'])
1153
1150
1154 @templatefunc('separate(sep, args)', argspec='sep *args')
1151 @templatefunc('separate(sep, args)', argspec='sep *args')
1155 def separate(context, mapping, args):
1152 def separate(context, mapping, args):
1156 """Add a separator between non-empty arguments."""
1153 """Add a separator between non-empty arguments."""
1157 if 'sep' not in args:
1154 if 'sep' not in args:
1158 # i18n: "separate" is a keyword
1155 # i18n: "separate" is a keyword
1159 raise error.ParseError(_("separate expects at least one argument"))
1156 raise error.ParseError(_("separate expects at least one argument"))
1160
1157
1161 sep = evalstring(context, mapping, args['sep'])
1158 sep = evalstring(context, mapping, args['sep'])
1162 first = True
1159 first = True
1163 for arg in args['args']:
1160 for arg in args['args']:
1164 argstr = evalstring(context, mapping, arg)
1161 argstr = evalstring(context, mapping, arg)
1165 if not argstr:
1162 if not argstr:
1166 continue
1163 continue
1167 if first:
1164 if first:
1168 first = False
1165 first = False
1169 else:
1166 else:
1170 yield sep
1167 yield sep
1171 yield argstr
1168 yield argstr
1172
1169
1173 @templatefunc('shortest(node, minlength=4)')
1170 @templatefunc('shortest(node, minlength=4)')
1174 def shortest(context, mapping, args):
1171 def shortest(context, mapping, args):
1175 """Obtain the shortest representation of
1172 """Obtain the shortest representation of
1176 a node."""
1173 a node."""
1177 if not (1 <= len(args) <= 2):
1174 if not (1 <= len(args) <= 2):
1178 # i18n: "shortest" is a keyword
1175 # i18n: "shortest" is a keyword
1179 raise error.ParseError(_("shortest() expects one or two arguments"))
1176 raise error.ParseError(_("shortest() expects one or two arguments"))
1180
1177
1181 node = evalstring(context, mapping, args[0])
1178 node = evalstring(context, mapping, args[0])
1182
1179
1183 minlength = 4
1180 minlength = 4
1184 if len(args) > 1:
1181 if len(args) > 1:
1185 minlength = evalinteger(context, mapping, args[1],
1182 minlength = evalinteger(context, mapping, args[1],
1186 # i18n: "shortest" is a keyword
1183 # i18n: "shortest" is a keyword
1187 _("shortest() expects an integer minlength"))
1184 _("shortest() expects an integer minlength"))
1188
1185
1189 # _partialmatch() of filtered changelog could take O(len(repo)) time,
1186 # _partialmatch() of filtered changelog could take O(len(repo)) time,
1190 # which would be unacceptably slow. so we look for hash collision in
1187 # which would be unacceptably slow. so we look for hash collision in
1191 # unfiltered space, which means some hashes may be slightly longer.
1188 # unfiltered space, which means some hashes may be slightly longer.
1192 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
1189 cl = context.resource(mapping, 'ctx')._repo.unfiltered().changelog
1193 return cl.shortest(node, minlength)
1190 return cl.shortest(node, minlength)
1194
1191
1195 @templatefunc('strip(text[, chars])')
1192 @templatefunc('strip(text[, chars])')
1196 def strip(context, mapping, args):
1193 def strip(context, mapping, args):
1197 """Strip characters from a string. By default,
1194 """Strip characters from a string. By default,
1198 strips all leading and trailing whitespace."""
1195 strips all leading and trailing whitespace."""
1199 if not (1 <= len(args) <= 2):
1196 if not (1 <= len(args) <= 2):
1200 # i18n: "strip" is a keyword
1197 # i18n: "strip" is a keyword
1201 raise error.ParseError(_("strip expects one or two arguments"))
1198 raise error.ParseError(_("strip expects one or two arguments"))
1202
1199
1203 text = evalstring(context, mapping, args[0])
1200 text = evalstring(context, mapping, args[0])
1204 if len(args) == 2:
1201 if len(args) == 2:
1205 chars = evalstring(context, mapping, args[1])
1202 chars = evalstring(context, mapping, args[1])
1206 return text.strip(chars)
1203 return text.strip(chars)
1207 return text.strip()
1204 return text.strip()
1208
1205
1209 @templatefunc('sub(pattern, replacement, expression)')
1206 @templatefunc('sub(pattern, replacement, expression)')
1210 def sub(context, mapping, args):
1207 def sub(context, mapping, args):
1211 """Perform text substitution
1208 """Perform text substitution
1212 using regular expressions."""
1209 using regular expressions."""
1213 if len(args) != 3:
1210 if len(args) != 3:
1214 # i18n: "sub" is a keyword
1211 # i18n: "sub" is a keyword
1215 raise error.ParseError(_("sub expects three arguments"))
1212 raise error.ParseError(_("sub expects three arguments"))
1216
1213
1217 pat = evalstring(context, mapping, args[0])
1214 pat = evalstring(context, mapping, args[0])
1218 rpl = evalstring(context, mapping, args[1])
1215 rpl = evalstring(context, mapping, args[1])
1219 src = evalstring(context, mapping, args[2])
1216 src = evalstring(context, mapping, args[2])
1220 try:
1217 try:
1221 patre = re.compile(pat)
1218 patre = re.compile(pat)
1222 except re.error:
1219 except re.error:
1223 # i18n: "sub" is a keyword
1220 # i18n: "sub" is a keyword
1224 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1221 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1225 try:
1222 try:
1226 yield patre.sub(rpl, src)
1223 yield patre.sub(rpl, src)
1227 except re.error:
1224 except re.error:
1228 # i18n: "sub" is a keyword
1225 # i18n: "sub" is a keyword
1229 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1226 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1230
1227
1231 @templatefunc('startswith(pattern, text)')
1228 @templatefunc('startswith(pattern, text)')
1232 def startswith(context, mapping, args):
1229 def startswith(context, mapping, args):
1233 """Returns the value from the "text" argument
1230 """Returns the value from the "text" argument
1234 if it begins with the content from the "pattern" argument."""
1231 if it begins with the content from the "pattern" argument."""
1235 if len(args) != 2:
1232 if len(args) != 2:
1236 # i18n: "startswith" is a keyword
1233 # i18n: "startswith" is a keyword
1237 raise error.ParseError(_("startswith expects two arguments"))
1234 raise error.ParseError(_("startswith expects two arguments"))
1238
1235
1239 patn = evalstring(context, mapping, args[0])
1236 patn = evalstring(context, mapping, args[0])
1240 text = evalstring(context, mapping, args[1])
1237 text = evalstring(context, mapping, args[1])
1241 if text.startswith(patn):
1238 if text.startswith(patn):
1242 return text
1239 return text
1243 return ''
1240 return ''
1244
1241
1245 @templatefunc('word(number, text[, separator])')
1242 @templatefunc('word(number, text[, separator])')
1246 def word(context, mapping, args):
1243 def word(context, mapping, args):
1247 """Return the nth word from a string."""
1244 """Return the nth word from a string."""
1248 if not (2 <= len(args) <= 3):
1245 if not (2 <= len(args) <= 3):
1249 # i18n: "word" is a keyword
1246 # i18n: "word" is a keyword
1250 raise error.ParseError(_("word expects two or three arguments, got %d")
1247 raise error.ParseError(_("word expects two or three arguments, got %d")
1251 % len(args))
1248 % len(args))
1252
1249
1253 num = evalinteger(context, mapping, args[0],
1250 num = evalinteger(context, mapping, args[0],
1254 # i18n: "word" is a keyword
1251 # i18n: "word" is a keyword
1255 _("word expects an integer index"))
1252 _("word expects an integer index"))
1256 text = evalstring(context, mapping, args[1])
1253 text = evalstring(context, mapping, args[1])
1257 if len(args) == 3:
1254 if len(args) == 3:
1258 splitter = evalstring(context, mapping, args[2])
1255 splitter = evalstring(context, mapping, args[2])
1259 else:
1256 else:
1260 splitter = None
1257 splitter = None
1261
1258
1262 tokens = text.split(splitter)
1259 tokens = text.split(splitter)
1263 if num >= len(tokens) or num < -len(tokens):
1260 if num >= len(tokens) or num < -len(tokens):
1264 return ''
1261 return ''
1265 else:
1262 else:
1266 return tokens[num]
1263 return tokens[num]
1267
1264
1268 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1265 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1269 exprmethods = {
1266 exprmethods = {
1270 "integer": lambda e, c: (runinteger, e[1]),
1267 "integer": lambda e, c: (runinteger, e[1]),
1271 "string": lambda e, c: (runstring, e[1]),
1268 "string": lambda e, c: (runstring, e[1]),
1272 "symbol": lambda e, c: (runsymbol, e[1]),
1269 "symbol": lambda e, c: (runsymbol, e[1]),
1273 "template": buildtemplate,
1270 "template": buildtemplate,
1274 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1271 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1275 ".": buildmember,
1272 ".": buildmember,
1276 "|": buildfilter,
1273 "|": buildfilter,
1277 "%": buildmap,
1274 "%": buildmap,
1278 "func": buildfunc,
1275 "func": buildfunc,
1279 "keyvalue": buildkeyvaluepair,
1276 "keyvalue": buildkeyvaluepair,
1280 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1277 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1281 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1278 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1282 "negate": buildnegate,
1279 "negate": buildnegate,
1283 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1280 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1284 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1281 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1285 }
1282 }
1286
1283
1287 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1284 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1288 methods = exprmethods.copy()
1285 methods = exprmethods.copy()
1289 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1286 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1290
1287
1291 class _aliasrules(parser.basealiasrules):
1288 class _aliasrules(parser.basealiasrules):
1292 """Parsing and expansion rule set of template aliases"""
1289 """Parsing and expansion rule set of template aliases"""
1293 _section = _('template alias')
1290 _section = _('template alias')
1294 _parse = staticmethod(_parseexpr)
1291 _parse = staticmethod(_parseexpr)
1295
1292
1296 @staticmethod
1293 @staticmethod
1297 def _trygetfunc(tree):
1294 def _trygetfunc(tree):
1298 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1295 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1299 None"""
1296 None"""
1300 if tree[0] == 'func' and tree[1][0] == 'symbol':
1297 if tree[0] == 'func' and tree[1][0] == 'symbol':
1301 return tree[1][1], getlist(tree[2])
1298 return tree[1][1], getlist(tree[2])
1302 if tree[0] == '|' and tree[2][0] == 'symbol':
1299 if tree[0] == '|' and tree[2][0] == 'symbol':
1303 return tree[2][1], [tree[1]]
1300 return tree[2][1], [tree[1]]
1304
1301
1305 def expandaliases(tree, aliases):
1302 def expandaliases(tree, aliases):
1306 """Return new tree of aliases are expanded"""
1303 """Return new tree of aliases are expanded"""
1307 aliasmap = _aliasrules.buildmap(aliases)
1304 aliasmap = _aliasrules.buildmap(aliases)
1308 return _aliasrules.expand(aliasmap, tree)
1305 return _aliasrules.expand(aliasmap, tree)
1309
1306
1310 # template engine
1307 # template engine
1311
1308
1312 stringify = templatefilters.stringify
1309 stringify = templatefilters.stringify
1313
1310
1314 def _flatten(thing):
1311 def _flatten(thing):
1315 '''yield a single stream from a possibly nested set of iterators'''
1312 '''yield a single stream from a possibly nested set of iterators'''
1316 thing = templatekw.unwraphybrid(thing)
1313 thing = templatekw.unwraphybrid(thing)
1317 if isinstance(thing, bytes):
1314 if isinstance(thing, bytes):
1318 yield thing
1315 yield thing
1319 elif isinstance(thing, str):
1316 elif isinstance(thing, str):
1320 # We can only hit this on Python 3, and it's here to guard
1317 # We can only hit this on Python 3, and it's here to guard
1321 # against infinite recursion.
1318 # against infinite recursion.
1322 raise error.ProgrammingError('Mercurial IO including templates is done'
1319 raise error.ProgrammingError('Mercurial IO including templates is done'
1323 ' with bytes, not strings')
1320 ' with bytes, not strings')
1324 elif thing is None:
1321 elif thing is None:
1325 pass
1322 pass
1326 elif not util.safehasattr(thing, '__iter__'):
1323 elif not util.safehasattr(thing, '__iter__'):
1327 yield pycompat.bytestr(thing)
1324 yield pycompat.bytestr(thing)
1328 else:
1325 else:
1329 for i in thing:
1326 for i in thing:
1330 i = templatekw.unwraphybrid(i)
1327 i = templatekw.unwraphybrid(i)
1331 if isinstance(i, bytes):
1328 if isinstance(i, bytes):
1332 yield i
1329 yield i
1333 elif i is None:
1330 elif i is None:
1334 pass
1331 pass
1335 elif not util.safehasattr(i, '__iter__'):
1332 elif not util.safehasattr(i, '__iter__'):
1336 yield pycompat.bytestr(i)
1333 yield pycompat.bytestr(i)
1337 else:
1334 else:
1338 for j in _flatten(i):
1335 for j in _flatten(i):
1339 yield j
1336 yield j
1340
1337
1341 def unquotestring(s):
1338 def unquotestring(s):
1342 '''unwrap quotes if any; otherwise returns unmodified string'''
1339 '''unwrap quotes if any; otherwise returns unmodified string'''
1343 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1340 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1344 return s
1341 return s
1345 return s[1:-1]
1342 return s[1:-1]
1346
1343
1347 class engine(object):
1344 class engine(object):
1348 '''template expansion engine.
1345 '''template expansion engine.
1349
1346
1350 template expansion works like this. a map file contains key=value
1347 template expansion works like this. a map file contains key=value
1351 pairs. if value is quoted, it is treated as string. otherwise, it
1348 pairs. if value is quoted, it is treated as string. otherwise, it
1352 is treated as name of template file.
1349 is treated as name of template file.
1353
1350
1354 templater is asked to expand a key in map. it looks up key, and
1351 templater is asked to expand a key in map. it looks up key, and
1355 looks for strings like this: {foo}. it expands {foo} by looking up
1352 looks for strings like this: {foo}. it expands {foo} by looking up
1356 foo in map, and substituting it. expansion is recursive: it stops
1353 foo in map, and substituting it. expansion is recursive: it stops
1357 when there is no more {foo} to replace.
1354 when there is no more {foo} to replace.
1358
1355
1359 expansion also allows formatting and filtering.
1356 expansion also allows formatting and filtering.
1360
1357
1361 format uses key to expand each item in list. syntax is
1358 format uses key to expand each item in list. syntax is
1362 {key%format}.
1359 {key%format}.
1363
1360
1364 filter uses function to transform value. syntax is
1361 filter uses function to transform value. syntax is
1365 {key|filter1|filter2|...}.'''
1362 {key|filter1|filter2|...}.'''
1366
1363
1367 def __init__(self, loader, filters=None, defaults=None, resources=None,
1364 def __init__(self, loader, filters=None, defaults=None, resources=None,
1368 aliases=()):
1365 aliases=()):
1369 self._loader = loader
1366 self._loader = loader
1370 if filters is None:
1367 if filters is None:
1371 filters = {}
1368 filters = {}
1372 self._filters = filters
1369 self._filters = filters
1373 if defaults is None:
1370 if defaults is None:
1374 defaults = {}
1371 defaults = {}
1375 if resources is None:
1372 if resources is None:
1376 resources = {}
1373 resources = {}
1377 self._defaults = defaults
1374 self._defaults = defaults
1378 self._resources = resources
1375 self._resources = resources
1379 self._aliasmap = _aliasrules.buildmap(aliases)
1376 self._aliasmap = _aliasrules.buildmap(aliases)
1380 self._cache = {} # key: (func, data)
1377 self._cache = {} # key: (func, data)
1381
1378
1382 def symbol(self, mapping, key):
1379 def symbol(self, mapping, key):
1383 """Resolve symbol to value or function; None if nothing found"""
1380 """Resolve symbol to value or function; None if nothing found"""
1384 v = None
1381 v = None
1385 if key not in self._resources:
1382 if key not in self._resources:
1386 v = mapping.get(key)
1383 v = mapping.get(key)
1387 if v is None:
1384 if v is None:
1388 v = self._defaults.get(key)
1385 v = self._defaults.get(key)
1389 return v
1386 return v
1390
1387
1391 def resource(self, mapping, key):
1388 def resource(self, mapping, key):
1392 """Return internal data (e.g. cache) used for keyword/function
1389 """Return internal data (e.g. cache) used for keyword/function
1393 evaluation"""
1390 evaluation"""
1394 v = None
1391 v = None
1395 if key in self._resources:
1392 if key in self._resources:
1396 v = mapping.get(key)
1393 v = mapping.get(key)
1397 if v is None:
1394 if v is None:
1398 v = self._resources.get(key)
1395 v = self._resources.get(key)
1399 if v is None:
1396 if v is None:
1400 raise ResourceUnavailable(_('template resource not available: %s')
1397 raise ResourceUnavailable(_('template resource not available: %s')
1401 % key)
1398 % key)
1402 return v
1399 return v
1403
1400
1404 def _load(self, t):
1401 def _load(self, t):
1405 '''load, parse, and cache a template'''
1402 '''load, parse, and cache a template'''
1406 if t not in self._cache:
1403 if t not in self._cache:
1407 # put poison to cut recursion while compiling 't'
1404 # put poison to cut recursion while compiling 't'
1408 self._cache[t] = (_runrecursivesymbol, t)
1405 self._cache[t] = (_runrecursivesymbol, t)
1409 try:
1406 try:
1410 x = parse(self._loader(t))
1407 x = parse(self._loader(t))
1411 if self._aliasmap:
1408 if self._aliasmap:
1412 x = _aliasrules.expand(self._aliasmap, x)
1409 x = _aliasrules.expand(self._aliasmap, x)
1413 self._cache[t] = compileexp(x, self, methods)
1410 self._cache[t] = compileexp(x, self, methods)
1414 except: # re-raises
1411 except: # re-raises
1415 del self._cache[t]
1412 del self._cache[t]
1416 raise
1413 raise
1417 return self._cache[t]
1414 return self._cache[t]
1418
1415
1419 def process(self, t, mapping):
1416 def process(self, t, mapping):
1420 '''Perform expansion. t is name of map element to expand.
1417 '''Perform expansion. t is name of map element to expand.
1421 mapping contains added elements for use during expansion. Is a
1418 mapping contains added elements for use during expansion. Is a
1422 generator.'''
1419 generator.'''
1423 func, data = self._load(t)
1420 func, data = self._load(t)
1424 return _flatten(func(self, mapping, data))
1421 return _flatten(func(self, mapping, data))
1425
1422
1426 engines = {'default': engine}
1423 engines = {'default': engine}
1427
1424
1428 def stylelist():
1425 def stylelist():
1429 paths = templatepaths()
1426 paths = templatepaths()
1430 if not paths:
1427 if not paths:
1431 return _('no templates found, try `hg debuginstall` for more info')
1428 return _('no templates found, try `hg debuginstall` for more info')
1432 dirlist = os.listdir(paths[0])
1429 dirlist = os.listdir(paths[0])
1433 stylelist = []
1430 stylelist = []
1434 for file in dirlist:
1431 for file in dirlist:
1435 split = file.split(".")
1432 split = file.split(".")
1436 if split[-1] in ('orig', 'rej'):
1433 if split[-1] in ('orig', 'rej'):
1437 continue
1434 continue
1438 if split[0] == "map-cmdline":
1435 if split[0] == "map-cmdline":
1439 stylelist.append(split[1])
1436 stylelist.append(split[1])
1440 return ", ".join(sorted(stylelist))
1437 return ", ".join(sorted(stylelist))
1441
1438
1442 def _readmapfile(mapfile):
1439 def _readmapfile(mapfile):
1443 """Load template elements from the given map file"""
1440 """Load template elements from the given map file"""
1444 if not os.path.exists(mapfile):
1441 if not os.path.exists(mapfile):
1445 raise error.Abort(_("style '%s' not found") % mapfile,
1442 raise error.Abort(_("style '%s' not found") % mapfile,
1446 hint=_("available styles: %s") % stylelist())
1443 hint=_("available styles: %s") % stylelist())
1447
1444
1448 base = os.path.dirname(mapfile)
1445 base = os.path.dirname(mapfile)
1449 conf = config.config(includepaths=templatepaths())
1446 conf = config.config(includepaths=templatepaths())
1450 conf.read(mapfile, remap={'': 'templates'})
1447 conf.read(mapfile, remap={'': 'templates'})
1451
1448
1452 cache = {}
1449 cache = {}
1453 tmap = {}
1450 tmap = {}
1454 aliases = []
1451 aliases = []
1455
1452
1456 val = conf.get('templates', '__base__')
1453 val = conf.get('templates', '__base__')
1457 if val and val[0] not in "'\"":
1454 if val and val[0] not in "'\"":
1458 # treat as a pointer to a base class for this style
1455 # treat as a pointer to a base class for this style
1459 path = util.normpath(os.path.join(base, val))
1456 path = util.normpath(os.path.join(base, val))
1460
1457
1461 # fallback check in template paths
1458 # fallback check in template paths
1462 if not os.path.exists(path):
1459 if not os.path.exists(path):
1463 for p in templatepaths():
1460 for p in templatepaths():
1464 p2 = util.normpath(os.path.join(p, val))
1461 p2 = util.normpath(os.path.join(p, val))
1465 if os.path.isfile(p2):
1462 if os.path.isfile(p2):
1466 path = p2
1463 path = p2
1467 break
1464 break
1468 p3 = util.normpath(os.path.join(p2, "map"))
1465 p3 = util.normpath(os.path.join(p2, "map"))
1469 if os.path.isfile(p3):
1466 if os.path.isfile(p3):
1470 path = p3
1467 path = p3
1471 break
1468 break
1472
1469
1473 cache, tmap, aliases = _readmapfile(path)
1470 cache, tmap, aliases = _readmapfile(path)
1474
1471
1475 for key, val in conf['templates'].items():
1472 for key, val in conf['templates'].items():
1476 if not val:
1473 if not val:
1477 raise error.ParseError(_('missing value'),
1474 raise error.ParseError(_('missing value'),
1478 conf.source('templates', key))
1475 conf.source('templates', key))
1479 if val[0] in "'\"":
1476 if val[0] in "'\"":
1480 if val[0] != val[-1]:
1477 if val[0] != val[-1]:
1481 raise error.ParseError(_('unmatched quotes'),
1478 raise error.ParseError(_('unmatched quotes'),
1482 conf.source('templates', key))
1479 conf.source('templates', key))
1483 cache[key] = unquotestring(val)
1480 cache[key] = unquotestring(val)
1484 elif key != '__base__':
1481 elif key != '__base__':
1485 val = 'default', val
1482 val = 'default', val
1486 if ':' in val[1]:
1483 if ':' in val[1]:
1487 val = val[1].split(':', 1)
1484 val = val[1].split(':', 1)
1488 tmap[key] = val[0], os.path.join(base, val[1])
1485 tmap[key] = val[0], os.path.join(base, val[1])
1489 aliases.extend(conf['templatealias'].items())
1486 aliases.extend(conf['templatealias'].items())
1490 return cache, tmap, aliases
1487 return cache, tmap, aliases
1491
1488
1492 class templater(object):
1489 class templater(object):
1493
1490
1494 def __init__(self, filters=None, defaults=None, resources=None,
1491 def __init__(self, filters=None, defaults=None, resources=None,
1495 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
1492 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
1496 """Create template engine optionally with preloaded template fragments
1493 """Create template engine optionally with preloaded template fragments
1497
1494
1498 - ``filters``: a dict of functions to transform a value into another.
1495 - ``filters``: a dict of functions to transform a value into another.
1499 - ``defaults``: a dict of symbol values/functions; may be overridden
1496 - ``defaults``: a dict of symbol values/functions; may be overridden
1500 by a ``mapping`` dict.
1497 by a ``mapping`` dict.
1501 - ``resources``: a dict of internal data (e.g. cache), inaccessible
1498 - ``resources``: a dict of internal data (e.g. cache), inaccessible
1502 from user template; may be overridden by a ``mapping`` dict.
1499 from user template; may be overridden by a ``mapping`` dict.
1503 - ``cache``: a dict of preloaded template fragments.
1500 - ``cache``: a dict of preloaded template fragments.
1504 - ``aliases``: a list of alias (name, replacement) pairs.
1501 - ``aliases``: a list of alias (name, replacement) pairs.
1505
1502
1506 self.cache may be updated later to register additional template
1503 self.cache may be updated later to register additional template
1507 fragments.
1504 fragments.
1508 """
1505 """
1509 if filters is None:
1506 if filters is None:
1510 filters = {}
1507 filters = {}
1511 if defaults is None:
1508 if defaults is None:
1512 defaults = {}
1509 defaults = {}
1513 if resources is None:
1510 if resources is None:
1514 resources = {}
1511 resources = {}
1515 if cache is None:
1512 if cache is None:
1516 cache = {}
1513 cache = {}
1517 self.cache = cache.copy()
1514 self.cache = cache.copy()
1518 self.map = {}
1515 self.map = {}
1519 self.filters = templatefilters.filters.copy()
1516 self.filters = templatefilters.filters.copy()
1520 self.filters.update(filters)
1517 self.filters.update(filters)
1521 self.defaults = defaults
1518 self.defaults = defaults
1522 self._resources = {'templ': self}
1519 self._resources = {'templ': self}
1523 self._resources.update(resources)
1520 self._resources.update(resources)
1524 self._aliases = aliases
1521 self._aliases = aliases
1525 self.minchunk, self.maxchunk = minchunk, maxchunk
1522 self.minchunk, self.maxchunk = minchunk, maxchunk
1526 self.ecache = {}
1523 self.ecache = {}
1527
1524
1528 @classmethod
1525 @classmethod
1529 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
1526 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
1530 cache=None, minchunk=1024, maxchunk=65536):
1527 cache=None, minchunk=1024, maxchunk=65536):
1531 """Create templater from the specified map file"""
1528 """Create templater from the specified map file"""
1532 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1529 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
1533 cache, tmap, aliases = _readmapfile(mapfile)
1530 cache, tmap, aliases = _readmapfile(mapfile)
1534 t.cache.update(cache)
1531 t.cache.update(cache)
1535 t.map = tmap
1532 t.map = tmap
1536 t._aliases = aliases
1533 t._aliases = aliases
1537 return t
1534 return t
1538
1535
1539 def __contains__(self, key):
1536 def __contains__(self, key):
1540 return key in self.cache or key in self.map
1537 return key in self.cache or key in self.map
1541
1538
1542 def load(self, t):
1539 def load(self, t):
1543 '''Get the template for the given template name. Use a local cache.'''
1540 '''Get the template for the given template name. Use a local cache.'''
1544 if t not in self.cache:
1541 if t not in self.cache:
1545 try:
1542 try:
1546 self.cache[t] = util.readfile(self.map[t][1])
1543 self.cache[t] = util.readfile(self.map[t][1])
1547 except KeyError as inst:
1544 except KeyError as inst:
1548 raise TemplateNotFound(_('"%s" not in template map') %
1545 raise TemplateNotFound(_('"%s" not in template map') %
1549 inst.args[0])
1546 inst.args[0])
1550 except IOError as inst:
1547 except IOError as inst:
1551 reason = (_('template file %s: %s')
1548 reason = (_('template file %s: %s')
1552 % (self.map[t][1], util.forcebytestr(inst.args[1])))
1549 % (self.map[t][1], util.forcebytestr(inst.args[1])))
1553 raise IOError(inst.args[0], encoding.strfromlocal(reason))
1550 raise IOError(inst.args[0], encoding.strfromlocal(reason))
1554 return self.cache[t]
1551 return self.cache[t]
1555
1552
1556 def render(self, mapping):
1553 def render(self, mapping):
1557 """Render the default unnamed template and return result as string"""
1554 """Render the default unnamed template and return result as string"""
1558 mapping = pycompat.strkwargs(mapping)
1555 mapping = pycompat.strkwargs(mapping)
1559 return stringify(self('', **mapping))
1556 return stringify(self('', **mapping))
1560
1557
1561 def __call__(self, t, **mapping):
1558 def __call__(self, t, **mapping):
1562 mapping = pycompat.byteskwargs(mapping)
1559 mapping = pycompat.byteskwargs(mapping)
1563 ttype = t in self.map and self.map[t][0] or 'default'
1560 ttype = t in self.map and self.map[t][0] or 'default'
1564 if ttype not in self.ecache:
1561 if ttype not in self.ecache:
1565 try:
1562 try:
1566 ecls = engines[ttype]
1563 ecls = engines[ttype]
1567 except KeyError:
1564 except KeyError:
1568 raise error.Abort(_('invalid template engine: %s') % ttype)
1565 raise error.Abort(_('invalid template engine: %s') % ttype)
1569 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1566 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1570 self._resources, self._aliases)
1567 self._resources, self._aliases)
1571 proc = self.ecache[ttype]
1568 proc = self.ecache[ttype]
1572
1569
1573 stream = proc.process(t, mapping)
1570 stream = proc.process(t, mapping)
1574 if self.minchunk:
1571 if self.minchunk:
1575 stream = util.increasingchunks(stream, min=self.minchunk,
1572 stream = util.increasingchunks(stream, min=self.minchunk,
1576 max=self.maxchunk)
1573 max=self.maxchunk)
1577 return stream
1574 return stream
1578
1575
1579 def templatepaths():
1576 def templatepaths():
1580 '''return locations used for template files.'''
1577 '''return locations used for template files.'''
1581 pathsrel = ['templates']
1578 pathsrel = ['templates']
1582 paths = [os.path.normpath(os.path.join(util.datapath, f))
1579 paths = [os.path.normpath(os.path.join(util.datapath, f))
1583 for f in pathsrel]
1580 for f in pathsrel]
1584 return [p for p in paths if os.path.isdir(p)]
1581 return [p for p in paths if os.path.isdir(p)]
1585
1582
1586 def templatepath(name):
1583 def templatepath(name):
1587 '''return location of template file. returns None if not found.'''
1584 '''return location of template file. returns None if not found.'''
1588 for p in templatepaths():
1585 for p in templatepaths():
1589 f = os.path.join(p, name)
1586 f = os.path.join(p, name)
1590 if os.path.exists(f):
1587 if os.path.exists(f):
1591 return f
1588 return f
1592 return None
1589 return None
1593
1590
1594 def stylemap(styles, paths=None):
1591 def stylemap(styles, paths=None):
1595 """Return path to mapfile for a given style.
1592 """Return path to mapfile for a given style.
1596
1593
1597 Searches mapfile in the following locations:
1594 Searches mapfile in the following locations:
1598 1. templatepath/style/map
1595 1. templatepath/style/map
1599 2. templatepath/map-style
1596 2. templatepath/map-style
1600 3. templatepath/map
1597 3. templatepath/map
1601 """
1598 """
1602
1599
1603 if paths is None:
1600 if paths is None:
1604 paths = templatepaths()
1601 paths = templatepaths()
1605 elif isinstance(paths, bytes):
1602 elif isinstance(paths, bytes):
1606 paths = [paths]
1603 paths = [paths]
1607
1604
1608 if isinstance(styles, bytes):
1605 if isinstance(styles, bytes):
1609 styles = [styles]
1606 styles = [styles]
1610
1607
1611 for style in styles:
1608 for style in styles:
1612 # only plain name is allowed to honor template paths
1609 # only plain name is allowed to honor template paths
1613 if (not style
1610 if (not style
1614 or style in (os.curdir, os.pardir)
1611 or style in (os.curdir, os.pardir)
1615 or pycompat.ossep in style
1612 or pycompat.ossep in style
1616 or pycompat.osaltsep and pycompat.osaltsep in style):
1613 or pycompat.osaltsep and pycompat.osaltsep in style):
1617 continue
1614 continue
1618 locations = [os.path.join(style, 'map'), 'map-' + style]
1615 locations = [os.path.join(style, 'map'), 'map-' + style]
1619 locations.append('map')
1616 locations.append('map')
1620
1617
1621 for path in paths:
1618 for path in paths:
1622 for location in locations:
1619 for location in locations:
1623 mapfile = os.path.join(path, location)
1620 mapfile = os.path.join(path, location)
1624 if os.path.isfile(mapfile):
1621 if os.path.isfile(mapfile):
1625 return style, mapfile
1622 return style, mapfile
1626
1623
1627 raise RuntimeError("No hgweb templates found in %r" % paths)
1624 raise RuntimeError("No hgweb templates found in %r" % paths)
1628
1625
1629 def loadfunction(ui, extname, registrarobj):
1626 def loadfunction(ui, extname, registrarobj):
1630 """Load template function from specified registrarobj
1627 """Load template function from specified registrarobj
1631 """
1628 """
1632 for name, func in registrarobj._table.iteritems():
1629 for name, func in registrarobj._table.iteritems():
1633 funcs[name] = func
1630 funcs[name] = func
1634
1631
1635 # tell hggettext to extract docstrings from these functions:
1632 # tell hggettext to extract docstrings from these functions:
1636 i18nfunctions = funcs.values()
1633 i18nfunctions = funcs.values()
General Comments 0
You need to be logged in to leave comments. Login now