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