##// END OF EJS Templates
templater: allow dynamically switching the default dict/list formatting...
Yuya Nishihara -
r36651:034a07e6 default
parent child Browse files
Show More
@@ -1,543 +1,547 b''
1 # formatter.py - generic output formatting for mercurial
1 # formatter.py - generic output formatting for mercurial
2 #
2 #
3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """Generic output formatting for Mercurial
8 """Generic output formatting for Mercurial
9
9
10 The formatter provides API to show data in various ways. The following
10 The formatter provides API to show data in various ways. The following
11 functions should be used in place of ui.write():
11 functions should be used in place of ui.write():
12
12
13 - fm.write() for unconditional output
13 - fm.write() for unconditional output
14 - fm.condwrite() to show some extra data conditionally in plain output
14 - fm.condwrite() to show some extra data conditionally in plain output
15 - fm.context() to provide changectx to template output
15 - fm.context() to provide changectx to template output
16 - fm.data() to provide extra data to JSON or template output
16 - fm.data() to provide extra data to JSON or template output
17 - fm.plain() to show raw text that isn't provided to JSON or template output
17 - fm.plain() to show raw text that isn't provided to JSON or template output
18
18
19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 beforehand so the data is converted to the appropriate data type. Use
20 beforehand so the data is converted to the appropriate data type. Use
21 fm.isplain() if you need to convert or format data conditionally which isn't
21 fm.isplain() if you need to convert or format data conditionally which isn't
22 supported by the formatter API.
22 supported by the formatter API.
23
23
24 To build nested structure (i.e. a list of dicts), use fm.nested().
24 To build nested structure (i.e. a list of dicts), use fm.nested().
25
25
26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27
27
28 fm.condwrite() vs 'if cond:':
28 fm.condwrite() vs 'if cond:':
29
29
30 In most cases, use fm.condwrite() so users can selectively show the data
30 In most cases, use fm.condwrite() so users can selectively show the data
31 in template output. If it's costly to build data, use plain 'if cond:' with
31 in template output. If it's costly to build data, use plain 'if cond:' with
32 fm.write().
32 fm.write().
33
33
34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35
35
36 fm.nested() should be used to form a tree structure (a list of dicts of
36 fm.nested() should be used to form a tree structure (a list of dicts of
37 lists of dicts...) which can be accessed through template keywords, e.g.
37 lists of dicts...) which can be accessed through template keywords, e.g.
38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 exports a dict-type object to template, which can be accessed by e.g.
39 exports a dict-type object to template, which can be accessed by e.g.
40 "{get(foo, key)}" function.
40 "{get(foo, key)}" function.
41
41
42 Doctest helper:
42 Doctest helper:
43
43
44 >>> def show(fn, verbose=False, **opts):
44 >>> def show(fn, verbose=False, **opts):
45 ... import sys
45 ... import sys
46 ... from . import ui as uimod
46 ... from . import ui as uimod
47 ... ui = uimod.ui()
47 ... ui = uimod.ui()
48 ... ui.verbose = verbose
48 ... ui.verbose = verbose
49 ... ui.pushbuffer()
49 ... ui.pushbuffer()
50 ... try:
50 ... try:
51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
52 ... pycompat.byteskwargs(opts)))
52 ... pycompat.byteskwargs(opts)))
53 ... finally:
53 ... finally:
54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
55
55
56 Basic example:
56 Basic example:
57
57
58 >>> def files(ui, fm):
58 >>> def files(ui, fm):
59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
60 ... for f in files:
60 ... for f in files:
61 ... fm.startitem()
61 ... fm.startitem()
62 ... fm.write(b'path', b'%s', f[0])
62 ... fm.write(b'path', b'%s', f[0])
63 ... fm.condwrite(ui.verbose, b'date', b' %s',
63 ... fm.condwrite(ui.verbose, b'date', b' %s',
64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
65 ... fm.data(size=f[1])
65 ... fm.data(size=f[1])
66 ... fm.plain(b'\\n')
66 ... fm.plain(b'\\n')
67 ... fm.end()
67 ... fm.end()
68 >>> show(files)
68 >>> show(files)
69 foo
69 foo
70 bar
70 bar
71 >>> show(files, verbose=True)
71 >>> show(files, verbose=True)
72 foo 1970-01-01 00:00:00
72 foo 1970-01-01 00:00:00
73 bar 1970-01-01 00:00:01
73 bar 1970-01-01 00:00:01
74 >>> show(files, template=b'json')
74 >>> show(files, template=b'json')
75 [
75 [
76 {
76 {
77 "date": [0, 0],
77 "date": [0, 0],
78 "path": "foo",
78 "path": "foo",
79 "size": 123
79 "size": 123
80 },
80 },
81 {
81 {
82 "date": [1, 0],
82 "date": [1, 0],
83 "path": "bar",
83 "path": "bar",
84 "size": 456
84 "size": 456
85 }
85 }
86 ]
86 ]
87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
88 path: foo
88 path: foo
89 date: 1970-01-01T00:00:00+00:00
89 date: 1970-01-01T00:00:00+00:00
90 path: bar
90 path: bar
91 date: 1970-01-01T00:00:01+00:00
91 date: 1970-01-01T00:00:01+00:00
92
92
93 Nested example:
93 Nested example:
94
94
95 >>> def subrepos(ui, fm):
95 >>> def subrepos(ui, fm):
96 ... fm.startitem()
96 ... fm.startitem()
97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
98 ... files(ui, fm.nested(b'files'))
98 ... files(ui, fm.nested(b'files'))
99 ... fm.end()
99 ... fm.end()
100 >>> show(subrepos)
100 >>> show(subrepos)
101 [baz]
101 [baz]
102 foo
102 foo
103 bar
103 bar
104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
104 >>> show(subrepos, template=b'{reponame}: {join(files % "{path}", ", ")}\\n')
105 baz: foo, bar
105 baz: foo, bar
106 """
106 """
107
107
108 from __future__ import absolute_import, print_function
108 from __future__ import absolute_import, print_function
109
109
110 import collections
110 import collections
111 import contextlib
111 import contextlib
112 import itertools
112 import itertools
113 import os
113 import os
114
114
115 from .i18n import _
115 from .i18n import _
116 from .node import (
116 from .node import (
117 hex,
117 hex,
118 short,
118 short,
119 )
119 )
120
120
121 from . import (
121 from . import (
122 error,
122 error,
123 pycompat,
123 pycompat,
124 templatefilters,
124 templatefilters,
125 templatekw,
125 templatekw,
126 templater,
126 templater,
127 util,
127 util,
128 )
128 )
129 from .utils import dateutil
129 from .utils import dateutil
130
130
131 pickle = util.pickle
131 pickle = util.pickle
132
132
133 class _nullconverter(object):
133 class _nullconverter(object):
134 '''convert non-primitive data types to be processed by formatter'''
134 '''convert non-primitive data types to be processed by formatter'''
135
135
136 # set to True if context object should be stored as item
136 # set to True if context object should be stored as item
137 storecontext = False
137 storecontext = False
138
138
139 @staticmethod
139 @staticmethod
140 def formatdate(date, fmt):
140 def formatdate(date, fmt):
141 '''convert date tuple to appropriate format'''
141 '''convert date tuple to appropriate format'''
142 return date
142 return date
143 @staticmethod
143 @staticmethod
144 def formatdict(data, key, value, fmt, sep):
144 def formatdict(data, key, value, fmt, sep):
145 '''convert dict or key-value pairs to appropriate dict format'''
145 '''convert dict or key-value pairs to appropriate dict format'''
146 # use plain dict instead of util.sortdict so that data can be
146 # use plain dict instead of util.sortdict so that data can be
147 # serialized as a builtin dict in pickle output
147 # serialized as a builtin dict in pickle output
148 return dict(data)
148 return dict(data)
149 @staticmethod
149 @staticmethod
150 def formatlist(data, name, fmt, sep):
150 def formatlist(data, name, fmt, sep):
151 '''convert iterable to appropriate list format'''
151 '''convert iterable to appropriate list format'''
152 return list(data)
152 return list(data)
153
153
154 class baseformatter(object):
154 class baseformatter(object):
155 def __init__(self, ui, topic, opts, converter):
155 def __init__(self, ui, topic, opts, converter):
156 self._ui = ui
156 self._ui = ui
157 self._topic = topic
157 self._topic = topic
158 self._style = opts.get("style")
158 self._style = opts.get("style")
159 self._template = opts.get("template")
159 self._template = opts.get("template")
160 self._converter = converter
160 self._converter = converter
161 self._item = None
161 self._item = None
162 # function to convert node to string suitable for this output
162 # function to convert node to string suitable for this output
163 self.hexfunc = hex
163 self.hexfunc = hex
164 def __enter__(self):
164 def __enter__(self):
165 return self
165 return self
166 def __exit__(self, exctype, excvalue, traceback):
166 def __exit__(self, exctype, excvalue, traceback):
167 if exctype is None:
167 if exctype is None:
168 self.end()
168 self.end()
169 def _showitem(self):
169 def _showitem(self):
170 '''show a formatted item once all data is collected'''
170 '''show a formatted item once all data is collected'''
171 def startitem(self):
171 def startitem(self):
172 '''begin an item in the format list'''
172 '''begin an item in the format list'''
173 if self._item is not None:
173 if self._item is not None:
174 self._showitem()
174 self._showitem()
175 self._item = {}
175 self._item = {}
176 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
176 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
177 '''convert date tuple to appropriate format'''
177 '''convert date tuple to appropriate format'''
178 return self._converter.formatdate(date, fmt)
178 return self._converter.formatdate(date, fmt)
179 def formatdict(self, data, key='key', value='value', fmt='%s=%s', sep=' '):
179 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
180 '''convert dict or key-value pairs to appropriate dict format'''
180 '''convert dict or key-value pairs to appropriate dict format'''
181 return self._converter.formatdict(data, key, value, fmt, sep)
181 return self._converter.formatdict(data, key, value, fmt, sep)
182 def formatlist(self, data, name, fmt='%s', sep=' '):
182 def formatlist(self, data, name, fmt=None, sep=' '):
183 '''convert iterable to appropriate list format'''
183 '''convert iterable to appropriate list format'''
184 # name is mandatory argument for now, but it could be optional if
184 # name is mandatory argument for now, but it could be optional if
185 # we have default template keyword, e.g. {item}
185 # we have default template keyword, e.g. {item}
186 return self._converter.formatlist(data, name, fmt, sep)
186 return self._converter.formatlist(data, name, fmt, sep)
187 def context(self, **ctxs):
187 def context(self, **ctxs):
188 '''insert context objects to be used to render template keywords'''
188 '''insert context objects to be used to render template keywords'''
189 ctxs = pycompat.byteskwargs(ctxs)
189 ctxs = pycompat.byteskwargs(ctxs)
190 assert all(k == 'ctx' for k in ctxs)
190 assert all(k == 'ctx' for k in ctxs)
191 if self._converter.storecontext:
191 if self._converter.storecontext:
192 self._item.update(ctxs)
192 self._item.update(ctxs)
193 def data(self, **data):
193 def data(self, **data):
194 '''insert data into item that's not shown in default output'''
194 '''insert data into item that's not shown in default output'''
195 data = pycompat.byteskwargs(data)
195 data = pycompat.byteskwargs(data)
196 self._item.update(data)
196 self._item.update(data)
197 def write(self, fields, deftext, *fielddata, **opts):
197 def write(self, fields, deftext, *fielddata, **opts):
198 '''do default text output while assigning data to item'''
198 '''do default text output while assigning data to item'''
199 fieldkeys = fields.split()
199 fieldkeys = fields.split()
200 assert len(fieldkeys) == len(fielddata)
200 assert len(fieldkeys) == len(fielddata)
201 self._item.update(zip(fieldkeys, fielddata))
201 self._item.update(zip(fieldkeys, fielddata))
202 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
202 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
203 '''do conditional write (primarily for plain formatter)'''
203 '''do conditional write (primarily for plain formatter)'''
204 fieldkeys = fields.split()
204 fieldkeys = fields.split()
205 assert len(fieldkeys) == len(fielddata)
205 assert len(fieldkeys) == len(fielddata)
206 self._item.update(zip(fieldkeys, fielddata))
206 self._item.update(zip(fieldkeys, fielddata))
207 def plain(self, text, **opts):
207 def plain(self, text, **opts):
208 '''show raw text for non-templated mode'''
208 '''show raw text for non-templated mode'''
209 def isplain(self):
209 def isplain(self):
210 '''check for plain formatter usage'''
210 '''check for plain formatter usage'''
211 return False
211 return False
212 def nested(self, field):
212 def nested(self, field):
213 '''sub formatter to store nested data in the specified field'''
213 '''sub formatter to store nested data in the specified field'''
214 self._item[field] = data = []
214 self._item[field] = data = []
215 return _nestedformatter(self._ui, self._converter, data)
215 return _nestedformatter(self._ui, self._converter, data)
216 def end(self):
216 def end(self):
217 '''end output for the formatter'''
217 '''end output for the formatter'''
218 if self._item is not None:
218 if self._item is not None:
219 self._showitem()
219 self._showitem()
220
220
221 def nullformatter(ui, topic):
221 def nullformatter(ui, topic):
222 '''formatter that prints nothing'''
222 '''formatter that prints nothing'''
223 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
223 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
224
224
225 class _nestedformatter(baseformatter):
225 class _nestedformatter(baseformatter):
226 '''build sub items and store them in the parent formatter'''
226 '''build sub items and store them in the parent formatter'''
227 def __init__(self, ui, converter, data):
227 def __init__(self, ui, converter, data):
228 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
228 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
229 self._data = data
229 self._data = data
230 def _showitem(self):
230 def _showitem(self):
231 self._data.append(self._item)
231 self._data.append(self._item)
232
232
233 def _iteritems(data):
233 def _iteritems(data):
234 '''iterate key-value pairs in stable order'''
234 '''iterate key-value pairs in stable order'''
235 if isinstance(data, dict):
235 if isinstance(data, dict):
236 return sorted(data.iteritems())
236 return sorted(data.iteritems())
237 return data
237 return data
238
238
239 class _plainconverter(object):
239 class _plainconverter(object):
240 '''convert non-primitive data types to text'''
240 '''convert non-primitive data types to text'''
241
241
242 storecontext = False
242 storecontext = False
243
243
244 @staticmethod
244 @staticmethod
245 def formatdate(date, fmt):
245 def formatdate(date, fmt):
246 '''stringify date tuple in the given format'''
246 '''stringify date tuple in the given format'''
247 return dateutil.datestr(date, fmt)
247 return dateutil.datestr(date, fmt)
248 @staticmethod
248 @staticmethod
249 def formatdict(data, key, value, fmt, sep):
249 def formatdict(data, key, value, fmt, sep):
250 '''stringify key-value pairs separated by sep'''
250 '''stringify key-value pairs separated by sep'''
251 if fmt is None:
252 fmt = '%s=%s'
251 return sep.join(fmt % (k, v) for k, v in _iteritems(data))
253 return sep.join(fmt % (k, v) for k, v in _iteritems(data))
252 @staticmethod
254 @staticmethod
253 def formatlist(data, name, fmt, sep):
255 def formatlist(data, name, fmt, sep):
254 '''stringify iterable separated by sep'''
256 '''stringify iterable separated by sep'''
257 if fmt is None:
258 fmt = '%s'
255 return sep.join(fmt % e for e in data)
259 return sep.join(fmt % e for e in data)
256
260
257 class plainformatter(baseformatter):
261 class plainformatter(baseformatter):
258 '''the default text output scheme'''
262 '''the default text output scheme'''
259 def __init__(self, ui, out, topic, opts):
263 def __init__(self, ui, out, topic, opts):
260 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
264 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
261 if ui.debugflag:
265 if ui.debugflag:
262 self.hexfunc = hex
266 self.hexfunc = hex
263 else:
267 else:
264 self.hexfunc = short
268 self.hexfunc = short
265 if ui is out:
269 if ui is out:
266 self._write = ui.write
270 self._write = ui.write
267 else:
271 else:
268 self._write = lambda s, **opts: out.write(s)
272 self._write = lambda s, **opts: out.write(s)
269 def startitem(self):
273 def startitem(self):
270 pass
274 pass
271 def data(self, **data):
275 def data(self, **data):
272 pass
276 pass
273 def write(self, fields, deftext, *fielddata, **opts):
277 def write(self, fields, deftext, *fielddata, **opts):
274 self._write(deftext % fielddata, **opts)
278 self._write(deftext % fielddata, **opts)
275 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
279 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
276 '''do conditional write'''
280 '''do conditional write'''
277 if cond:
281 if cond:
278 self._write(deftext % fielddata, **opts)
282 self._write(deftext % fielddata, **opts)
279 def plain(self, text, **opts):
283 def plain(self, text, **opts):
280 self._write(text, **opts)
284 self._write(text, **opts)
281 def isplain(self):
285 def isplain(self):
282 return True
286 return True
283 def nested(self, field):
287 def nested(self, field):
284 # nested data will be directly written to ui
288 # nested data will be directly written to ui
285 return self
289 return self
286 def end(self):
290 def end(self):
287 pass
291 pass
288
292
289 class debugformatter(baseformatter):
293 class debugformatter(baseformatter):
290 def __init__(self, ui, out, topic, opts):
294 def __init__(self, ui, out, topic, opts):
291 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
295 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
292 self._out = out
296 self._out = out
293 self._out.write("%s = [\n" % self._topic)
297 self._out.write("%s = [\n" % self._topic)
294 def _showitem(self):
298 def _showitem(self):
295 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
299 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
296 def end(self):
300 def end(self):
297 baseformatter.end(self)
301 baseformatter.end(self)
298 self._out.write("]\n")
302 self._out.write("]\n")
299
303
300 class pickleformatter(baseformatter):
304 class pickleformatter(baseformatter):
301 def __init__(self, ui, out, topic, opts):
305 def __init__(self, ui, out, topic, opts):
302 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
306 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
303 self._out = out
307 self._out = out
304 self._data = []
308 self._data = []
305 def _showitem(self):
309 def _showitem(self):
306 self._data.append(self._item)
310 self._data.append(self._item)
307 def end(self):
311 def end(self):
308 baseformatter.end(self)
312 baseformatter.end(self)
309 self._out.write(pickle.dumps(self._data))
313 self._out.write(pickle.dumps(self._data))
310
314
311 class jsonformatter(baseformatter):
315 class jsonformatter(baseformatter):
312 def __init__(self, ui, out, topic, opts):
316 def __init__(self, ui, out, topic, opts):
313 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
317 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
314 self._out = out
318 self._out = out
315 self._out.write("[")
319 self._out.write("[")
316 self._first = True
320 self._first = True
317 def _showitem(self):
321 def _showitem(self):
318 if self._first:
322 if self._first:
319 self._first = False
323 self._first = False
320 else:
324 else:
321 self._out.write(",")
325 self._out.write(",")
322
326
323 self._out.write("\n {\n")
327 self._out.write("\n {\n")
324 first = True
328 first = True
325 for k, v in sorted(self._item.items()):
329 for k, v in sorted(self._item.items()):
326 if first:
330 if first:
327 first = False
331 first = False
328 else:
332 else:
329 self._out.write(",\n")
333 self._out.write(",\n")
330 u = templatefilters.json(v, paranoid=False)
334 u = templatefilters.json(v, paranoid=False)
331 self._out.write(' "%s": %s' % (k, u))
335 self._out.write(' "%s": %s' % (k, u))
332 self._out.write("\n }")
336 self._out.write("\n }")
333 def end(self):
337 def end(self):
334 baseformatter.end(self)
338 baseformatter.end(self)
335 self._out.write("\n]\n")
339 self._out.write("\n]\n")
336
340
337 class _templateconverter(object):
341 class _templateconverter(object):
338 '''convert non-primitive data types to be processed by templater'''
342 '''convert non-primitive data types to be processed by templater'''
339
343
340 storecontext = True
344 storecontext = True
341
345
342 @staticmethod
346 @staticmethod
343 def formatdate(date, fmt):
347 def formatdate(date, fmt):
344 '''return date tuple'''
348 '''return date tuple'''
345 return date
349 return date
346 @staticmethod
350 @staticmethod
347 def formatdict(data, key, value, fmt, sep):
351 def formatdict(data, key, value, fmt, sep):
348 '''build object that can be evaluated as either plain string or dict'''
352 '''build object that can be evaluated as either plain string or dict'''
349 data = util.sortdict(_iteritems(data))
353 data = util.sortdict(_iteritems(data))
350 def f():
354 def f():
351 yield _plainconverter.formatdict(data, key, value, fmt, sep)
355 yield _plainconverter.formatdict(data, key, value, fmt, sep)
352 return templatekw.hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
356 return templatekw.hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
353 @staticmethod
357 @staticmethod
354 def formatlist(data, name, fmt, sep):
358 def formatlist(data, name, fmt, sep):
355 '''build object that can be evaluated as either plain string or list'''
359 '''build object that can be evaluated as either plain string or list'''
356 data = list(data)
360 data = list(data)
357 def f():
361 def f():
358 yield _plainconverter.formatlist(data, name, fmt, sep)
362 yield _plainconverter.formatlist(data, name, fmt, sep)
359 return templatekw.hybridlist(data, name=name, fmt=fmt, gen=f)
363 return templatekw.hybridlist(data, name=name, fmt=fmt, gen=f)
360
364
361 class templateformatter(baseformatter):
365 class templateformatter(baseformatter):
362 def __init__(self, ui, out, topic, opts):
366 def __init__(self, ui, out, topic, opts):
363 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
367 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
364 self._out = out
368 self._out = out
365 spec = lookuptemplate(ui, topic, opts.get('template', ''))
369 spec = lookuptemplate(ui, topic, opts.get('template', ''))
366 self._tref = spec.ref
370 self._tref = spec.ref
367 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
371 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
368 resources=templateresources(ui),
372 resources=templateresources(ui),
369 cache=templatekw.defaulttempl)
373 cache=templatekw.defaulttempl)
370 self._parts = templatepartsmap(spec, self._t,
374 self._parts = templatepartsmap(spec, self._t,
371 ['docheader', 'docfooter', 'separator'])
375 ['docheader', 'docfooter', 'separator'])
372 self._counter = itertools.count()
376 self._counter = itertools.count()
373 self._renderitem('docheader', {})
377 self._renderitem('docheader', {})
374
378
375 def _showitem(self):
379 def _showitem(self):
376 item = self._item.copy()
380 item = self._item.copy()
377 item['index'] = index = next(self._counter)
381 item['index'] = index = next(self._counter)
378 if index > 0:
382 if index > 0:
379 self._renderitem('separator', {})
383 self._renderitem('separator', {})
380 self._renderitem(self._tref, item)
384 self._renderitem(self._tref, item)
381
385
382 def _renderitem(self, part, item):
386 def _renderitem(self, part, item):
383 if part not in self._parts:
387 if part not in self._parts:
384 return
388 return
385 ref = self._parts[part]
389 ref = self._parts[part]
386
390
387 # TODO: add support for filectx
391 # TODO: add support for filectx
388 props = {}
392 props = {}
389 # explicitly-defined fields precede templatekw
393 # explicitly-defined fields precede templatekw
390 props.update(item)
394 props.update(item)
391 if 'ctx' in item:
395 if 'ctx' in item:
392 # but template resources must be always available
396 # but template resources must be always available
393 props['repo'] = props['ctx'].repo()
397 props['repo'] = props['ctx'].repo()
394 props['revcache'] = {}
398 props['revcache'] = {}
395 props = pycompat.strkwargs(props)
399 props = pycompat.strkwargs(props)
396 g = self._t(ref, **props)
400 g = self._t(ref, **props)
397 self._out.write(templater.stringify(g))
401 self._out.write(templater.stringify(g))
398
402
399 def end(self):
403 def end(self):
400 baseformatter.end(self)
404 baseformatter.end(self)
401 self._renderitem('docfooter', {})
405 self._renderitem('docfooter', {})
402
406
403 templatespec = collections.namedtuple(r'templatespec',
407 templatespec = collections.namedtuple(r'templatespec',
404 r'ref tmpl mapfile')
408 r'ref tmpl mapfile')
405
409
406 def lookuptemplate(ui, topic, tmpl):
410 def lookuptemplate(ui, topic, tmpl):
407 """Find the template matching the given -T/--template spec 'tmpl'
411 """Find the template matching the given -T/--template spec 'tmpl'
408
412
409 'tmpl' can be any of the following:
413 'tmpl' can be any of the following:
410
414
411 - a literal template (e.g. '{rev}')
415 - a literal template (e.g. '{rev}')
412 - a map-file name or path (e.g. 'changelog')
416 - a map-file name or path (e.g. 'changelog')
413 - a reference to [templates] in config file
417 - a reference to [templates] in config file
414 - a path to raw template file
418 - a path to raw template file
415
419
416 A map file defines a stand-alone template environment. If a map file
420 A map file defines a stand-alone template environment. If a map file
417 selected, all templates defined in the file will be loaded, and the
421 selected, all templates defined in the file will be loaded, and the
418 template matching the given topic will be rendered. Aliases won't be
422 template matching the given topic will be rendered. Aliases won't be
419 loaded from user config, but from the map file.
423 loaded from user config, but from the map file.
420
424
421 If no map file selected, all templates in [templates] section will be
425 If no map file selected, all templates in [templates] section will be
422 available as well as aliases in [templatealias].
426 available as well as aliases in [templatealias].
423 """
427 """
424
428
425 # looks like a literal template?
429 # looks like a literal template?
426 if '{' in tmpl:
430 if '{' in tmpl:
427 return templatespec('', tmpl, None)
431 return templatespec('', tmpl, None)
428
432
429 # perhaps a stock style?
433 # perhaps a stock style?
430 if not os.path.split(tmpl)[0]:
434 if not os.path.split(tmpl)[0]:
431 mapname = (templater.templatepath('map-cmdline.' + tmpl)
435 mapname = (templater.templatepath('map-cmdline.' + tmpl)
432 or templater.templatepath(tmpl))
436 or templater.templatepath(tmpl))
433 if mapname and os.path.isfile(mapname):
437 if mapname and os.path.isfile(mapname):
434 return templatespec(topic, None, mapname)
438 return templatespec(topic, None, mapname)
435
439
436 # perhaps it's a reference to [templates]
440 # perhaps it's a reference to [templates]
437 if ui.config('templates', tmpl):
441 if ui.config('templates', tmpl):
438 return templatespec(tmpl, None, None)
442 return templatespec(tmpl, None, None)
439
443
440 if tmpl == 'list':
444 if tmpl == 'list':
441 ui.write(_("available styles: %s\n") % templater.stylelist())
445 ui.write(_("available styles: %s\n") % templater.stylelist())
442 raise error.Abort(_("specify a template"))
446 raise error.Abort(_("specify a template"))
443
447
444 # perhaps it's a path to a map or a template
448 # perhaps it's a path to a map or a template
445 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
449 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
446 # is it a mapfile for a style?
450 # is it a mapfile for a style?
447 if os.path.basename(tmpl).startswith("map-"):
451 if os.path.basename(tmpl).startswith("map-"):
448 return templatespec(topic, None, os.path.realpath(tmpl))
452 return templatespec(topic, None, os.path.realpath(tmpl))
449 with util.posixfile(tmpl, 'rb') as f:
453 with util.posixfile(tmpl, 'rb') as f:
450 tmpl = f.read()
454 tmpl = f.read()
451 return templatespec('', tmpl, None)
455 return templatespec('', tmpl, None)
452
456
453 # constant string?
457 # constant string?
454 return templatespec('', tmpl, None)
458 return templatespec('', tmpl, None)
455
459
456 def templatepartsmap(spec, t, partnames):
460 def templatepartsmap(spec, t, partnames):
457 """Create a mapping of {part: ref}"""
461 """Create a mapping of {part: ref}"""
458 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
462 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
459 if spec.mapfile:
463 if spec.mapfile:
460 partsmap.update((p, p) for p in partnames if p in t)
464 partsmap.update((p, p) for p in partnames if p in t)
461 elif spec.ref:
465 elif spec.ref:
462 for part in partnames:
466 for part in partnames:
463 ref = '%s:%s' % (spec.ref, part) # select config sub-section
467 ref = '%s:%s' % (spec.ref, part) # select config sub-section
464 if ref in t:
468 if ref in t:
465 partsmap[part] = ref
469 partsmap[part] = ref
466 return partsmap
470 return partsmap
467
471
468 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
472 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
469 """Create a templater from either a literal template or loading from
473 """Create a templater from either a literal template or loading from
470 a map file"""
474 a map file"""
471 assert not (spec.tmpl and spec.mapfile)
475 assert not (spec.tmpl and spec.mapfile)
472 if spec.mapfile:
476 if spec.mapfile:
473 frommapfile = templater.templater.frommapfile
477 frommapfile = templater.templater.frommapfile
474 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
478 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
475 cache=cache)
479 cache=cache)
476 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
480 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
477 cache=cache)
481 cache=cache)
478
482
479 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
483 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
480 """Create a templater from a string template 'tmpl'"""
484 """Create a templater from a string template 'tmpl'"""
481 aliases = ui.configitems('templatealias')
485 aliases = ui.configitems('templatealias')
482 t = templater.templater(defaults=defaults, resources=resources,
486 t = templater.templater(defaults=defaults, resources=resources,
483 cache=cache, aliases=aliases)
487 cache=cache, aliases=aliases)
484 t.cache.update((k, templater.unquotestring(v))
488 t.cache.update((k, templater.unquotestring(v))
485 for k, v in ui.configitems('templates'))
489 for k, v in ui.configitems('templates'))
486 if tmpl:
490 if tmpl:
487 t.cache[''] = tmpl
491 t.cache[''] = tmpl
488 return t
492 return t
489
493
490 def templateresources(ui, repo=None):
494 def templateresources(ui, repo=None):
491 """Create a dict of template resources designed for the default templatekw
495 """Create a dict of template resources designed for the default templatekw
492 and function"""
496 and function"""
493 return {
497 return {
494 'cache': {}, # for templatekw/funcs to store reusable data
498 'cache': {}, # for templatekw/funcs to store reusable data
495 'ctx': None,
499 'ctx': None,
496 'repo': repo,
500 'repo': repo,
497 'revcache': None, # per-ctx cache; set later
501 'revcache': None, # per-ctx cache; set later
498 'ui': ui,
502 'ui': ui,
499 }
503 }
500
504
501 def formatter(ui, out, topic, opts):
505 def formatter(ui, out, topic, opts):
502 template = opts.get("template", "")
506 template = opts.get("template", "")
503 if template == "json":
507 if template == "json":
504 return jsonformatter(ui, out, topic, opts)
508 return jsonformatter(ui, out, topic, opts)
505 elif template == "pickle":
509 elif template == "pickle":
506 return pickleformatter(ui, out, topic, opts)
510 return pickleformatter(ui, out, topic, opts)
507 elif template == "debug":
511 elif template == "debug":
508 return debugformatter(ui, out, topic, opts)
512 return debugformatter(ui, out, topic, opts)
509 elif template != "":
513 elif template != "":
510 return templateformatter(ui, out, topic, opts)
514 return templateformatter(ui, out, topic, opts)
511 # developer config: ui.formatdebug
515 # developer config: ui.formatdebug
512 elif ui.configbool('ui', 'formatdebug'):
516 elif ui.configbool('ui', 'formatdebug'):
513 return debugformatter(ui, out, topic, opts)
517 return debugformatter(ui, out, topic, opts)
514 # deprecated config: ui.formatjson
518 # deprecated config: ui.formatjson
515 elif ui.configbool('ui', 'formatjson'):
519 elif ui.configbool('ui', 'formatjson'):
516 return jsonformatter(ui, out, topic, opts)
520 return jsonformatter(ui, out, topic, opts)
517 return plainformatter(ui, out, topic, opts)
521 return plainformatter(ui, out, topic, opts)
518
522
519 @contextlib.contextmanager
523 @contextlib.contextmanager
520 def openformatter(ui, filename, topic, opts):
524 def openformatter(ui, filename, topic, opts):
521 """Create a formatter that writes outputs to the specified file
525 """Create a formatter that writes outputs to the specified file
522
526
523 Must be invoked using the 'with' statement.
527 Must be invoked using the 'with' statement.
524 """
528 """
525 with util.posixfile(filename, 'wb') as out:
529 with util.posixfile(filename, 'wb') as out:
526 with formatter(ui, out, topic, opts) as fm:
530 with formatter(ui, out, topic, opts) as fm:
527 yield fm
531 yield fm
528
532
529 @contextlib.contextmanager
533 @contextlib.contextmanager
530 def _neverending(fm):
534 def _neverending(fm):
531 yield fm
535 yield fm
532
536
533 def maybereopen(fm, filename, opts):
537 def maybereopen(fm, filename, opts):
534 """Create a formatter backed by file if filename specified, else return
538 """Create a formatter backed by file if filename specified, else return
535 the given formatter
539 the given formatter
536
540
537 Must be invoked using the 'with' statement. This will never call fm.end()
541 Must be invoked using the 'with' statement. This will never call fm.end()
538 of the given formatter.
542 of the given formatter.
539 """
543 """
540 if filename:
544 if filename:
541 return openformatter(fm._ui, filename, fm._topic, opts)
545 return openformatter(fm._ui, filename, fm._topic, opts)
542 else:
546 else:
543 return _neverending(fm)
547 return _neverending(fm)
@@ -1,995 +1,999 b''
1 # templatekw.py - common changeset template keywords
1 # templatekw.py - common changeset template keywords
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 from .i18n import _
10 from .i18n import _
11 from .node import (
11 from .node import (
12 hex,
12 hex,
13 nullid,
13 nullid,
14 )
14 )
15
15
16 from . import (
16 from . import (
17 encoding,
17 encoding,
18 error,
18 error,
19 hbisect,
19 hbisect,
20 i18n,
20 i18n,
21 obsutil,
21 obsutil,
22 patch,
22 patch,
23 pycompat,
23 pycompat,
24 registrar,
24 registrar,
25 scmutil,
25 scmutil,
26 util,
26 util,
27 )
27 )
28
28
29 class _hybrid(object):
29 class _hybrid(object):
30 """Wrapper for list or dict to support legacy template
30 """Wrapper for list or dict to support legacy template
31
31
32 This class allows us to handle both:
32 This class allows us to handle both:
33 - "{files}" (legacy command-line-specific list hack) and
33 - "{files}" (legacy command-line-specific list hack) and
34 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
34 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
35 and to access raw values:
35 and to access raw values:
36 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
36 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
37 - "{get(extras, key)}"
37 - "{get(extras, key)}"
38 - "{files|json}"
38 - "{files|json}"
39 """
39 """
40
40
41 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
41 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
42 if gen is not None:
42 if gen is not None:
43 self.gen = gen # generator or function returning generator
43 self.gen = gen # generator or function returning generator
44 self._values = values
44 self._values = values
45 self._makemap = makemap
45 self._makemap = makemap
46 self.joinfmt = joinfmt
46 self.joinfmt = joinfmt
47 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
47 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
48 def gen(self):
48 def gen(self):
49 """Default generator to stringify this as {join(self, ' ')}"""
49 """Default generator to stringify this as {join(self, ' ')}"""
50 for i, x in enumerate(self._values):
50 for i, x in enumerate(self._values):
51 if i > 0:
51 if i > 0:
52 yield ' '
52 yield ' '
53 yield self.joinfmt(x)
53 yield self.joinfmt(x)
54 def itermaps(self):
54 def itermaps(self):
55 makemap = self._makemap
55 makemap = self._makemap
56 for x in self._values:
56 for x in self._values:
57 yield makemap(x)
57 yield makemap(x)
58 def __contains__(self, x):
58 def __contains__(self, x):
59 return x in self._values
59 return x in self._values
60 def __getitem__(self, key):
60 def __getitem__(self, key):
61 return self._values[key]
61 return self._values[key]
62 def __len__(self):
62 def __len__(self):
63 return len(self._values)
63 return len(self._values)
64 def __iter__(self):
64 def __iter__(self):
65 return iter(self._values)
65 return iter(self._values)
66 def __getattr__(self, name):
66 def __getattr__(self, name):
67 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
67 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
68 r'itervalues', r'keys', r'values'):
68 r'itervalues', r'keys', r'values'):
69 raise AttributeError(name)
69 raise AttributeError(name)
70 return getattr(self._values, name)
70 return getattr(self._values, name)
71
71
72 class _mappable(object):
72 class _mappable(object):
73 """Wrapper for non-list/dict object to support map operation
73 """Wrapper for non-list/dict object to support map operation
74
74
75 This class allows us to handle both:
75 This class allows us to handle both:
76 - "{manifest}"
76 - "{manifest}"
77 - "{manifest % '{rev}:{node}'}"
77 - "{manifest % '{rev}:{node}'}"
78 - "{manifest.rev}"
78 - "{manifest.rev}"
79
79
80 Unlike a _hybrid, this does not simulate the behavior of the underling
80 Unlike a _hybrid, this does not simulate the behavior of the underling
81 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
81 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
82 """
82 """
83
83
84 def __init__(self, gen, key, value, makemap):
84 def __init__(self, gen, key, value, makemap):
85 if gen is not None:
85 if gen is not None:
86 self.gen = gen # generator or function returning generator
86 self.gen = gen # generator or function returning generator
87 self._key = key
87 self._key = key
88 self._value = value # may be generator of strings
88 self._value = value # may be generator of strings
89 self._makemap = makemap
89 self._makemap = makemap
90
90
91 def gen(self):
91 def gen(self):
92 yield pycompat.bytestr(self._value)
92 yield pycompat.bytestr(self._value)
93
93
94 def tomap(self):
94 def tomap(self):
95 return self._makemap(self._key)
95 return self._makemap(self._key)
96
96
97 def itermaps(self):
97 def itermaps(self):
98 yield self.tomap()
98 yield self.tomap()
99
99
100 def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None):
100 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
101 """Wrap data to support both dict-like and string-like operations"""
101 """Wrap data to support both dict-like and string-like operations"""
102 if fmt is None:
103 fmt = '%s=%s'
102 return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
104 return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
103 lambda k: fmt % (k, data[k]))
105 lambda k: fmt % (k, data[k]))
104
106
105 def hybridlist(data, name, fmt='%s', gen=None):
107 def hybridlist(data, name, fmt=None, gen=None):
106 """Wrap data to support both list-like and string-like operations"""
108 """Wrap data to support both list-like and string-like operations"""
109 if fmt is None:
110 fmt = '%s'
107 return _hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % x)
111 return _hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % x)
108
112
109 def unwraphybrid(thing):
113 def unwraphybrid(thing):
110 """Return an object which can be stringified possibly by using a legacy
114 """Return an object which can be stringified possibly by using a legacy
111 template"""
115 template"""
112 gen = getattr(thing, 'gen', None)
116 gen = getattr(thing, 'gen', None)
113 if gen is None:
117 if gen is None:
114 return thing
118 return thing
115 if callable(gen):
119 if callable(gen):
116 return gen()
120 return gen()
117 return gen
121 return gen
118
122
119 def unwrapvalue(thing):
123 def unwrapvalue(thing):
120 """Move the inner value object out of the wrapper"""
124 """Move the inner value object out of the wrapper"""
121 if not util.safehasattr(thing, '_value'):
125 if not util.safehasattr(thing, '_value'):
122 return thing
126 return thing
123 return thing._value
127 return thing._value
124
128
125 def wraphybridvalue(container, key, value):
129 def wraphybridvalue(container, key, value):
126 """Wrap an element of hybrid container to be mappable
130 """Wrap an element of hybrid container to be mappable
127
131
128 The key is passed to the makemap function of the given container, which
132 The key is passed to the makemap function of the given container, which
129 should be an item generated by iter(container).
133 should be an item generated by iter(container).
130 """
134 """
131 makemap = getattr(container, '_makemap', None)
135 makemap = getattr(container, '_makemap', None)
132 if makemap is None:
136 if makemap is None:
133 return value
137 return value
134 if util.safehasattr(value, '_makemap'):
138 if util.safehasattr(value, '_makemap'):
135 # a nested hybrid list/dict, which has its own way of map operation
139 # a nested hybrid list/dict, which has its own way of map operation
136 return value
140 return value
137 return _mappable(None, key, value, makemap)
141 return _mappable(None, key, value, makemap)
138
142
139 def compatdict(context, mapping, name, data, key='key', value='value',
143 def compatdict(context, mapping, name, data, key='key', value='value',
140 fmt='%s=%s', plural=None, separator=' '):
144 fmt=None, plural=None, separator=' '):
141 """Wrap data like hybriddict(), but also supports old-style list template
145 """Wrap data like hybriddict(), but also supports old-style list template
142
146
143 This exists for backward compatibility with the old-style template. Use
147 This exists for backward compatibility with the old-style template. Use
144 hybriddict() for new template keywords.
148 hybriddict() for new template keywords.
145 """
149 """
146 c = [{key: k, value: v} for k, v in data.iteritems()]
150 c = [{key: k, value: v} for k, v in data.iteritems()]
147 t = context.resource(mapping, 'templ')
151 t = context.resource(mapping, 'templ')
148 f = _showlist(name, c, t, mapping, plural, separator)
152 f = _showlist(name, c, t, mapping, plural, separator)
149 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
153 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
150
154
151 def compatlist(context, mapping, name, data, element=None, fmt='%s',
155 def compatlist(context, mapping, name, data, element=None, fmt=None,
152 plural=None, separator=' '):
156 plural=None, separator=' '):
153 """Wrap data like hybridlist(), but also supports old-style list template
157 """Wrap data like hybridlist(), but also supports old-style list template
154
158
155 This exists for backward compatibility with the old-style template. Use
159 This exists for backward compatibility with the old-style template. Use
156 hybridlist() for new template keywords.
160 hybridlist() for new template keywords.
157 """
161 """
158 t = context.resource(mapping, 'templ')
162 t = context.resource(mapping, 'templ')
159 f = _showlist(name, data, t, mapping, plural, separator)
163 f = _showlist(name, data, t, mapping, plural, separator)
160 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
164 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
161
165
162 def showdict(name, data, mapping, plural=None, key='key', value='value',
166 def showdict(name, data, mapping, plural=None, key='key', value='value',
163 fmt='%s=%s', separator=' '):
167 fmt=None, separator=' '):
164 ui = mapping.get('ui')
168 ui = mapping.get('ui')
165 if ui:
169 if ui:
166 ui.deprecwarn("templatekw.showdict() is deprecated, use compatdict()",
170 ui.deprecwarn("templatekw.showdict() is deprecated, use compatdict()",
167 '4.6')
171 '4.6')
168 c = [{key: k, value: v} for k, v in data.iteritems()]
172 c = [{key: k, value: v} for k, v in data.iteritems()]
169 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
173 f = _showlist(name, c, mapping['templ'], mapping, plural, separator)
170 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
174 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
171
175
172 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
176 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
173 ui = mapping.get('ui')
177 ui = mapping.get('ui')
174 if ui:
178 if ui:
175 ui.deprecwarn("templatekw.showlist() is deprecated, use compatlist()",
179 ui.deprecwarn("templatekw.showlist() is deprecated, use compatlist()",
176 '4.6')
180 '4.6')
177 if not element:
181 if not element:
178 element = name
182 element = name
179 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
183 f = _showlist(name, values, mapping['templ'], mapping, plural, separator)
180 return hybridlist(values, name=element, gen=f)
184 return hybridlist(values, name=element, gen=f)
181
185
182 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
186 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
183 '''expand set of values.
187 '''expand set of values.
184 name is name of key in template map.
188 name is name of key in template map.
185 values is list of strings or dicts.
189 values is list of strings or dicts.
186 plural is plural of name, if not simply name + 's'.
190 plural is plural of name, if not simply name + 's'.
187 separator is used to join values as a string
191 separator is used to join values as a string
188
192
189 expansion works like this, given name 'foo'.
193 expansion works like this, given name 'foo'.
190
194
191 if values is empty, expand 'no_foos'.
195 if values is empty, expand 'no_foos'.
192
196
193 if 'foo' not in template map, return values as a string,
197 if 'foo' not in template map, return values as a string,
194 joined by 'separator'.
198 joined by 'separator'.
195
199
196 expand 'start_foos'.
200 expand 'start_foos'.
197
201
198 for each value, expand 'foo'. if 'last_foo' in template
202 for each value, expand 'foo'. if 'last_foo' in template
199 map, expand it instead of 'foo' for last key.
203 map, expand it instead of 'foo' for last key.
200
204
201 expand 'end_foos'.
205 expand 'end_foos'.
202 '''
206 '''
203 strmapping = pycompat.strkwargs(mapping)
207 strmapping = pycompat.strkwargs(mapping)
204 if not plural:
208 if not plural:
205 plural = name + 's'
209 plural = name + 's'
206 if not values:
210 if not values:
207 noname = 'no_' + plural
211 noname = 'no_' + plural
208 if noname in templ:
212 if noname in templ:
209 yield templ(noname, **strmapping)
213 yield templ(noname, **strmapping)
210 return
214 return
211 if name not in templ:
215 if name not in templ:
212 if isinstance(values[0], bytes):
216 if isinstance(values[0], bytes):
213 yield separator.join(values)
217 yield separator.join(values)
214 else:
218 else:
215 for v in values:
219 for v in values:
216 r = dict(v)
220 r = dict(v)
217 r.update(mapping)
221 r.update(mapping)
218 yield r
222 yield r
219 return
223 return
220 startname = 'start_' + plural
224 startname = 'start_' + plural
221 if startname in templ:
225 if startname in templ:
222 yield templ(startname, **strmapping)
226 yield templ(startname, **strmapping)
223 vmapping = mapping.copy()
227 vmapping = mapping.copy()
224 def one(v, tag=name):
228 def one(v, tag=name):
225 try:
229 try:
226 vmapping.update(v)
230 vmapping.update(v)
227 # Python 2 raises ValueError if the type of v is wrong. Python
231 # Python 2 raises ValueError if the type of v is wrong. Python
228 # 3 raises TypeError.
232 # 3 raises TypeError.
229 except (AttributeError, TypeError, ValueError):
233 except (AttributeError, TypeError, ValueError):
230 try:
234 try:
231 # Python 2 raises ValueError trying to destructure an e.g.
235 # Python 2 raises ValueError trying to destructure an e.g.
232 # bytes. Python 3 raises TypeError.
236 # bytes. Python 3 raises TypeError.
233 for a, b in v:
237 for a, b in v:
234 vmapping[a] = b
238 vmapping[a] = b
235 except (TypeError, ValueError):
239 except (TypeError, ValueError):
236 vmapping[name] = v
240 vmapping[name] = v
237 return templ(tag, **pycompat.strkwargs(vmapping))
241 return templ(tag, **pycompat.strkwargs(vmapping))
238 lastname = 'last_' + name
242 lastname = 'last_' + name
239 if lastname in templ:
243 if lastname in templ:
240 last = values.pop()
244 last = values.pop()
241 else:
245 else:
242 last = None
246 last = None
243 for v in values:
247 for v in values:
244 yield one(v)
248 yield one(v)
245 if last is not None:
249 if last is not None:
246 yield one(last, tag=lastname)
250 yield one(last, tag=lastname)
247 endname = 'end_' + plural
251 endname = 'end_' + plural
248 if endname in templ:
252 if endname in templ:
249 yield templ(endname, **strmapping)
253 yield templ(endname, **strmapping)
250
254
251 def getlatesttags(context, mapping, pattern=None):
255 def getlatesttags(context, mapping, pattern=None):
252 '''return date, distance and name for the latest tag of rev'''
256 '''return date, distance and name for the latest tag of rev'''
253 repo = context.resource(mapping, 'repo')
257 repo = context.resource(mapping, 'repo')
254 ctx = context.resource(mapping, 'ctx')
258 ctx = context.resource(mapping, 'ctx')
255 cache = context.resource(mapping, 'cache')
259 cache = context.resource(mapping, 'cache')
256
260
257 cachename = 'latesttags'
261 cachename = 'latesttags'
258 if pattern is not None:
262 if pattern is not None:
259 cachename += '-' + pattern
263 cachename += '-' + pattern
260 match = util.stringmatcher(pattern)[2]
264 match = util.stringmatcher(pattern)[2]
261 else:
265 else:
262 match = util.always
266 match = util.always
263
267
264 if cachename not in cache:
268 if cachename not in cache:
265 # Cache mapping from rev to a tuple with tag date, tag
269 # Cache mapping from rev to a tuple with tag date, tag
266 # distance and tag name
270 # distance and tag name
267 cache[cachename] = {-1: (0, 0, ['null'])}
271 cache[cachename] = {-1: (0, 0, ['null'])}
268 latesttags = cache[cachename]
272 latesttags = cache[cachename]
269
273
270 rev = ctx.rev()
274 rev = ctx.rev()
271 todo = [rev]
275 todo = [rev]
272 while todo:
276 while todo:
273 rev = todo.pop()
277 rev = todo.pop()
274 if rev in latesttags:
278 if rev in latesttags:
275 continue
279 continue
276 ctx = repo[rev]
280 ctx = repo[rev]
277 tags = [t for t in ctx.tags()
281 tags = [t for t in ctx.tags()
278 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
282 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
279 and match(t))]
283 and match(t))]
280 if tags:
284 if tags:
281 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
285 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
282 continue
286 continue
283 try:
287 try:
284 ptags = [latesttags[p.rev()] for p in ctx.parents()]
288 ptags = [latesttags[p.rev()] for p in ctx.parents()]
285 if len(ptags) > 1:
289 if len(ptags) > 1:
286 if ptags[0][2] == ptags[1][2]:
290 if ptags[0][2] == ptags[1][2]:
287 # The tuples are laid out so the right one can be found by
291 # The tuples are laid out so the right one can be found by
288 # comparison in this case.
292 # comparison in this case.
289 pdate, pdist, ptag = max(ptags)
293 pdate, pdist, ptag = max(ptags)
290 else:
294 else:
291 def key(x):
295 def key(x):
292 changessincetag = len(repo.revs('only(%d, %s)',
296 changessincetag = len(repo.revs('only(%d, %s)',
293 ctx.rev(), x[2][0]))
297 ctx.rev(), x[2][0]))
294 # Smallest number of changes since tag wins. Date is
298 # Smallest number of changes since tag wins. Date is
295 # used as tiebreaker.
299 # used as tiebreaker.
296 return [-changessincetag, x[0]]
300 return [-changessincetag, x[0]]
297 pdate, pdist, ptag = max(ptags, key=key)
301 pdate, pdist, ptag = max(ptags, key=key)
298 else:
302 else:
299 pdate, pdist, ptag = ptags[0]
303 pdate, pdist, ptag = ptags[0]
300 except KeyError:
304 except KeyError:
301 # Cache miss - recurse
305 # Cache miss - recurse
302 todo.append(rev)
306 todo.append(rev)
303 todo.extend(p.rev() for p in ctx.parents())
307 todo.extend(p.rev() for p in ctx.parents())
304 continue
308 continue
305 latesttags[rev] = pdate, pdist + 1, ptag
309 latesttags[rev] = pdate, pdist + 1, ptag
306 return latesttags[rev]
310 return latesttags[rev]
307
311
308 def getrenamedfn(repo, endrev=None):
312 def getrenamedfn(repo, endrev=None):
309 rcache = {}
313 rcache = {}
310 if endrev is None:
314 if endrev is None:
311 endrev = len(repo)
315 endrev = len(repo)
312
316
313 def getrenamed(fn, rev):
317 def getrenamed(fn, rev):
314 '''looks up all renames for a file (up to endrev) the first
318 '''looks up all renames for a file (up to endrev) the first
315 time the file is given. It indexes on the changerev and only
319 time the file is given. It indexes on the changerev and only
316 parses the manifest if linkrev != changerev.
320 parses the manifest if linkrev != changerev.
317 Returns rename info for fn at changerev rev.'''
321 Returns rename info for fn at changerev rev.'''
318 if fn not in rcache:
322 if fn not in rcache:
319 rcache[fn] = {}
323 rcache[fn] = {}
320 fl = repo.file(fn)
324 fl = repo.file(fn)
321 for i in fl:
325 for i in fl:
322 lr = fl.linkrev(i)
326 lr = fl.linkrev(i)
323 renamed = fl.renamed(fl.node(i))
327 renamed = fl.renamed(fl.node(i))
324 rcache[fn][lr] = renamed
328 rcache[fn][lr] = renamed
325 if lr >= endrev:
329 if lr >= endrev:
326 break
330 break
327 if rev in rcache[fn]:
331 if rev in rcache[fn]:
328 return rcache[fn][rev]
332 return rcache[fn][rev]
329
333
330 # If linkrev != rev (i.e. rev not found in rcache) fallback to
334 # If linkrev != rev (i.e. rev not found in rcache) fallback to
331 # filectx logic.
335 # filectx logic.
332 try:
336 try:
333 return repo[rev][fn].renamed()
337 return repo[rev][fn].renamed()
334 except error.LookupError:
338 except error.LookupError:
335 return None
339 return None
336
340
337 return getrenamed
341 return getrenamed
338
342
339 def getlogcolumns():
343 def getlogcolumns():
340 """Return a dict of log column labels"""
344 """Return a dict of log column labels"""
341 _ = pycompat.identity # temporarily disable gettext
345 _ = pycompat.identity # temporarily disable gettext
342 # i18n: column positioning for "hg log"
346 # i18n: column positioning for "hg log"
343 columns = _('bookmark: %s\n'
347 columns = _('bookmark: %s\n'
344 'branch: %s\n'
348 'branch: %s\n'
345 'changeset: %s\n'
349 'changeset: %s\n'
346 'copies: %s\n'
350 'copies: %s\n'
347 'date: %s\n'
351 'date: %s\n'
348 'extra: %s=%s\n'
352 'extra: %s=%s\n'
349 'files+: %s\n'
353 'files+: %s\n'
350 'files-: %s\n'
354 'files-: %s\n'
351 'files: %s\n'
355 'files: %s\n'
352 'instability: %s\n'
356 'instability: %s\n'
353 'manifest: %s\n'
357 'manifest: %s\n'
354 'obsolete: %s\n'
358 'obsolete: %s\n'
355 'parent: %s\n'
359 'parent: %s\n'
356 'phase: %s\n'
360 'phase: %s\n'
357 'summary: %s\n'
361 'summary: %s\n'
358 'tag: %s\n'
362 'tag: %s\n'
359 'user: %s\n')
363 'user: %s\n')
360 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
364 return dict(zip([s.split(':', 1)[0] for s in columns.splitlines()],
361 i18n._(columns).splitlines(True)))
365 i18n._(columns).splitlines(True)))
362
366
363 # default templates internally used for rendering of lists
367 # default templates internally used for rendering of lists
364 defaulttempl = {
368 defaulttempl = {
365 'parent': '{rev}:{node|formatnode} ',
369 'parent': '{rev}:{node|formatnode} ',
366 'manifest': '{rev}:{node|formatnode}',
370 'manifest': '{rev}:{node|formatnode}',
367 'file_copy': '{name} ({source})',
371 'file_copy': '{name} ({source})',
368 'envvar': '{key}={value}',
372 'envvar': '{key}={value}',
369 'extra': '{key}={value|stringescape}'
373 'extra': '{key}={value|stringescape}'
370 }
374 }
371 # filecopy is preserved for compatibility reasons
375 # filecopy is preserved for compatibility reasons
372 defaulttempl['filecopy'] = defaulttempl['file_copy']
376 defaulttempl['filecopy'] = defaulttempl['file_copy']
373
377
374 # keywords are callables (see registrar.templatekeyword for details)
378 # keywords are callables (see registrar.templatekeyword for details)
375 keywords = {}
379 keywords = {}
376 templatekeyword = registrar.templatekeyword(keywords)
380 templatekeyword = registrar.templatekeyword(keywords)
377
381
378 @templatekeyword('author', requires={'ctx'})
382 @templatekeyword('author', requires={'ctx'})
379 def showauthor(context, mapping):
383 def showauthor(context, mapping):
380 """String. The unmodified author of the changeset."""
384 """String. The unmodified author of the changeset."""
381 ctx = context.resource(mapping, 'ctx')
385 ctx = context.resource(mapping, 'ctx')
382 return ctx.user()
386 return ctx.user()
383
387
384 @templatekeyword('bisect', requires={'repo', 'ctx'})
388 @templatekeyword('bisect', requires={'repo', 'ctx'})
385 def showbisect(context, mapping):
389 def showbisect(context, mapping):
386 """String. The changeset bisection status."""
390 """String. The changeset bisection status."""
387 repo = context.resource(mapping, 'repo')
391 repo = context.resource(mapping, 'repo')
388 ctx = context.resource(mapping, 'ctx')
392 ctx = context.resource(mapping, 'ctx')
389 return hbisect.label(repo, ctx.node())
393 return hbisect.label(repo, ctx.node())
390
394
391 @templatekeyword('branch', requires={'ctx'})
395 @templatekeyword('branch', requires={'ctx'})
392 def showbranch(context, mapping):
396 def showbranch(context, mapping):
393 """String. The name of the branch on which the changeset was
397 """String. The name of the branch on which the changeset was
394 committed.
398 committed.
395 """
399 """
396 ctx = context.resource(mapping, 'ctx')
400 ctx = context.resource(mapping, 'ctx')
397 return ctx.branch()
401 return ctx.branch()
398
402
399 @templatekeyword('branches', requires={'ctx', 'templ'})
403 @templatekeyword('branches', requires={'ctx', 'templ'})
400 def showbranches(context, mapping):
404 def showbranches(context, mapping):
401 """List of strings. The name of the branch on which the
405 """List of strings. The name of the branch on which the
402 changeset was committed. Will be empty if the branch name was
406 changeset was committed. Will be empty if the branch name was
403 default. (DEPRECATED)
407 default. (DEPRECATED)
404 """
408 """
405 ctx = context.resource(mapping, 'ctx')
409 ctx = context.resource(mapping, 'ctx')
406 branch = ctx.branch()
410 branch = ctx.branch()
407 if branch != 'default':
411 if branch != 'default':
408 return compatlist(context, mapping, 'branch', [branch],
412 return compatlist(context, mapping, 'branch', [branch],
409 plural='branches')
413 plural='branches')
410 return compatlist(context, mapping, 'branch', [], plural='branches')
414 return compatlist(context, mapping, 'branch', [], plural='branches')
411
415
412 @templatekeyword('bookmarks', requires={'repo', 'ctx', 'templ'})
416 @templatekeyword('bookmarks', requires={'repo', 'ctx', 'templ'})
413 def showbookmarks(context, mapping):
417 def showbookmarks(context, mapping):
414 """List of strings. Any bookmarks associated with the
418 """List of strings. Any bookmarks associated with the
415 changeset. Also sets 'active', the name of the active bookmark.
419 changeset. Also sets 'active', the name of the active bookmark.
416 """
420 """
417 repo = context.resource(mapping, 'repo')
421 repo = context.resource(mapping, 'repo')
418 ctx = context.resource(mapping, 'ctx')
422 ctx = context.resource(mapping, 'ctx')
419 templ = context.resource(mapping, 'templ')
423 templ = context.resource(mapping, 'templ')
420 bookmarks = ctx.bookmarks()
424 bookmarks = ctx.bookmarks()
421 active = repo._activebookmark
425 active = repo._activebookmark
422 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
426 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
423 f = _showlist('bookmark', bookmarks, templ, mapping)
427 f = _showlist('bookmark', bookmarks, templ, mapping)
424 return _hybrid(f, bookmarks, makemap, pycompat.identity)
428 return _hybrid(f, bookmarks, makemap, pycompat.identity)
425
429
426 @templatekeyword('children', requires={'ctx', 'templ'})
430 @templatekeyword('children', requires={'ctx', 'templ'})
427 def showchildren(context, mapping):
431 def showchildren(context, mapping):
428 """List of strings. The children of the changeset."""
432 """List of strings. The children of the changeset."""
429 ctx = context.resource(mapping, 'ctx')
433 ctx = context.resource(mapping, 'ctx')
430 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
434 childrevs = ['%d:%s' % (cctx.rev(), cctx) for cctx in ctx.children()]
431 return compatlist(context, mapping, 'children', childrevs, element='child')
435 return compatlist(context, mapping, 'children', childrevs, element='child')
432
436
433 # Deprecated, but kept alive for help generation a purpose.
437 # Deprecated, but kept alive for help generation a purpose.
434 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
438 @templatekeyword('currentbookmark', requires={'repo', 'ctx'})
435 def showcurrentbookmark(context, mapping):
439 def showcurrentbookmark(context, mapping):
436 """String. The active bookmark, if it is associated with the changeset.
440 """String. The active bookmark, if it is associated with the changeset.
437 (DEPRECATED)"""
441 (DEPRECATED)"""
438 return showactivebookmark(context, mapping)
442 return showactivebookmark(context, mapping)
439
443
440 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
444 @templatekeyword('activebookmark', requires={'repo', 'ctx'})
441 def showactivebookmark(context, mapping):
445 def showactivebookmark(context, mapping):
442 """String. The active bookmark, if it is associated with the changeset."""
446 """String. The active bookmark, if it is associated with the changeset."""
443 repo = context.resource(mapping, 'repo')
447 repo = context.resource(mapping, 'repo')
444 ctx = context.resource(mapping, 'ctx')
448 ctx = context.resource(mapping, 'ctx')
445 active = repo._activebookmark
449 active = repo._activebookmark
446 if active and active in ctx.bookmarks():
450 if active and active in ctx.bookmarks():
447 return active
451 return active
448 return ''
452 return ''
449
453
450 @templatekeyword('date', requires={'ctx'})
454 @templatekeyword('date', requires={'ctx'})
451 def showdate(context, mapping):
455 def showdate(context, mapping):
452 """Date information. The date when the changeset was committed."""
456 """Date information. The date when the changeset was committed."""
453 ctx = context.resource(mapping, 'ctx')
457 ctx = context.resource(mapping, 'ctx')
454 return ctx.date()
458 return ctx.date()
455
459
456 @templatekeyword('desc', requires={'ctx'})
460 @templatekeyword('desc', requires={'ctx'})
457 def showdescription(context, mapping):
461 def showdescription(context, mapping):
458 """String. The text of the changeset description."""
462 """String. The text of the changeset description."""
459 ctx = context.resource(mapping, 'ctx')
463 ctx = context.resource(mapping, 'ctx')
460 s = ctx.description()
464 s = ctx.description()
461 if isinstance(s, encoding.localstr):
465 if isinstance(s, encoding.localstr):
462 # try hard to preserve utf-8 bytes
466 # try hard to preserve utf-8 bytes
463 return encoding.tolocal(encoding.fromlocal(s).strip())
467 return encoding.tolocal(encoding.fromlocal(s).strip())
464 else:
468 else:
465 return s.strip()
469 return s.strip()
466
470
467 @templatekeyword('diffstat', requires={'ctx'})
471 @templatekeyword('diffstat', requires={'ctx'})
468 def showdiffstat(context, mapping):
472 def showdiffstat(context, mapping):
469 """String. Statistics of changes with the following format:
473 """String. Statistics of changes with the following format:
470 "modified files: +added/-removed lines"
474 "modified files: +added/-removed lines"
471 """
475 """
472 ctx = context.resource(mapping, 'ctx')
476 ctx = context.resource(mapping, 'ctx')
473 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
477 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
474 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
478 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
475 return '%d: +%d/-%d' % (len(stats), adds, removes)
479 return '%d: +%d/-%d' % (len(stats), adds, removes)
476
480
477 @templatekeyword('envvars', requires={'ui', 'templ'})
481 @templatekeyword('envvars', requires={'ui', 'templ'})
478 def showenvvars(context, mapping):
482 def showenvvars(context, mapping):
479 """A dictionary of environment variables. (EXPERIMENTAL)"""
483 """A dictionary of environment variables. (EXPERIMENTAL)"""
480 ui = context.resource(mapping, 'ui')
484 ui = context.resource(mapping, 'ui')
481 env = ui.exportableenviron()
485 env = ui.exportableenviron()
482 env = util.sortdict((k, env[k]) for k in sorted(env))
486 env = util.sortdict((k, env[k]) for k in sorted(env))
483 return compatdict(context, mapping, 'envvar', env, plural='envvars')
487 return compatdict(context, mapping, 'envvar', env, plural='envvars')
484
488
485 @templatekeyword('extras', requires={'ctx', 'templ'})
489 @templatekeyword('extras', requires={'ctx', 'templ'})
486 def showextras(context, mapping):
490 def showextras(context, mapping):
487 """List of dicts with key, value entries of the 'extras'
491 """List of dicts with key, value entries of the 'extras'
488 field of this changeset."""
492 field of this changeset."""
489 ctx = context.resource(mapping, 'ctx')
493 ctx = context.resource(mapping, 'ctx')
490 templ = context.resource(mapping, 'templ')
494 templ = context.resource(mapping, 'templ')
491 extras = ctx.extra()
495 extras = ctx.extra()
492 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
496 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
493 makemap = lambda k: {'key': k, 'value': extras[k]}
497 makemap = lambda k: {'key': k, 'value': extras[k]}
494 c = [makemap(k) for k in extras]
498 c = [makemap(k) for k in extras]
495 f = _showlist('extra', c, templ, mapping, plural='extras')
499 f = _showlist('extra', c, templ, mapping, plural='extras')
496 return _hybrid(f, extras, makemap,
500 return _hybrid(f, extras, makemap,
497 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
501 lambda k: '%s=%s' % (k, util.escapestr(extras[k])))
498
502
499 def _showfilesbystat(context, mapping, name, index):
503 def _showfilesbystat(context, mapping, name, index):
500 repo = context.resource(mapping, 'repo')
504 repo = context.resource(mapping, 'repo')
501 ctx = context.resource(mapping, 'ctx')
505 ctx = context.resource(mapping, 'ctx')
502 revcache = context.resource(mapping, 'revcache')
506 revcache = context.resource(mapping, 'revcache')
503 if 'files' not in revcache:
507 if 'files' not in revcache:
504 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
508 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
505 files = revcache['files'][index]
509 files = revcache['files'][index]
506 return compatlist(context, mapping, name, files, element='file')
510 return compatlist(context, mapping, name, files, element='file')
507
511
508 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache', 'templ'})
512 @templatekeyword('file_adds', requires={'repo', 'ctx', 'revcache', 'templ'})
509 def showfileadds(context, mapping):
513 def showfileadds(context, mapping):
510 """List of strings. Files added by this changeset."""
514 """List of strings. Files added by this changeset."""
511 return _showfilesbystat(context, mapping, 'file_add', 1)
515 return _showfilesbystat(context, mapping, 'file_add', 1)
512
516
513 @templatekeyword('file_copies',
517 @templatekeyword('file_copies',
514 requires={'repo', 'ctx', 'cache', 'revcache', 'templ'})
518 requires={'repo', 'ctx', 'cache', 'revcache', 'templ'})
515 def showfilecopies(context, mapping):
519 def showfilecopies(context, mapping):
516 """List of strings. Files copied in this changeset with
520 """List of strings. Files copied in this changeset with
517 their sources.
521 their sources.
518 """
522 """
519 repo = context.resource(mapping, 'repo')
523 repo = context.resource(mapping, 'repo')
520 ctx = context.resource(mapping, 'ctx')
524 ctx = context.resource(mapping, 'ctx')
521 cache = context.resource(mapping, 'cache')
525 cache = context.resource(mapping, 'cache')
522 copies = context.resource(mapping, 'revcache').get('copies')
526 copies = context.resource(mapping, 'revcache').get('copies')
523 if copies is None:
527 if copies is None:
524 if 'getrenamed' not in cache:
528 if 'getrenamed' not in cache:
525 cache['getrenamed'] = getrenamedfn(repo)
529 cache['getrenamed'] = getrenamedfn(repo)
526 copies = []
530 copies = []
527 getrenamed = cache['getrenamed']
531 getrenamed = cache['getrenamed']
528 for fn in ctx.files():
532 for fn in ctx.files():
529 rename = getrenamed(fn, ctx.rev())
533 rename = getrenamed(fn, ctx.rev())
530 if rename:
534 if rename:
531 copies.append((fn, rename[0]))
535 copies.append((fn, rename[0]))
532
536
533 copies = util.sortdict(copies)
537 copies = util.sortdict(copies)
534 return compatdict(context, mapping, 'file_copy', copies,
538 return compatdict(context, mapping, 'file_copy', copies,
535 key='name', value='source', fmt='%s (%s)',
539 key='name', value='source', fmt='%s (%s)',
536 plural='file_copies')
540 plural='file_copies')
537
541
538 # showfilecopiesswitch() displays file copies only if copy records are
542 # showfilecopiesswitch() displays file copies only if copy records are
539 # provided before calling the templater, usually with a --copies
543 # provided before calling the templater, usually with a --copies
540 # command line switch.
544 # command line switch.
541 @templatekeyword('file_copies_switch', requires={'revcache', 'templ'})
545 @templatekeyword('file_copies_switch', requires={'revcache', 'templ'})
542 def showfilecopiesswitch(context, mapping):
546 def showfilecopiesswitch(context, mapping):
543 """List of strings. Like "file_copies" but displayed
547 """List of strings. Like "file_copies" but displayed
544 only if the --copied switch is set.
548 only if the --copied switch is set.
545 """
549 """
546 copies = context.resource(mapping, 'revcache').get('copies') or []
550 copies = context.resource(mapping, 'revcache').get('copies') or []
547 copies = util.sortdict(copies)
551 copies = util.sortdict(copies)
548 return compatdict(context, mapping, 'file_copy', copies,
552 return compatdict(context, mapping, 'file_copy', copies,
549 key='name', value='source', fmt='%s (%s)',
553 key='name', value='source', fmt='%s (%s)',
550 plural='file_copies')
554 plural='file_copies')
551
555
552 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache', 'templ'})
556 @templatekeyword('file_dels', requires={'repo', 'ctx', 'revcache', 'templ'})
553 def showfiledels(context, mapping):
557 def showfiledels(context, mapping):
554 """List of strings. Files removed by this changeset."""
558 """List of strings. Files removed by this changeset."""
555 return _showfilesbystat(context, mapping, 'file_del', 2)
559 return _showfilesbystat(context, mapping, 'file_del', 2)
556
560
557 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache', 'templ'})
561 @templatekeyword('file_mods', requires={'repo', 'ctx', 'revcache', 'templ'})
558 def showfilemods(context, mapping):
562 def showfilemods(context, mapping):
559 """List of strings. Files modified by this changeset."""
563 """List of strings. Files modified by this changeset."""
560 return _showfilesbystat(context, mapping, 'file_mod', 0)
564 return _showfilesbystat(context, mapping, 'file_mod', 0)
561
565
562 @templatekeyword('files', requires={'ctx', 'templ'})
566 @templatekeyword('files', requires={'ctx', 'templ'})
563 def showfiles(context, mapping):
567 def showfiles(context, mapping):
564 """List of strings. All files modified, added, or removed by this
568 """List of strings. All files modified, added, or removed by this
565 changeset.
569 changeset.
566 """
570 """
567 ctx = context.resource(mapping, 'ctx')
571 ctx = context.resource(mapping, 'ctx')
568 return compatlist(context, mapping, 'file', ctx.files())
572 return compatlist(context, mapping, 'file', ctx.files())
569
573
570 @templatekeyword('graphnode', requires={'repo', 'ctx'})
574 @templatekeyword('graphnode', requires={'repo', 'ctx'})
571 def showgraphnode(context, mapping):
575 def showgraphnode(context, mapping):
572 """String. The character representing the changeset node in an ASCII
576 """String. The character representing the changeset node in an ASCII
573 revision graph."""
577 revision graph."""
574 repo = context.resource(mapping, 'repo')
578 repo = context.resource(mapping, 'repo')
575 ctx = context.resource(mapping, 'ctx')
579 ctx = context.resource(mapping, 'ctx')
576 return getgraphnode(repo, ctx)
580 return getgraphnode(repo, ctx)
577
581
578 def getgraphnode(repo, ctx):
582 def getgraphnode(repo, ctx):
579 wpnodes = repo.dirstate.parents()
583 wpnodes = repo.dirstate.parents()
580 if wpnodes[1] == nullid:
584 if wpnodes[1] == nullid:
581 wpnodes = wpnodes[:1]
585 wpnodes = wpnodes[:1]
582 if ctx.node() in wpnodes:
586 if ctx.node() in wpnodes:
583 return '@'
587 return '@'
584 elif ctx.obsolete():
588 elif ctx.obsolete():
585 return 'x'
589 return 'x'
586 elif ctx.isunstable():
590 elif ctx.isunstable():
587 return '*'
591 return '*'
588 elif ctx.closesbranch():
592 elif ctx.closesbranch():
589 return '_'
593 return '_'
590 else:
594 else:
591 return 'o'
595 return 'o'
592
596
593 @templatekeyword('graphwidth', requires=())
597 @templatekeyword('graphwidth', requires=())
594 def showgraphwidth(context, mapping):
598 def showgraphwidth(context, mapping):
595 """Integer. The width of the graph drawn by 'log --graph' or zero."""
599 """Integer. The width of the graph drawn by 'log --graph' or zero."""
596 # just hosts documentation; should be overridden by template mapping
600 # just hosts documentation; should be overridden by template mapping
597 return 0
601 return 0
598
602
599 @templatekeyword('index', requires=())
603 @templatekeyword('index', requires=())
600 def showindex(context, mapping):
604 def showindex(context, mapping):
601 """Integer. The current iteration of the loop. (0 indexed)"""
605 """Integer. The current iteration of the loop. (0 indexed)"""
602 # just hosts documentation; should be overridden by template mapping
606 # just hosts documentation; should be overridden by template mapping
603 raise error.Abort(_("can't use index in this context"))
607 raise error.Abort(_("can't use index in this context"))
604
608
605 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache', 'templ'})
609 @templatekeyword('latesttag', requires={'repo', 'ctx', 'cache', 'templ'})
606 def showlatesttag(context, mapping):
610 def showlatesttag(context, mapping):
607 """List of strings. The global tags on the most recent globally
611 """List of strings. The global tags on the most recent globally
608 tagged ancestor of this changeset. If no such tags exist, the list
612 tagged ancestor of this changeset. If no such tags exist, the list
609 consists of the single string "null".
613 consists of the single string "null".
610 """
614 """
611 return showlatesttags(context, mapping, None)
615 return showlatesttags(context, mapping, None)
612
616
613 def showlatesttags(context, mapping, pattern):
617 def showlatesttags(context, mapping, pattern):
614 """helper method for the latesttag keyword and function"""
618 """helper method for the latesttag keyword and function"""
615 latesttags = getlatesttags(context, mapping, pattern)
619 latesttags = getlatesttags(context, mapping, pattern)
616
620
617 # latesttag[0] is an implementation detail for sorting csets on different
621 # latesttag[0] is an implementation detail for sorting csets on different
618 # branches in a stable manner- it is the date the tagged cset was created,
622 # branches in a stable manner- it is the date the tagged cset was created,
619 # not the date the tag was created. Therefore it isn't made visible here.
623 # not the date the tag was created. Therefore it isn't made visible here.
620 makemap = lambda v: {
624 makemap = lambda v: {
621 'changes': _showchangessincetag,
625 'changes': _showchangessincetag,
622 'distance': latesttags[1],
626 'distance': latesttags[1],
623 'latesttag': v, # BC with {latesttag % '{latesttag}'}
627 'latesttag': v, # BC with {latesttag % '{latesttag}'}
624 'tag': v
628 'tag': v
625 }
629 }
626
630
627 tags = latesttags[2]
631 tags = latesttags[2]
628 templ = context.resource(mapping, 'templ')
632 templ = context.resource(mapping, 'templ')
629 f = _showlist('latesttag', tags, templ, mapping, separator=':')
633 f = _showlist('latesttag', tags, templ, mapping, separator=':')
630 return _hybrid(f, tags, makemap, pycompat.identity)
634 return _hybrid(f, tags, makemap, pycompat.identity)
631
635
632 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
636 @templatekeyword('latesttagdistance', requires={'repo', 'ctx', 'cache'})
633 def showlatesttagdistance(context, mapping):
637 def showlatesttagdistance(context, mapping):
634 """Integer. Longest path to the latest tag."""
638 """Integer. Longest path to the latest tag."""
635 return getlatesttags(context, mapping)[1]
639 return getlatesttags(context, mapping)[1]
636
640
637 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
641 @templatekeyword('changessincelatesttag', requires={'repo', 'ctx', 'cache'})
638 def showchangessincelatesttag(context, mapping):
642 def showchangessincelatesttag(context, mapping):
639 """Integer. All ancestors not in the latest tag."""
643 """Integer. All ancestors not in the latest tag."""
640 mapping = mapping.copy()
644 mapping = mapping.copy()
641 mapping['tag'] = getlatesttags(context, mapping)[2][0]
645 mapping['tag'] = getlatesttags(context, mapping)[2][0]
642 return _showchangessincetag(context, mapping)
646 return _showchangessincetag(context, mapping)
643
647
644 def _showchangessincetag(context, mapping):
648 def _showchangessincetag(context, mapping):
645 repo = context.resource(mapping, 'repo')
649 repo = context.resource(mapping, 'repo')
646 ctx = context.resource(mapping, 'ctx')
650 ctx = context.resource(mapping, 'ctx')
647 offset = 0
651 offset = 0
648 revs = [ctx.rev()]
652 revs = [ctx.rev()]
649 tag = context.symbol(mapping, 'tag')
653 tag = context.symbol(mapping, 'tag')
650
654
651 # The only() revset doesn't currently support wdir()
655 # The only() revset doesn't currently support wdir()
652 if ctx.rev() is None:
656 if ctx.rev() is None:
653 offset = 1
657 offset = 1
654 revs = [p.rev() for p in ctx.parents()]
658 revs = [p.rev() for p in ctx.parents()]
655
659
656 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
660 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
657
661
658 # teach templater latesttags.changes is switched to (context, mapping) API
662 # teach templater latesttags.changes is switched to (context, mapping) API
659 _showchangessincetag._requires = {'repo', 'ctx'}
663 _showchangessincetag._requires = {'repo', 'ctx'}
660
664
661 @templatekeyword('manifest', requires={'repo', 'ctx', 'templ'})
665 @templatekeyword('manifest', requires={'repo', 'ctx', 'templ'})
662 def showmanifest(context, mapping):
666 def showmanifest(context, mapping):
663 repo = context.resource(mapping, 'repo')
667 repo = context.resource(mapping, 'repo')
664 ctx = context.resource(mapping, 'ctx')
668 ctx = context.resource(mapping, 'ctx')
665 templ = context.resource(mapping, 'templ')
669 templ = context.resource(mapping, 'templ')
666 mnode = ctx.manifestnode()
670 mnode = ctx.manifestnode()
667 if mnode is None:
671 if mnode is None:
668 # just avoid crash, we might want to use the 'ff...' hash in future
672 # just avoid crash, we might want to use the 'ff...' hash in future
669 return
673 return
670 mrev = repo.manifestlog._revlog.rev(mnode)
674 mrev = repo.manifestlog._revlog.rev(mnode)
671 mhex = hex(mnode)
675 mhex = hex(mnode)
672 mapping = mapping.copy()
676 mapping = mapping.copy()
673 mapping.update({'rev': mrev, 'node': mhex})
677 mapping.update({'rev': mrev, 'node': mhex})
674 f = templ('manifest', **pycompat.strkwargs(mapping))
678 f = templ('manifest', **pycompat.strkwargs(mapping))
675 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
679 # TODO: perhaps 'ctx' should be dropped from mapping because manifest
676 # rev and node are completely different from changeset's.
680 # rev and node are completely different from changeset's.
677 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
681 return _mappable(f, None, f, lambda x: {'rev': mrev, 'node': mhex})
678
682
679 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx', 'templ'})
683 @templatekeyword('obsfate', requires={'ui', 'repo', 'ctx', 'templ'})
680 def showobsfate(context, mapping):
684 def showobsfate(context, mapping):
681 # this function returns a list containing pre-formatted obsfate strings.
685 # this function returns a list containing pre-formatted obsfate strings.
682 #
686 #
683 # This function will be replaced by templates fragments when we will have
687 # This function will be replaced by templates fragments when we will have
684 # the verbosity templatekw available.
688 # the verbosity templatekw available.
685 succsandmarkers = showsuccsandmarkers(context, mapping)
689 succsandmarkers = showsuccsandmarkers(context, mapping)
686
690
687 ui = context.resource(mapping, 'ui')
691 ui = context.resource(mapping, 'ui')
688 values = []
692 values = []
689
693
690 for x in succsandmarkers:
694 for x in succsandmarkers:
691 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
695 values.append(obsutil.obsfateprinter(x['successors'], x['markers'], ui))
692
696
693 return compatlist(context, mapping, "fate", values)
697 return compatlist(context, mapping, "fate", values)
694
698
695 def shownames(context, mapping, namespace):
699 def shownames(context, mapping, namespace):
696 """helper method to generate a template keyword for a namespace"""
700 """helper method to generate a template keyword for a namespace"""
697 repo = context.resource(mapping, 'repo')
701 repo = context.resource(mapping, 'repo')
698 ctx = context.resource(mapping, 'ctx')
702 ctx = context.resource(mapping, 'ctx')
699 ns = repo.names[namespace]
703 ns = repo.names[namespace]
700 names = ns.names(repo, ctx.node())
704 names = ns.names(repo, ctx.node())
701 return compatlist(context, mapping, ns.templatename, names,
705 return compatlist(context, mapping, ns.templatename, names,
702 plural=namespace)
706 plural=namespace)
703
707
704 @templatekeyword('namespaces', requires={'repo', 'ctx', 'templ'})
708 @templatekeyword('namespaces', requires={'repo', 'ctx', 'templ'})
705 def shownamespaces(context, mapping):
709 def shownamespaces(context, mapping):
706 """Dict of lists. Names attached to this changeset per
710 """Dict of lists. Names attached to this changeset per
707 namespace."""
711 namespace."""
708 repo = context.resource(mapping, 'repo')
712 repo = context.resource(mapping, 'repo')
709 ctx = context.resource(mapping, 'ctx')
713 ctx = context.resource(mapping, 'ctx')
710 templ = context.resource(mapping, 'templ')
714 templ = context.resource(mapping, 'templ')
711
715
712 namespaces = util.sortdict()
716 namespaces = util.sortdict()
713 def makensmapfn(ns):
717 def makensmapfn(ns):
714 # 'name' for iterating over namespaces, templatename for local reference
718 # 'name' for iterating over namespaces, templatename for local reference
715 return lambda v: {'name': v, ns.templatename: v}
719 return lambda v: {'name': v, ns.templatename: v}
716
720
717 for k, ns in repo.names.iteritems():
721 for k, ns in repo.names.iteritems():
718 names = ns.names(repo, ctx.node())
722 names = ns.names(repo, ctx.node())
719 f = _showlist('name', names, templ, mapping)
723 f = _showlist('name', names, templ, mapping)
720 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
724 namespaces[k] = _hybrid(f, names, makensmapfn(ns), pycompat.identity)
721
725
722 f = _showlist('namespace', list(namespaces), templ, mapping)
726 f = _showlist('namespace', list(namespaces), templ, mapping)
723
727
724 def makemap(ns):
728 def makemap(ns):
725 return {
729 return {
726 'namespace': ns,
730 'namespace': ns,
727 'names': namespaces[ns],
731 'names': namespaces[ns],
728 'builtin': repo.names[ns].builtin,
732 'builtin': repo.names[ns].builtin,
729 'colorname': repo.names[ns].colorname,
733 'colorname': repo.names[ns].colorname,
730 }
734 }
731
735
732 return _hybrid(f, namespaces, makemap, pycompat.identity)
736 return _hybrid(f, namespaces, makemap, pycompat.identity)
733
737
734 @templatekeyword('node', requires={'ctx'})
738 @templatekeyword('node', requires={'ctx'})
735 def shownode(context, mapping):
739 def shownode(context, mapping):
736 """String. The changeset identification hash, as a 40 hexadecimal
740 """String. The changeset identification hash, as a 40 hexadecimal
737 digit string.
741 digit string.
738 """
742 """
739 ctx = context.resource(mapping, 'ctx')
743 ctx = context.resource(mapping, 'ctx')
740 return ctx.hex()
744 return ctx.hex()
741
745
742 @templatekeyword('obsolete', requires={'ctx'})
746 @templatekeyword('obsolete', requires={'ctx'})
743 def showobsolete(context, mapping):
747 def showobsolete(context, mapping):
744 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
748 """String. Whether the changeset is obsolete. (EXPERIMENTAL)"""
745 ctx = context.resource(mapping, 'ctx')
749 ctx = context.resource(mapping, 'ctx')
746 if ctx.obsolete():
750 if ctx.obsolete():
747 return 'obsolete'
751 return 'obsolete'
748 return ''
752 return ''
749
753
750 @templatekeyword('peerurls', requires={'repo'})
754 @templatekeyword('peerurls', requires={'repo'})
751 def showpeerurls(context, mapping):
755 def showpeerurls(context, mapping):
752 """A dictionary of repository locations defined in the [paths] section
756 """A dictionary of repository locations defined in the [paths] section
753 of your configuration file."""
757 of your configuration file."""
754 repo = context.resource(mapping, 'repo')
758 repo = context.resource(mapping, 'repo')
755 # see commands.paths() for naming of dictionary keys
759 # see commands.paths() for naming of dictionary keys
756 paths = repo.ui.paths
760 paths = repo.ui.paths
757 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
761 urls = util.sortdict((k, p.rawloc) for k, p in sorted(paths.iteritems()))
758 def makemap(k):
762 def makemap(k):
759 p = paths[k]
763 p = paths[k]
760 d = {'name': k, 'url': p.rawloc}
764 d = {'name': k, 'url': p.rawloc}
761 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
765 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
762 return d
766 return d
763 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
767 return _hybrid(None, urls, makemap, lambda k: '%s=%s' % (k, urls[k]))
764
768
765 @templatekeyword("predecessors", requires={'repo', 'ctx'})
769 @templatekeyword("predecessors", requires={'repo', 'ctx'})
766 def showpredecessors(context, mapping):
770 def showpredecessors(context, mapping):
767 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
771 """Returns the list if the closest visible successors. (EXPERIMENTAL)"""
768 repo = context.resource(mapping, 'repo')
772 repo = context.resource(mapping, 'repo')
769 ctx = context.resource(mapping, 'ctx')
773 ctx = context.resource(mapping, 'ctx')
770 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
774 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
771 predecessors = map(hex, predecessors)
775 predecessors = map(hex, predecessors)
772
776
773 return _hybrid(None, predecessors,
777 return _hybrid(None, predecessors,
774 lambda x: {'ctx': repo[x], 'revcache': {}},
778 lambda x: {'ctx': repo[x], 'revcache': {}},
775 lambda x: scmutil.formatchangeid(repo[x]))
779 lambda x: scmutil.formatchangeid(repo[x]))
776
780
777 @templatekeyword('reporoot', requires={'repo'})
781 @templatekeyword('reporoot', requires={'repo'})
778 def showreporoot(context, mapping):
782 def showreporoot(context, mapping):
779 """String. The root directory of the current repository."""
783 """String. The root directory of the current repository."""
780 repo = context.resource(mapping, 'repo')
784 repo = context.resource(mapping, 'repo')
781 return repo.root
785 return repo.root
782
786
783 @templatekeyword("successorssets", requires={'repo', 'ctx'})
787 @templatekeyword("successorssets", requires={'repo', 'ctx'})
784 def showsuccessorssets(context, mapping):
788 def showsuccessorssets(context, mapping):
785 """Returns a string of sets of successors for a changectx. Format used
789 """Returns a string of sets of successors for a changectx. Format used
786 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
790 is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and ctx2
787 while also diverged into ctx3. (EXPERIMENTAL)"""
791 while also diverged into ctx3. (EXPERIMENTAL)"""
788 repo = context.resource(mapping, 'repo')
792 repo = context.resource(mapping, 'repo')
789 ctx = context.resource(mapping, 'ctx')
793 ctx = context.resource(mapping, 'ctx')
790 if not ctx.obsolete():
794 if not ctx.obsolete():
791 return ''
795 return ''
792
796
793 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
797 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
794 ssets = [[hex(n) for n in ss] for ss in ssets]
798 ssets = [[hex(n) for n in ss] for ss in ssets]
795
799
796 data = []
800 data = []
797 for ss in ssets:
801 for ss in ssets:
798 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
802 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
799 lambda x: scmutil.formatchangeid(repo[x]))
803 lambda x: scmutil.formatchangeid(repo[x]))
800 data.append(h)
804 data.append(h)
801
805
802 # Format the successorssets
806 # Format the successorssets
803 def render(d):
807 def render(d):
804 t = []
808 t = []
805 for i in d.gen():
809 for i in d.gen():
806 t.append(i)
810 t.append(i)
807 return "".join(t)
811 return "".join(t)
808
812
809 def gen(data):
813 def gen(data):
810 yield "; ".join(render(d) for d in data)
814 yield "; ".join(render(d) for d in data)
811
815
812 return _hybrid(gen(data), data, lambda x: {'successorset': x},
816 return _hybrid(gen(data), data, lambda x: {'successorset': x},
813 pycompat.identity)
817 pycompat.identity)
814
818
815 @templatekeyword("succsandmarkers", requires={'repo', 'ctx', 'templ'})
819 @templatekeyword("succsandmarkers", requires={'repo', 'ctx', 'templ'})
816 def showsuccsandmarkers(context, mapping):
820 def showsuccsandmarkers(context, mapping):
817 """Returns a list of dict for each final successor of ctx. The dict
821 """Returns a list of dict for each final successor of ctx. The dict
818 contains successors node id in "successors" keys and the list of
822 contains successors node id in "successors" keys and the list of
819 obs-markers from ctx to the set of successors in "markers".
823 obs-markers from ctx to the set of successors in "markers".
820 (EXPERIMENTAL)
824 (EXPERIMENTAL)
821 """
825 """
822 repo = context.resource(mapping, 'repo')
826 repo = context.resource(mapping, 'repo')
823 ctx = context.resource(mapping, 'ctx')
827 ctx = context.resource(mapping, 'ctx')
824 templ = context.resource(mapping, 'templ')
828 templ = context.resource(mapping, 'templ')
825
829
826 values = obsutil.successorsandmarkers(repo, ctx)
830 values = obsutil.successorsandmarkers(repo, ctx)
827
831
828 if values is None:
832 if values is None:
829 values = []
833 values = []
830
834
831 # Format successors and markers to avoid exposing binary to templates
835 # Format successors and markers to avoid exposing binary to templates
832 data = []
836 data = []
833 for i in values:
837 for i in values:
834 # Format successors
838 # Format successors
835 successors = i['successors']
839 successors = i['successors']
836
840
837 successors = [hex(n) for n in successors]
841 successors = [hex(n) for n in successors]
838 successors = _hybrid(None, successors,
842 successors = _hybrid(None, successors,
839 lambda x: {'ctx': repo[x], 'revcache': {}},
843 lambda x: {'ctx': repo[x], 'revcache': {}},
840 lambda x: scmutil.formatchangeid(repo[x]))
844 lambda x: scmutil.formatchangeid(repo[x]))
841
845
842 # Format markers
846 # Format markers
843 finalmarkers = []
847 finalmarkers = []
844 for m in i['markers']:
848 for m in i['markers']:
845 hexprec = hex(m[0])
849 hexprec = hex(m[0])
846 hexsucs = tuple(hex(n) for n in m[1])
850 hexsucs = tuple(hex(n) for n in m[1])
847 hexparents = None
851 hexparents = None
848 if m[5] is not None:
852 if m[5] is not None:
849 hexparents = tuple(hex(n) for n in m[5])
853 hexparents = tuple(hex(n) for n in m[5])
850 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
854 newmarker = (hexprec, hexsucs) + m[2:5] + (hexparents,) + m[6:]
851 finalmarkers.append(newmarker)
855 finalmarkers.append(newmarker)
852
856
853 data.append({'successors': successors, 'markers': finalmarkers})
857 data.append({'successors': successors, 'markers': finalmarkers})
854
858
855 f = _showlist('succsandmarkers', data, templ, mapping)
859 f = _showlist('succsandmarkers', data, templ, mapping)
856 return _hybrid(f, data, lambda x: x, pycompat.identity)
860 return _hybrid(f, data, lambda x: x, pycompat.identity)
857
861
858 @templatekeyword('p1rev', requires={'ctx'})
862 @templatekeyword('p1rev', requires={'ctx'})
859 def showp1rev(context, mapping):
863 def showp1rev(context, mapping):
860 """Integer. The repository-local revision number of the changeset's
864 """Integer. The repository-local revision number of the changeset's
861 first parent, or -1 if the changeset has no parents."""
865 first parent, or -1 if the changeset has no parents."""
862 ctx = context.resource(mapping, 'ctx')
866 ctx = context.resource(mapping, 'ctx')
863 return ctx.p1().rev()
867 return ctx.p1().rev()
864
868
865 @templatekeyword('p2rev', requires={'ctx'})
869 @templatekeyword('p2rev', requires={'ctx'})
866 def showp2rev(context, mapping):
870 def showp2rev(context, mapping):
867 """Integer. The repository-local revision number of the changeset's
871 """Integer. The repository-local revision number of the changeset's
868 second parent, or -1 if the changeset has no second parent."""
872 second parent, or -1 if the changeset has no second parent."""
869 ctx = context.resource(mapping, 'ctx')
873 ctx = context.resource(mapping, 'ctx')
870 return ctx.p2().rev()
874 return ctx.p2().rev()
871
875
872 @templatekeyword('p1node', requires={'ctx'})
876 @templatekeyword('p1node', requires={'ctx'})
873 def showp1node(context, mapping):
877 def showp1node(context, mapping):
874 """String. The identification hash of the changeset's first parent,
878 """String. The identification hash of the changeset's first parent,
875 as a 40 digit hexadecimal string. If the changeset has no parents, all
879 as a 40 digit hexadecimal string. If the changeset has no parents, all
876 digits are 0."""
880 digits are 0."""
877 ctx = context.resource(mapping, 'ctx')
881 ctx = context.resource(mapping, 'ctx')
878 return ctx.p1().hex()
882 return ctx.p1().hex()
879
883
880 @templatekeyword('p2node', requires={'ctx'})
884 @templatekeyword('p2node', requires={'ctx'})
881 def showp2node(context, mapping):
885 def showp2node(context, mapping):
882 """String. The identification hash of the changeset's second
886 """String. The identification hash of the changeset's second
883 parent, as a 40 digit hexadecimal string. If the changeset has no second
887 parent, as a 40 digit hexadecimal string. If the changeset has no second
884 parent, all digits are 0."""
888 parent, all digits are 0."""
885 ctx = context.resource(mapping, 'ctx')
889 ctx = context.resource(mapping, 'ctx')
886 return ctx.p2().hex()
890 return ctx.p2().hex()
887
891
888 @templatekeyword('parents', requires={'repo', 'ctx', 'templ'})
892 @templatekeyword('parents', requires={'repo', 'ctx', 'templ'})
889 def showparents(context, mapping):
893 def showparents(context, mapping):
890 """List of strings. The parents of the changeset in "rev:node"
894 """List of strings. The parents of the changeset in "rev:node"
891 format. If the changeset has only one "natural" parent (the predecessor
895 format. If the changeset has only one "natural" parent (the predecessor
892 revision) nothing is shown."""
896 revision) nothing is shown."""
893 repo = context.resource(mapping, 'repo')
897 repo = context.resource(mapping, 'repo')
894 ctx = context.resource(mapping, 'ctx')
898 ctx = context.resource(mapping, 'ctx')
895 templ = context.resource(mapping, 'templ')
899 templ = context.resource(mapping, 'templ')
896 pctxs = scmutil.meaningfulparents(repo, ctx)
900 pctxs = scmutil.meaningfulparents(repo, ctx)
897 prevs = [p.rev() for p in pctxs]
901 prevs = [p.rev() for p in pctxs]
898 parents = [[('rev', p.rev()),
902 parents = [[('rev', p.rev()),
899 ('node', p.hex()),
903 ('node', p.hex()),
900 ('phase', p.phasestr())]
904 ('phase', p.phasestr())]
901 for p in pctxs]
905 for p in pctxs]
902 f = _showlist('parent', parents, templ, mapping)
906 f = _showlist('parent', parents, templ, mapping)
903 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
907 return _hybrid(f, prevs, lambda x: {'ctx': repo[x], 'revcache': {}},
904 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
908 lambda x: scmutil.formatchangeid(repo[x]), keytype=int)
905
909
906 @templatekeyword('phase', requires={'ctx'})
910 @templatekeyword('phase', requires={'ctx'})
907 def showphase(context, mapping):
911 def showphase(context, mapping):
908 """String. The changeset phase name."""
912 """String. The changeset phase name."""
909 ctx = context.resource(mapping, 'ctx')
913 ctx = context.resource(mapping, 'ctx')
910 return ctx.phasestr()
914 return ctx.phasestr()
911
915
912 @templatekeyword('phaseidx', requires={'ctx'})
916 @templatekeyword('phaseidx', requires={'ctx'})
913 def showphaseidx(context, mapping):
917 def showphaseidx(context, mapping):
914 """Integer. The changeset phase index. (ADVANCED)"""
918 """Integer. The changeset phase index. (ADVANCED)"""
915 ctx = context.resource(mapping, 'ctx')
919 ctx = context.resource(mapping, 'ctx')
916 return ctx.phase()
920 return ctx.phase()
917
921
918 @templatekeyword('rev', requires={'ctx'})
922 @templatekeyword('rev', requires={'ctx'})
919 def showrev(context, mapping):
923 def showrev(context, mapping):
920 """Integer. The repository-local changeset revision number."""
924 """Integer. The repository-local changeset revision number."""
921 ctx = context.resource(mapping, 'ctx')
925 ctx = context.resource(mapping, 'ctx')
922 return scmutil.intrev(ctx)
926 return scmutil.intrev(ctx)
923
927
924 def showrevslist(context, mapping, name, revs):
928 def showrevslist(context, mapping, name, revs):
925 """helper to generate a list of revisions in which a mapped template will
929 """helper to generate a list of revisions in which a mapped template will
926 be evaluated"""
930 be evaluated"""
927 repo = context.resource(mapping, 'repo')
931 repo = context.resource(mapping, 'repo')
928 templ = context.resource(mapping, 'templ')
932 templ = context.resource(mapping, 'templ')
929 f = _showlist(name, ['%d' % r for r in revs], templ, mapping)
933 f = _showlist(name, ['%d' % r for r in revs], templ, mapping)
930 return _hybrid(f, revs,
934 return _hybrid(f, revs,
931 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
935 lambda x: {name: x, 'ctx': repo[x], 'revcache': {}},
932 pycompat.identity, keytype=int)
936 pycompat.identity, keytype=int)
933
937
934 @templatekeyword('subrepos', requires={'ctx', 'templ'})
938 @templatekeyword('subrepos', requires={'ctx', 'templ'})
935 def showsubrepos(context, mapping):
939 def showsubrepos(context, mapping):
936 """List of strings. Updated subrepositories in the changeset."""
940 """List of strings. Updated subrepositories in the changeset."""
937 ctx = context.resource(mapping, 'ctx')
941 ctx = context.resource(mapping, 'ctx')
938 substate = ctx.substate
942 substate = ctx.substate
939 if not substate:
943 if not substate:
940 return compatlist(context, mapping, 'subrepo', [])
944 return compatlist(context, mapping, 'subrepo', [])
941 psubstate = ctx.parents()[0].substate or {}
945 psubstate = ctx.parents()[0].substate or {}
942 subrepos = []
946 subrepos = []
943 for sub in substate:
947 for sub in substate:
944 if sub not in psubstate or substate[sub] != psubstate[sub]:
948 if sub not in psubstate or substate[sub] != psubstate[sub]:
945 subrepos.append(sub) # modified or newly added in ctx
949 subrepos.append(sub) # modified or newly added in ctx
946 for sub in psubstate:
950 for sub in psubstate:
947 if sub not in substate:
951 if sub not in substate:
948 subrepos.append(sub) # removed in ctx
952 subrepos.append(sub) # removed in ctx
949 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
953 return compatlist(context, mapping, 'subrepo', sorted(subrepos))
950
954
951 # don't remove "showtags" definition, even though namespaces will put
955 # don't remove "showtags" definition, even though namespaces will put
952 # a helper function for "tags" keyword into "keywords" map automatically,
956 # a helper function for "tags" keyword into "keywords" map automatically,
953 # because online help text is built without namespaces initialization
957 # because online help text is built without namespaces initialization
954 @templatekeyword('tags', requires={'repo', 'ctx', 'templ'})
958 @templatekeyword('tags', requires={'repo', 'ctx', 'templ'})
955 def showtags(context, mapping):
959 def showtags(context, mapping):
956 """List of strings. Any tags associated with the changeset."""
960 """List of strings. Any tags associated with the changeset."""
957 return shownames(context, mapping, 'tags')
961 return shownames(context, mapping, 'tags')
958
962
959 @templatekeyword('termwidth', requires={'ui'})
963 @templatekeyword('termwidth', requires={'ui'})
960 def showtermwidth(context, mapping):
964 def showtermwidth(context, mapping):
961 """Integer. The width of the current terminal."""
965 """Integer. The width of the current terminal."""
962 ui = context.resource(mapping, 'ui')
966 ui = context.resource(mapping, 'ui')
963 return ui.termwidth()
967 return ui.termwidth()
964
968
965 @templatekeyword('instabilities', requires={'ctx', 'templ'})
969 @templatekeyword('instabilities', requires={'ctx', 'templ'})
966 def showinstabilities(context, mapping):
970 def showinstabilities(context, mapping):
967 """List of strings. Evolution instabilities affecting the changeset.
971 """List of strings. Evolution instabilities affecting the changeset.
968 (EXPERIMENTAL)
972 (EXPERIMENTAL)
969 """
973 """
970 ctx = context.resource(mapping, 'ctx')
974 ctx = context.resource(mapping, 'ctx')
971 return compatlist(context, mapping, 'instability', ctx.instabilities(),
975 return compatlist(context, mapping, 'instability', ctx.instabilities(),
972 plural='instabilities')
976 plural='instabilities')
973
977
974 @templatekeyword('verbosity', requires={'ui'})
978 @templatekeyword('verbosity', requires={'ui'})
975 def showverbosity(context, mapping):
979 def showverbosity(context, mapping):
976 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
980 """String. The current output verbosity in 'debug', 'quiet', 'verbose',
977 or ''."""
981 or ''."""
978 ui = context.resource(mapping, 'ui')
982 ui = context.resource(mapping, 'ui')
979 # see logcmdutil.changesettemplater for priority of these flags
983 # see logcmdutil.changesettemplater for priority of these flags
980 if ui.debugflag:
984 if ui.debugflag:
981 return 'debug'
985 return 'debug'
982 elif ui.quiet:
986 elif ui.quiet:
983 return 'quiet'
987 return 'quiet'
984 elif ui.verbose:
988 elif ui.verbose:
985 return 'verbose'
989 return 'verbose'
986 return ''
990 return ''
987
991
988 def loadkeyword(ui, extname, registrarobj):
992 def loadkeyword(ui, extname, registrarobj):
989 """Load template keyword from specified registrarobj
993 """Load template keyword from specified registrarobj
990 """
994 """
991 for name, func in registrarobj._table.iteritems():
995 for name, func in registrarobj._table.iteritems():
992 keywords[name] = func
996 keywords[name] = func
993
997
994 # tell hggettext to extract docstrings from these functions:
998 # tell hggettext to extract docstrings from these functions:
995 i18nfunctions = keywords.values()
999 i18nfunctions = keywords.values()
General Comments 0
You need to be logged in to leave comments. Login now