##// END OF EJS Templates
templater: convert resources to a table of callables for future extension...
Yuya Nishihara -
r36997:255f635c default
parent child Browse files
Show More
@@ -1,554 +1,563
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 templateutil,
127 templateutil,
128 util,
128 util,
129 )
129 )
130 from .utils import dateutil
130 from .utils import dateutil
131
131
132 pickle = util.pickle
132 pickle = util.pickle
133
133
134 class _nullconverter(object):
134 class _nullconverter(object):
135 '''convert non-primitive data types to be processed by formatter'''
135 '''convert non-primitive data types to be processed by formatter'''
136
136
137 # set to True if context object should be stored as item
137 # set to True if context object should be stored as item
138 storecontext = False
138 storecontext = False
139
139
140 @staticmethod
140 @staticmethod
141 def formatdate(date, fmt):
141 def formatdate(date, fmt):
142 '''convert date tuple to appropriate format'''
142 '''convert date tuple to appropriate format'''
143 return date
143 return date
144 @staticmethod
144 @staticmethod
145 def formatdict(data, key, value, fmt, sep):
145 def formatdict(data, key, value, fmt, sep):
146 '''convert dict or key-value pairs to appropriate dict format'''
146 '''convert dict or key-value pairs to appropriate dict format'''
147 # use plain dict instead of util.sortdict so that data can be
147 # use plain dict instead of util.sortdict so that data can be
148 # serialized as a builtin dict in pickle output
148 # serialized as a builtin dict in pickle output
149 return dict(data)
149 return dict(data)
150 @staticmethod
150 @staticmethod
151 def formatlist(data, name, fmt, sep):
151 def formatlist(data, name, fmt, sep):
152 '''convert iterable to appropriate list format'''
152 '''convert iterable to appropriate list format'''
153 return list(data)
153 return list(data)
154
154
155 class baseformatter(object):
155 class baseformatter(object):
156 def __init__(self, ui, topic, opts, converter):
156 def __init__(self, ui, topic, opts, converter):
157 self._ui = ui
157 self._ui = ui
158 self._topic = topic
158 self._topic = topic
159 self._style = opts.get("style")
159 self._style = opts.get("style")
160 self._template = opts.get("template")
160 self._template = opts.get("template")
161 self._converter = converter
161 self._converter = converter
162 self._item = None
162 self._item = None
163 # function to convert node to string suitable for this output
163 # function to convert node to string suitable for this output
164 self.hexfunc = hex
164 self.hexfunc = hex
165 def __enter__(self):
165 def __enter__(self):
166 return self
166 return self
167 def __exit__(self, exctype, excvalue, traceback):
167 def __exit__(self, exctype, excvalue, traceback):
168 if exctype is None:
168 if exctype is None:
169 self.end()
169 self.end()
170 def _showitem(self):
170 def _showitem(self):
171 '''show a formatted item once all data is collected'''
171 '''show a formatted item once all data is collected'''
172 def startitem(self):
172 def startitem(self):
173 '''begin an item in the format list'''
173 '''begin an item in the format list'''
174 if self._item is not None:
174 if self._item is not None:
175 self._showitem()
175 self._showitem()
176 self._item = {}
176 self._item = {}
177 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
177 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
178 '''convert date tuple to appropriate format'''
178 '''convert date tuple to appropriate format'''
179 return self._converter.formatdate(date, fmt)
179 return self._converter.formatdate(date, fmt)
180 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
180 def formatdict(self, data, key='key', value='value', fmt=None, sep=' '):
181 '''convert dict or key-value pairs to appropriate dict format'''
181 '''convert dict or key-value pairs to appropriate dict format'''
182 return self._converter.formatdict(data, key, value, fmt, sep)
182 return self._converter.formatdict(data, key, value, fmt, sep)
183 def formatlist(self, data, name, fmt=None, sep=' '):
183 def formatlist(self, data, name, fmt=None, sep=' '):
184 '''convert iterable to appropriate list format'''
184 '''convert iterable to appropriate list format'''
185 # name is mandatory argument for now, but it could be optional if
185 # name is mandatory argument for now, but it could be optional if
186 # we have default template keyword, e.g. {item}
186 # we have default template keyword, e.g. {item}
187 return self._converter.formatlist(data, name, fmt, sep)
187 return self._converter.formatlist(data, name, fmt, sep)
188 def context(self, **ctxs):
188 def context(self, **ctxs):
189 '''insert context objects to be used to render template keywords'''
189 '''insert context objects to be used to render template keywords'''
190 ctxs = pycompat.byteskwargs(ctxs)
190 ctxs = pycompat.byteskwargs(ctxs)
191 assert all(k == 'ctx' for k in ctxs)
191 assert all(k == 'ctx' for k in ctxs)
192 if self._converter.storecontext:
192 if self._converter.storecontext:
193 self._item.update(ctxs)
193 self._item.update(ctxs)
194 def data(self, **data):
194 def data(self, **data):
195 '''insert data into item that's not shown in default output'''
195 '''insert data into item that's not shown in default output'''
196 data = pycompat.byteskwargs(data)
196 data = pycompat.byteskwargs(data)
197 self._item.update(data)
197 self._item.update(data)
198 def write(self, fields, deftext, *fielddata, **opts):
198 def write(self, fields, deftext, *fielddata, **opts):
199 '''do default text output while assigning data to item'''
199 '''do default text output while assigning data to item'''
200 fieldkeys = fields.split()
200 fieldkeys = fields.split()
201 assert len(fieldkeys) == len(fielddata)
201 assert len(fieldkeys) == len(fielddata)
202 self._item.update(zip(fieldkeys, fielddata))
202 self._item.update(zip(fieldkeys, fielddata))
203 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
203 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
204 '''do conditional write (primarily for plain formatter)'''
204 '''do conditional write (primarily for plain formatter)'''
205 fieldkeys = fields.split()
205 fieldkeys = fields.split()
206 assert len(fieldkeys) == len(fielddata)
206 assert len(fieldkeys) == len(fielddata)
207 self._item.update(zip(fieldkeys, fielddata))
207 self._item.update(zip(fieldkeys, fielddata))
208 def plain(self, text, **opts):
208 def plain(self, text, **opts):
209 '''show raw text for non-templated mode'''
209 '''show raw text for non-templated mode'''
210 def isplain(self):
210 def isplain(self):
211 '''check for plain formatter usage'''
211 '''check for plain formatter usage'''
212 return False
212 return False
213 def nested(self, field):
213 def nested(self, field):
214 '''sub formatter to store nested data in the specified field'''
214 '''sub formatter to store nested data in the specified field'''
215 self._item[field] = data = []
215 self._item[field] = data = []
216 return _nestedformatter(self._ui, self._converter, data)
216 return _nestedformatter(self._ui, self._converter, data)
217 def end(self):
217 def end(self):
218 '''end output for the formatter'''
218 '''end output for the formatter'''
219 if self._item is not None:
219 if self._item is not None:
220 self._showitem()
220 self._showitem()
221
221
222 def nullformatter(ui, topic):
222 def nullformatter(ui, topic):
223 '''formatter that prints nothing'''
223 '''formatter that prints nothing'''
224 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
224 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
225
225
226 class _nestedformatter(baseformatter):
226 class _nestedformatter(baseformatter):
227 '''build sub items and store them in the parent formatter'''
227 '''build sub items and store them in the parent formatter'''
228 def __init__(self, ui, converter, data):
228 def __init__(self, ui, converter, data):
229 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
229 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
230 self._data = data
230 self._data = data
231 def _showitem(self):
231 def _showitem(self):
232 self._data.append(self._item)
232 self._data.append(self._item)
233
233
234 def _iteritems(data):
234 def _iteritems(data):
235 '''iterate key-value pairs in stable order'''
235 '''iterate key-value pairs in stable order'''
236 if isinstance(data, dict):
236 if isinstance(data, dict):
237 return sorted(data.iteritems())
237 return sorted(data.iteritems())
238 return data
238 return data
239
239
240 class _plainconverter(object):
240 class _plainconverter(object):
241 '''convert non-primitive data types to text'''
241 '''convert non-primitive data types to text'''
242
242
243 storecontext = False
243 storecontext = False
244
244
245 @staticmethod
245 @staticmethod
246 def formatdate(date, fmt):
246 def formatdate(date, fmt):
247 '''stringify date tuple in the given format'''
247 '''stringify date tuple in the given format'''
248 return dateutil.datestr(date, fmt)
248 return dateutil.datestr(date, fmt)
249 @staticmethod
249 @staticmethod
250 def formatdict(data, key, value, fmt, sep):
250 def formatdict(data, key, value, fmt, sep):
251 '''stringify key-value pairs separated by sep'''
251 '''stringify key-value pairs separated by sep'''
252 prefmt = pycompat.identity
252 prefmt = pycompat.identity
253 if fmt is None:
253 if fmt is None:
254 fmt = '%s=%s'
254 fmt = '%s=%s'
255 prefmt = pycompat.bytestr
255 prefmt = pycompat.bytestr
256 return sep.join(fmt % (prefmt(k), prefmt(v))
256 return sep.join(fmt % (prefmt(k), prefmt(v))
257 for k, v in _iteritems(data))
257 for k, v in _iteritems(data))
258 @staticmethod
258 @staticmethod
259 def formatlist(data, name, fmt, sep):
259 def formatlist(data, name, fmt, sep):
260 '''stringify iterable separated by sep'''
260 '''stringify iterable separated by sep'''
261 prefmt = pycompat.identity
261 prefmt = pycompat.identity
262 if fmt is None:
262 if fmt is None:
263 fmt = '%s'
263 fmt = '%s'
264 prefmt = pycompat.bytestr
264 prefmt = pycompat.bytestr
265 return sep.join(fmt % prefmt(e) for e in data)
265 return sep.join(fmt % prefmt(e) for e in data)
266
266
267 class plainformatter(baseformatter):
267 class plainformatter(baseformatter):
268 '''the default text output scheme'''
268 '''the default text output scheme'''
269 def __init__(self, ui, out, topic, opts):
269 def __init__(self, ui, out, topic, opts):
270 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
270 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
271 if ui.debugflag:
271 if ui.debugflag:
272 self.hexfunc = hex
272 self.hexfunc = hex
273 else:
273 else:
274 self.hexfunc = short
274 self.hexfunc = short
275 if ui is out:
275 if ui is out:
276 self._write = ui.write
276 self._write = ui.write
277 else:
277 else:
278 self._write = lambda s, **opts: out.write(s)
278 self._write = lambda s, **opts: out.write(s)
279 def startitem(self):
279 def startitem(self):
280 pass
280 pass
281 def data(self, **data):
281 def data(self, **data):
282 pass
282 pass
283 def write(self, fields, deftext, *fielddata, **opts):
283 def write(self, fields, deftext, *fielddata, **opts):
284 self._write(deftext % fielddata, **opts)
284 self._write(deftext % fielddata, **opts)
285 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
285 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
286 '''do conditional write'''
286 '''do conditional write'''
287 if cond:
287 if cond:
288 self._write(deftext % fielddata, **opts)
288 self._write(deftext % fielddata, **opts)
289 def plain(self, text, **opts):
289 def plain(self, text, **opts):
290 self._write(text, **opts)
290 self._write(text, **opts)
291 def isplain(self):
291 def isplain(self):
292 return True
292 return True
293 def nested(self, field):
293 def nested(self, field):
294 # nested data will be directly written to ui
294 # nested data will be directly written to ui
295 return self
295 return self
296 def end(self):
296 def end(self):
297 pass
297 pass
298
298
299 class debugformatter(baseformatter):
299 class debugformatter(baseformatter):
300 def __init__(self, ui, out, topic, opts):
300 def __init__(self, ui, out, topic, opts):
301 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
301 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
302 self._out = out
302 self._out = out
303 self._out.write("%s = [\n" % self._topic)
303 self._out.write("%s = [\n" % self._topic)
304 def _showitem(self):
304 def _showitem(self):
305 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
305 self._out.write(' %s,\n' % pycompat.byterepr(self._item))
306 def end(self):
306 def end(self):
307 baseformatter.end(self)
307 baseformatter.end(self)
308 self._out.write("]\n")
308 self._out.write("]\n")
309
309
310 class pickleformatter(baseformatter):
310 class pickleformatter(baseformatter):
311 def __init__(self, ui, out, topic, opts):
311 def __init__(self, ui, out, topic, opts):
312 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
312 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
313 self._out = out
313 self._out = out
314 self._data = []
314 self._data = []
315 def _showitem(self):
315 def _showitem(self):
316 self._data.append(self._item)
316 self._data.append(self._item)
317 def end(self):
317 def end(self):
318 baseformatter.end(self)
318 baseformatter.end(self)
319 self._out.write(pickle.dumps(self._data))
319 self._out.write(pickle.dumps(self._data))
320
320
321 class jsonformatter(baseformatter):
321 class jsonformatter(baseformatter):
322 def __init__(self, ui, out, topic, opts):
322 def __init__(self, ui, out, topic, opts):
323 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
323 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
324 self._out = out
324 self._out = out
325 self._out.write("[")
325 self._out.write("[")
326 self._first = True
326 self._first = True
327 def _showitem(self):
327 def _showitem(self):
328 if self._first:
328 if self._first:
329 self._first = False
329 self._first = False
330 else:
330 else:
331 self._out.write(",")
331 self._out.write(",")
332
332
333 self._out.write("\n {\n")
333 self._out.write("\n {\n")
334 first = True
334 first = True
335 for k, v in sorted(self._item.items()):
335 for k, v in sorted(self._item.items()):
336 if first:
336 if first:
337 first = False
337 first = False
338 else:
338 else:
339 self._out.write(",\n")
339 self._out.write(",\n")
340 u = templatefilters.json(v, paranoid=False)
340 u = templatefilters.json(v, paranoid=False)
341 self._out.write(' "%s": %s' % (k, u))
341 self._out.write(' "%s": %s' % (k, u))
342 self._out.write("\n }")
342 self._out.write("\n }")
343 def end(self):
343 def end(self):
344 baseformatter.end(self)
344 baseformatter.end(self)
345 self._out.write("\n]\n")
345 self._out.write("\n]\n")
346
346
347 class _templateconverter(object):
347 class _templateconverter(object):
348 '''convert non-primitive data types to be processed by templater'''
348 '''convert non-primitive data types to be processed by templater'''
349
349
350 storecontext = True
350 storecontext = True
351
351
352 @staticmethod
352 @staticmethod
353 def formatdate(date, fmt):
353 def formatdate(date, fmt):
354 '''return date tuple'''
354 '''return date tuple'''
355 return date
355 return date
356 @staticmethod
356 @staticmethod
357 def formatdict(data, key, value, fmt, sep):
357 def formatdict(data, key, value, fmt, sep):
358 '''build object that can be evaluated as either plain string or dict'''
358 '''build object that can be evaluated as either plain string or dict'''
359 data = util.sortdict(_iteritems(data))
359 data = util.sortdict(_iteritems(data))
360 def f():
360 def f():
361 yield _plainconverter.formatdict(data, key, value, fmt, sep)
361 yield _plainconverter.formatdict(data, key, value, fmt, sep)
362 return templateutil.hybriddict(data, key=key, value=value, fmt=fmt,
362 return templateutil.hybriddict(data, key=key, value=value, fmt=fmt,
363 gen=f)
363 gen=f)
364 @staticmethod
364 @staticmethod
365 def formatlist(data, name, fmt, sep):
365 def formatlist(data, name, fmt, sep):
366 '''build object that can be evaluated as either plain string or list'''
366 '''build object that can be evaluated as either plain string or list'''
367 data = list(data)
367 data = list(data)
368 def f():
368 def f():
369 yield _plainconverter.formatlist(data, name, fmt, sep)
369 yield _plainconverter.formatlist(data, name, fmt, sep)
370 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
370 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
371
371
372 class templateformatter(baseformatter):
372 class templateformatter(baseformatter):
373 def __init__(self, ui, out, topic, opts):
373 def __init__(self, ui, out, topic, opts):
374 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
374 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
375 self._out = out
375 self._out = out
376 spec = lookuptemplate(ui, topic, opts.get('template', ''))
376 spec = lookuptemplate(ui, topic, opts.get('template', ''))
377 self._tref = spec.ref
377 self._tref = spec.ref
378 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
378 self._t = loadtemplater(ui, spec, defaults=templatekw.keywords,
379 resources=templateresources(ui),
379 resources=templateresources(ui),
380 cache=templatekw.defaulttempl)
380 cache=templatekw.defaulttempl)
381 self._parts = templatepartsmap(spec, self._t,
381 self._parts = templatepartsmap(spec, self._t,
382 ['docheader', 'docfooter', 'separator'])
382 ['docheader', 'docfooter', 'separator'])
383 self._counter = itertools.count()
383 self._counter = itertools.count()
384 self._renderitem('docheader', {})
384 self._renderitem('docheader', {})
385
385
386 def _showitem(self):
386 def _showitem(self):
387 item = self._item.copy()
387 item = self._item.copy()
388 item['index'] = index = next(self._counter)
388 item['index'] = index = next(self._counter)
389 if index > 0:
389 if index > 0:
390 self._renderitem('separator', {})
390 self._renderitem('separator', {})
391 self._renderitem(self._tref, item)
391 self._renderitem(self._tref, item)
392
392
393 def _renderitem(self, part, item):
393 def _renderitem(self, part, item):
394 if part not in self._parts:
394 if part not in self._parts:
395 return
395 return
396 ref = self._parts[part]
396 ref = self._parts[part]
397
397
398 # TODO: add support for filectx
398 # TODO: add support for filectx
399 props = {}
399 props = {}
400 # explicitly-defined fields precede templatekw
400 # explicitly-defined fields precede templatekw
401 props.update(item)
401 props.update(item)
402 if 'ctx' in item:
402 if 'ctx' in item:
403 # but template resources must be always available
403 # but template resources must be always available
404 props['repo'] = props['ctx'].repo()
404 props['repo'] = props['ctx'].repo()
405 props['revcache'] = {}
405 props['revcache'] = {}
406 props = pycompat.strkwargs(props)
406 props = pycompat.strkwargs(props)
407 g = self._t(ref, **props)
407 g = self._t(ref, **props)
408 self._out.write(templateutil.stringify(g))
408 self._out.write(templateutil.stringify(g))
409
409
410 def end(self):
410 def end(self):
411 baseformatter.end(self)
411 baseformatter.end(self)
412 self._renderitem('docfooter', {})
412 self._renderitem('docfooter', {})
413
413
414 templatespec = collections.namedtuple(r'templatespec',
414 templatespec = collections.namedtuple(r'templatespec',
415 r'ref tmpl mapfile')
415 r'ref tmpl mapfile')
416
416
417 def lookuptemplate(ui, topic, tmpl):
417 def lookuptemplate(ui, topic, tmpl):
418 """Find the template matching the given -T/--template spec 'tmpl'
418 """Find the template matching the given -T/--template spec 'tmpl'
419
419
420 'tmpl' can be any of the following:
420 'tmpl' can be any of the following:
421
421
422 - a literal template (e.g. '{rev}')
422 - a literal template (e.g. '{rev}')
423 - a map-file name or path (e.g. 'changelog')
423 - a map-file name or path (e.g. 'changelog')
424 - a reference to [templates] in config file
424 - a reference to [templates] in config file
425 - a path to raw template file
425 - a path to raw template file
426
426
427 A map file defines a stand-alone template environment. If a map file
427 A map file defines a stand-alone template environment. If a map file
428 selected, all templates defined in the file will be loaded, and the
428 selected, all templates defined in the file will be loaded, and the
429 template matching the given topic will be rendered. Aliases won't be
429 template matching the given topic will be rendered. Aliases won't be
430 loaded from user config, but from the map file.
430 loaded from user config, but from the map file.
431
431
432 If no map file selected, all templates in [templates] section will be
432 If no map file selected, all templates in [templates] section will be
433 available as well as aliases in [templatealias].
433 available as well as aliases in [templatealias].
434 """
434 """
435
435
436 # looks like a literal template?
436 # looks like a literal template?
437 if '{' in tmpl:
437 if '{' in tmpl:
438 return templatespec('', tmpl, None)
438 return templatespec('', tmpl, None)
439
439
440 # perhaps a stock style?
440 # perhaps a stock style?
441 if not os.path.split(tmpl)[0]:
441 if not os.path.split(tmpl)[0]:
442 mapname = (templater.templatepath('map-cmdline.' + tmpl)
442 mapname = (templater.templatepath('map-cmdline.' + tmpl)
443 or templater.templatepath(tmpl))
443 or templater.templatepath(tmpl))
444 if mapname and os.path.isfile(mapname):
444 if mapname and os.path.isfile(mapname):
445 return templatespec(topic, None, mapname)
445 return templatespec(topic, None, mapname)
446
446
447 # perhaps it's a reference to [templates]
447 # perhaps it's a reference to [templates]
448 if ui.config('templates', tmpl):
448 if ui.config('templates', tmpl):
449 return templatespec(tmpl, None, None)
449 return templatespec(tmpl, None, None)
450
450
451 if tmpl == 'list':
451 if tmpl == 'list':
452 ui.write(_("available styles: %s\n") % templater.stylelist())
452 ui.write(_("available styles: %s\n") % templater.stylelist())
453 raise error.Abort(_("specify a template"))
453 raise error.Abort(_("specify a template"))
454
454
455 # perhaps it's a path to a map or a template
455 # perhaps it's a path to a map or a template
456 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
456 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
457 # is it a mapfile for a style?
457 # is it a mapfile for a style?
458 if os.path.basename(tmpl).startswith("map-"):
458 if os.path.basename(tmpl).startswith("map-"):
459 return templatespec(topic, None, os.path.realpath(tmpl))
459 return templatespec(topic, None, os.path.realpath(tmpl))
460 with util.posixfile(tmpl, 'rb') as f:
460 with util.posixfile(tmpl, 'rb') as f:
461 tmpl = f.read()
461 tmpl = f.read()
462 return templatespec('', tmpl, None)
462 return templatespec('', tmpl, None)
463
463
464 # constant string?
464 # constant string?
465 return templatespec('', tmpl, None)
465 return templatespec('', tmpl, None)
466
466
467 def templatepartsmap(spec, t, partnames):
467 def templatepartsmap(spec, t, partnames):
468 """Create a mapping of {part: ref}"""
468 """Create a mapping of {part: ref}"""
469 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
469 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
470 if spec.mapfile:
470 if spec.mapfile:
471 partsmap.update((p, p) for p in partnames if p in t)
471 partsmap.update((p, p) for p in partnames if p in t)
472 elif spec.ref:
472 elif spec.ref:
473 for part in partnames:
473 for part in partnames:
474 ref = '%s:%s' % (spec.ref, part) # select config sub-section
474 ref = '%s:%s' % (spec.ref, part) # select config sub-section
475 if ref in t:
475 if ref in t:
476 partsmap[part] = ref
476 partsmap[part] = ref
477 return partsmap
477 return partsmap
478
478
479 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
479 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
480 """Create a templater from either a literal template or loading from
480 """Create a templater from either a literal template or loading from
481 a map file"""
481 a map file"""
482 assert not (spec.tmpl and spec.mapfile)
482 assert not (spec.tmpl and spec.mapfile)
483 if spec.mapfile:
483 if spec.mapfile:
484 frommapfile = templater.templater.frommapfile
484 frommapfile = templater.templater.frommapfile
485 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
485 return frommapfile(spec.mapfile, defaults=defaults, resources=resources,
486 cache=cache)
486 cache=cache)
487 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
487 return maketemplater(ui, spec.tmpl, defaults=defaults, resources=resources,
488 cache=cache)
488 cache=cache)
489
489
490 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
490 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
491 """Create a templater from a string template 'tmpl'"""
491 """Create a templater from a string template 'tmpl'"""
492 aliases = ui.configitems('templatealias')
492 aliases = ui.configitems('templatealias')
493 t = templater.templater(defaults=defaults, resources=resources,
493 t = templater.templater(defaults=defaults, resources=resources,
494 cache=cache, aliases=aliases)
494 cache=cache, aliases=aliases)
495 t.cache.update((k, templater.unquotestring(v))
495 t.cache.update((k, templater.unquotestring(v))
496 for k, v in ui.configitems('templates'))
496 for k, v in ui.configitems('templates'))
497 if tmpl:
497 if tmpl:
498 t.cache[''] = tmpl
498 t.cache[''] = tmpl
499 return t
499 return t
500
500
501 def templateresources(ui, repo=None):
501 def templateresources(ui, repo=None):
502 """Create a dict of template resources designed for the default templatekw
502 """Create a dict of template resources designed for the default templatekw
503 and function"""
503 and function"""
504 return {
504 resmap = {
505 'cache': {}, # for templatekw/funcs to store reusable data
505 'cache': {}, # for templatekw/funcs to store reusable data
506 'ctx': None,
507 'repo': repo,
506 'repo': repo,
508 'revcache': None, # per-ctx cache; set later
509 'ui': ui,
507 'ui': ui,
510 }
508 }
511
509
510 def getsome(context, mapping, key):
511 return resmap.get(key)
512
513 return {
514 'cache': getsome,
515 'ctx': getsome,
516 'repo': getsome,
517 'revcache': getsome, # per-ctx cache; set later
518 'ui': getsome,
519 }
520
512 def formatter(ui, out, topic, opts):
521 def formatter(ui, out, topic, opts):
513 template = opts.get("template", "")
522 template = opts.get("template", "")
514 if template == "json":
523 if template == "json":
515 return jsonformatter(ui, out, topic, opts)
524 return jsonformatter(ui, out, topic, opts)
516 elif template == "pickle":
525 elif template == "pickle":
517 return pickleformatter(ui, out, topic, opts)
526 return pickleformatter(ui, out, topic, opts)
518 elif template == "debug":
527 elif template == "debug":
519 return debugformatter(ui, out, topic, opts)
528 return debugformatter(ui, out, topic, opts)
520 elif template != "":
529 elif template != "":
521 return templateformatter(ui, out, topic, opts)
530 return templateformatter(ui, out, topic, opts)
522 # developer config: ui.formatdebug
531 # developer config: ui.formatdebug
523 elif ui.configbool('ui', 'formatdebug'):
532 elif ui.configbool('ui', 'formatdebug'):
524 return debugformatter(ui, out, topic, opts)
533 return debugformatter(ui, out, topic, opts)
525 # deprecated config: ui.formatjson
534 # deprecated config: ui.formatjson
526 elif ui.configbool('ui', 'formatjson'):
535 elif ui.configbool('ui', 'formatjson'):
527 return jsonformatter(ui, out, topic, opts)
536 return jsonformatter(ui, out, topic, opts)
528 return plainformatter(ui, out, topic, opts)
537 return plainformatter(ui, out, topic, opts)
529
538
530 @contextlib.contextmanager
539 @contextlib.contextmanager
531 def openformatter(ui, filename, topic, opts):
540 def openformatter(ui, filename, topic, opts):
532 """Create a formatter that writes outputs to the specified file
541 """Create a formatter that writes outputs to the specified file
533
542
534 Must be invoked using the 'with' statement.
543 Must be invoked using the 'with' statement.
535 """
544 """
536 with util.posixfile(filename, 'wb') as out:
545 with util.posixfile(filename, 'wb') as out:
537 with formatter(ui, out, topic, opts) as fm:
546 with formatter(ui, out, topic, opts) as fm:
538 yield fm
547 yield fm
539
548
540 @contextlib.contextmanager
549 @contextlib.contextmanager
541 def _neverending(fm):
550 def _neverending(fm):
542 yield fm
551 yield fm
543
552
544 def maybereopen(fm, filename, opts):
553 def maybereopen(fm, filename, opts):
545 """Create a formatter backed by file if filename specified, else return
554 """Create a formatter backed by file if filename specified, else return
546 the given formatter
555 the given formatter
547
556
548 Must be invoked using the 'with' statement. This will never call fm.end()
557 Must be invoked using the 'with' statement. This will never call fm.end()
549 of the given formatter.
558 of the given formatter.
550 """
559 """
551 if filename:
560 if filename:
552 return openformatter(fm._ui, filename, fm._topic, opts)
561 return openformatter(fm._ui, filename, fm._topic, opts)
553 else:
562 else:
554 return _neverending(fm)
563 return _neverending(fm)
@@ -1,941 +1,942
1 # logcmdutil.py - utility for log-like commands
1 # logcmdutil.py - utility for log-like commands
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 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 import itertools
10 import itertools
11 import os
11 import os
12
12
13 from .i18n import _
13 from .i18n import _
14 from .node import (
14 from .node import (
15 hex,
15 hex,
16 nullid,
16 nullid,
17 )
17 )
18
18
19 from . import (
19 from . import (
20 dagop,
20 dagop,
21 encoding,
21 encoding,
22 error,
22 error,
23 formatter,
23 formatter,
24 graphmod,
24 graphmod,
25 match as matchmod,
25 match as matchmod,
26 mdiff,
26 mdiff,
27 patch,
27 patch,
28 pathutil,
28 pathutil,
29 pycompat,
29 pycompat,
30 revset,
30 revset,
31 revsetlang,
31 revsetlang,
32 scmutil,
32 scmutil,
33 smartset,
33 smartset,
34 templatekw,
34 templatekw,
35 templater,
35 templater,
36 templateutil,
36 templateutil,
37 util,
37 util,
38 )
38 )
39 from .utils import dateutil
39 from .utils import dateutil
40
40
41 def getlimit(opts):
41 def getlimit(opts):
42 """get the log limit according to option -l/--limit"""
42 """get the log limit according to option -l/--limit"""
43 limit = opts.get('limit')
43 limit = opts.get('limit')
44 if limit:
44 if limit:
45 try:
45 try:
46 limit = int(limit)
46 limit = int(limit)
47 except ValueError:
47 except ValueError:
48 raise error.Abort(_('limit must be a positive integer'))
48 raise error.Abort(_('limit must be a positive integer'))
49 if limit <= 0:
49 if limit <= 0:
50 raise error.Abort(_('limit must be positive'))
50 raise error.Abort(_('limit must be positive'))
51 else:
51 else:
52 limit = None
52 limit = None
53 return limit
53 return limit
54
54
55 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
55 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
56 changes=None, stat=False, fp=None, prefix='',
56 changes=None, stat=False, fp=None, prefix='',
57 root='', listsubrepos=False, hunksfilterfn=None):
57 root='', listsubrepos=False, hunksfilterfn=None):
58 '''show diff or diffstat.'''
58 '''show diff or diffstat.'''
59 if root:
59 if root:
60 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
60 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
61 else:
61 else:
62 relroot = ''
62 relroot = ''
63 if relroot != '':
63 if relroot != '':
64 # XXX relative roots currently don't work if the root is within a
64 # XXX relative roots currently don't work if the root is within a
65 # subrepo
65 # subrepo
66 uirelroot = match.uipath(relroot)
66 uirelroot = match.uipath(relroot)
67 relroot += '/'
67 relroot += '/'
68 for matchroot in match.files():
68 for matchroot in match.files():
69 if not matchroot.startswith(relroot):
69 if not matchroot.startswith(relroot):
70 ui.warn(_('warning: %s not inside relative root %s\n') % (
70 ui.warn(_('warning: %s not inside relative root %s\n') % (
71 match.uipath(matchroot), uirelroot))
71 match.uipath(matchroot), uirelroot))
72
72
73 if stat:
73 if stat:
74 diffopts = diffopts.copy(context=0, noprefix=False)
74 diffopts = diffopts.copy(context=0, noprefix=False)
75 width = 80
75 width = 80
76 if not ui.plain():
76 if not ui.plain():
77 width = ui.termwidth()
77 width = ui.termwidth()
78
78
79 chunks = patch.diff(repo, node1, node2, match, changes, opts=diffopts,
79 chunks = patch.diff(repo, node1, node2, match, changes, opts=diffopts,
80 prefix=prefix, relroot=relroot,
80 prefix=prefix, relroot=relroot,
81 hunksfilterfn=hunksfilterfn)
81 hunksfilterfn=hunksfilterfn)
82
82
83 if fp is not None or ui.canwritewithoutlabels():
83 if fp is not None or ui.canwritewithoutlabels():
84 out = fp or ui
84 out = fp or ui
85 if stat:
85 if stat:
86 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
86 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
87 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
87 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
88 out.write(chunk)
88 out.write(chunk)
89 else:
89 else:
90 if stat:
90 if stat:
91 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
91 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
92 else:
92 else:
93 chunks = patch.difflabel(lambda chunks, **kwargs: chunks, chunks,
93 chunks = patch.difflabel(lambda chunks, **kwargs: chunks, chunks,
94 opts=diffopts)
94 opts=diffopts)
95 if ui.canbatchlabeledwrites():
95 if ui.canbatchlabeledwrites():
96 def gen():
96 def gen():
97 for chunk, label in chunks:
97 for chunk, label in chunks:
98 yield ui.label(chunk, label=label)
98 yield ui.label(chunk, label=label)
99 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
99 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
100 ui.write(chunk)
100 ui.write(chunk)
101 else:
101 else:
102 for chunk, label in chunks:
102 for chunk, label in chunks:
103 ui.write(chunk, label=label)
103 ui.write(chunk, label=label)
104
104
105 if listsubrepos:
105 if listsubrepos:
106 ctx1 = repo[node1]
106 ctx1 = repo[node1]
107 ctx2 = repo[node2]
107 ctx2 = repo[node2]
108 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
108 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
109 tempnode2 = node2
109 tempnode2 = node2
110 try:
110 try:
111 if node2 is not None:
111 if node2 is not None:
112 tempnode2 = ctx2.substate[subpath][1]
112 tempnode2 = ctx2.substate[subpath][1]
113 except KeyError:
113 except KeyError:
114 # A subrepo that existed in node1 was deleted between node1 and
114 # A subrepo that existed in node1 was deleted between node1 and
115 # node2 (inclusive). Thus, ctx2's substate won't contain that
115 # node2 (inclusive). Thus, ctx2's substate won't contain that
116 # subpath. The best we can do is to ignore it.
116 # subpath. The best we can do is to ignore it.
117 tempnode2 = None
117 tempnode2 = None
118 submatch = matchmod.subdirmatcher(subpath, match)
118 submatch = matchmod.subdirmatcher(subpath, match)
119 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
119 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
120 stat=stat, fp=fp, prefix=prefix)
120 stat=stat, fp=fp, prefix=prefix)
121
121
122 class changesetdiffer(object):
122 class changesetdiffer(object):
123 """Generate diff of changeset with pre-configured filtering functions"""
123 """Generate diff of changeset with pre-configured filtering functions"""
124
124
125 def _makefilematcher(self, ctx):
125 def _makefilematcher(self, ctx):
126 return scmutil.matchall(ctx.repo())
126 return scmutil.matchall(ctx.repo())
127
127
128 def _makehunksfilter(self, ctx):
128 def _makehunksfilter(self, ctx):
129 return None
129 return None
130
130
131 def showdiff(self, ui, ctx, diffopts, stat=False):
131 def showdiff(self, ui, ctx, diffopts, stat=False):
132 repo = ctx.repo()
132 repo = ctx.repo()
133 node = ctx.node()
133 node = ctx.node()
134 prev = ctx.p1().node()
134 prev = ctx.p1().node()
135 diffordiffstat(ui, repo, diffopts, prev, node,
135 diffordiffstat(ui, repo, diffopts, prev, node,
136 match=self._makefilematcher(ctx), stat=stat,
136 match=self._makefilematcher(ctx), stat=stat,
137 hunksfilterfn=self._makehunksfilter(ctx))
137 hunksfilterfn=self._makehunksfilter(ctx))
138
138
139 def changesetlabels(ctx):
139 def changesetlabels(ctx):
140 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
140 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
141 if ctx.obsolete():
141 if ctx.obsolete():
142 labels.append('changeset.obsolete')
142 labels.append('changeset.obsolete')
143 if ctx.isunstable():
143 if ctx.isunstable():
144 labels.append('changeset.unstable')
144 labels.append('changeset.unstable')
145 for instability in ctx.instabilities():
145 for instability in ctx.instabilities():
146 labels.append('instability.%s' % instability)
146 labels.append('instability.%s' % instability)
147 return ' '.join(labels)
147 return ' '.join(labels)
148
148
149 class changesetprinter(object):
149 class changesetprinter(object):
150 '''show changeset information when templating not requested.'''
150 '''show changeset information when templating not requested.'''
151
151
152 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
152 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
153 self.ui = ui
153 self.ui = ui
154 self.repo = repo
154 self.repo = repo
155 self.buffered = buffered
155 self.buffered = buffered
156 self._differ = differ or changesetdiffer()
156 self._differ = differ or changesetdiffer()
157 self.diffopts = diffopts or {}
157 self.diffopts = diffopts or {}
158 self.header = {}
158 self.header = {}
159 self.hunk = {}
159 self.hunk = {}
160 self.lastheader = None
160 self.lastheader = None
161 self.footer = None
161 self.footer = None
162 self._columns = templatekw.getlogcolumns()
162 self._columns = templatekw.getlogcolumns()
163
163
164 def flush(self, ctx):
164 def flush(self, ctx):
165 rev = ctx.rev()
165 rev = ctx.rev()
166 if rev in self.header:
166 if rev in self.header:
167 h = self.header[rev]
167 h = self.header[rev]
168 if h != self.lastheader:
168 if h != self.lastheader:
169 self.lastheader = h
169 self.lastheader = h
170 self.ui.write(h)
170 self.ui.write(h)
171 del self.header[rev]
171 del self.header[rev]
172 if rev in self.hunk:
172 if rev in self.hunk:
173 self.ui.write(self.hunk[rev])
173 self.ui.write(self.hunk[rev])
174 del self.hunk[rev]
174 del self.hunk[rev]
175
175
176 def close(self):
176 def close(self):
177 if self.footer:
177 if self.footer:
178 self.ui.write(self.footer)
178 self.ui.write(self.footer)
179
179
180 def show(self, ctx, copies=None, **props):
180 def show(self, ctx, copies=None, **props):
181 props = pycompat.byteskwargs(props)
181 props = pycompat.byteskwargs(props)
182 if self.buffered:
182 if self.buffered:
183 self.ui.pushbuffer(labeled=True)
183 self.ui.pushbuffer(labeled=True)
184 self._show(ctx, copies, props)
184 self._show(ctx, copies, props)
185 self.hunk[ctx.rev()] = self.ui.popbuffer()
185 self.hunk[ctx.rev()] = self.ui.popbuffer()
186 else:
186 else:
187 self._show(ctx, copies, props)
187 self._show(ctx, copies, props)
188
188
189 def _show(self, ctx, copies, props):
189 def _show(self, ctx, copies, props):
190 '''show a single changeset or file revision'''
190 '''show a single changeset or file revision'''
191 changenode = ctx.node()
191 changenode = ctx.node()
192 rev = ctx.rev()
192 rev = ctx.rev()
193
193
194 if self.ui.quiet:
194 if self.ui.quiet:
195 self.ui.write("%s\n" % scmutil.formatchangeid(ctx),
195 self.ui.write("%s\n" % scmutil.formatchangeid(ctx),
196 label='log.node')
196 label='log.node')
197 return
197 return
198
198
199 columns = self._columns
199 columns = self._columns
200 self.ui.write(columns['changeset'] % scmutil.formatchangeid(ctx),
200 self.ui.write(columns['changeset'] % scmutil.formatchangeid(ctx),
201 label=changesetlabels(ctx))
201 label=changesetlabels(ctx))
202
202
203 # branches are shown first before any other names due to backwards
203 # branches are shown first before any other names due to backwards
204 # compatibility
204 # compatibility
205 branch = ctx.branch()
205 branch = ctx.branch()
206 # don't show the default branch name
206 # don't show the default branch name
207 if branch != 'default':
207 if branch != 'default':
208 self.ui.write(columns['branch'] % branch, label='log.branch')
208 self.ui.write(columns['branch'] % branch, label='log.branch')
209
209
210 for nsname, ns in self.repo.names.iteritems():
210 for nsname, ns in self.repo.names.iteritems():
211 # branches has special logic already handled above, so here we just
211 # branches has special logic already handled above, so here we just
212 # skip it
212 # skip it
213 if nsname == 'branches':
213 if nsname == 'branches':
214 continue
214 continue
215 # we will use the templatename as the color name since those two
215 # we will use the templatename as the color name since those two
216 # should be the same
216 # should be the same
217 for name in ns.names(self.repo, changenode):
217 for name in ns.names(self.repo, changenode):
218 self.ui.write(ns.logfmt % name,
218 self.ui.write(ns.logfmt % name,
219 label='log.%s' % ns.colorname)
219 label='log.%s' % ns.colorname)
220 if self.ui.debugflag:
220 if self.ui.debugflag:
221 self.ui.write(columns['phase'] % ctx.phasestr(), label='log.phase')
221 self.ui.write(columns['phase'] % ctx.phasestr(), label='log.phase')
222 for pctx in scmutil.meaningfulparents(self.repo, ctx):
222 for pctx in scmutil.meaningfulparents(self.repo, ctx):
223 label = 'log.parent changeset.%s' % pctx.phasestr()
223 label = 'log.parent changeset.%s' % pctx.phasestr()
224 self.ui.write(columns['parent'] % scmutil.formatchangeid(pctx),
224 self.ui.write(columns['parent'] % scmutil.formatchangeid(pctx),
225 label=label)
225 label=label)
226
226
227 if self.ui.debugflag and rev is not None:
227 if self.ui.debugflag and rev is not None:
228 mnode = ctx.manifestnode()
228 mnode = ctx.manifestnode()
229 mrev = self.repo.manifestlog._revlog.rev(mnode)
229 mrev = self.repo.manifestlog._revlog.rev(mnode)
230 self.ui.write(columns['manifest']
230 self.ui.write(columns['manifest']
231 % scmutil.formatrevnode(self.ui, mrev, mnode),
231 % scmutil.formatrevnode(self.ui, mrev, mnode),
232 label='ui.debug log.manifest')
232 label='ui.debug log.manifest')
233 self.ui.write(columns['user'] % ctx.user(), label='log.user')
233 self.ui.write(columns['user'] % ctx.user(), label='log.user')
234 self.ui.write(columns['date'] % dateutil.datestr(ctx.date()),
234 self.ui.write(columns['date'] % dateutil.datestr(ctx.date()),
235 label='log.date')
235 label='log.date')
236
236
237 if ctx.isunstable():
237 if ctx.isunstable():
238 instabilities = ctx.instabilities()
238 instabilities = ctx.instabilities()
239 self.ui.write(columns['instability'] % ', '.join(instabilities),
239 self.ui.write(columns['instability'] % ', '.join(instabilities),
240 label='log.instability')
240 label='log.instability')
241
241
242 elif ctx.obsolete():
242 elif ctx.obsolete():
243 self._showobsfate(ctx)
243 self._showobsfate(ctx)
244
244
245 self._exthook(ctx)
245 self._exthook(ctx)
246
246
247 if self.ui.debugflag:
247 if self.ui.debugflag:
248 files = ctx.p1().status(ctx)[:3]
248 files = ctx.p1().status(ctx)[:3]
249 for key, value in zip(['files', 'files+', 'files-'], files):
249 for key, value in zip(['files', 'files+', 'files-'], files):
250 if value:
250 if value:
251 self.ui.write(columns[key] % " ".join(value),
251 self.ui.write(columns[key] % " ".join(value),
252 label='ui.debug log.files')
252 label='ui.debug log.files')
253 elif ctx.files() and self.ui.verbose:
253 elif ctx.files() and self.ui.verbose:
254 self.ui.write(columns['files'] % " ".join(ctx.files()),
254 self.ui.write(columns['files'] % " ".join(ctx.files()),
255 label='ui.note log.files')
255 label='ui.note log.files')
256 if copies and self.ui.verbose:
256 if copies and self.ui.verbose:
257 copies = ['%s (%s)' % c for c in copies]
257 copies = ['%s (%s)' % c for c in copies]
258 self.ui.write(columns['copies'] % ' '.join(copies),
258 self.ui.write(columns['copies'] % ' '.join(copies),
259 label='ui.note log.copies')
259 label='ui.note log.copies')
260
260
261 extra = ctx.extra()
261 extra = ctx.extra()
262 if extra and self.ui.debugflag:
262 if extra and self.ui.debugflag:
263 for key, value in sorted(extra.items()):
263 for key, value in sorted(extra.items()):
264 self.ui.write(columns['extra'] % (key, util.escapestr(value)),
264 self.ui.write(columns['extra'] % (key, util.escapestr(value)),
265 label='ui.debug log.extra')
265 label='ui.debug log.extra')
266
266
267 description = ctx.description().strip()
267 description = ctx.description().strip()
268 if description:
268 if description:
269 if self.ui.verbose:
269 if self.ui.verbose:
270 self.ui.write(_("description:\n"),
270 self.ui.write(_("description:\n"),
271 label='ui.note log.description')
271 label='ui.note log.description')
272 self.ui.write(description,
272 self.ui.write(description,
273 label='ui.note log.description')
273 label='ui.note log.description')
274 self.ui.write("\n\n")
274 self.ui.write("\n\n")
275 else:
275 else:
276 self.ui.write(columns['summary'] % description.splitlines()[0],
276 self.ui.write(columns['summary'] % description.splitlines()[0],
277 label='log.summary')
277 label='log.summary')
278 self.ui.write("\n")
278 self.ui.write("\n")
279
279
280 self._showpatch(ctx)
280 self._showpatch(ctx)
281
281
282 def _showobsfate(self, ctx):
282 def _showobsfate(self, ctx):
283 # TODO: do not depend on templater
283 # TODO: do not depend on templater
284 tres = formatter.templateresources(self.repo.ui, self.repo)
284 tres = formatter.templateresources(self.repo.ui, self.repo)
285 t = formatter.maketemplater(self.repo.ui, '{join(obsfate, "\n")}',
285 t = formatter.maketemplater(self.repo.ui, '{join(obsfate, "\n")}',
286 defaults=templatekw.keywords,
286 defaults=templatekw.keywords,
287 resources=tres)
287 resources=tres)
288 obsfate = t.render({'ctx': ctx, 'revcache': {}}).splitlines()
288 obsfate = t.render({'ctx': ctx, 'revcache': {}}).splitlines()
289
289
290 if obsfate:
290 if obsfate:
291 for obsfateline in obsfate:
291 for obsfateline in obsfate:
292 self.ui.write(self._columns['obsolete'] % obsfateline,
292 self.ui.write(self._columns['obsolete'] % obsfateline,
293 label='log.obsfate')
293 label='log.obsfate')
294
294
295 def _exthook(self, ctx):
295 def _exthook(self, ctx):
296 '''empty method used by extension as a hook point
296 '''empty method used by extension as a hook point
297 '''
297 '''
298
298
299 def _showpatch(self, ctx):
299 def _showpatch(self, ctx):
300 stat = self.diffopts.get('stat')
300 stat = self.diffopts.get('stat')
301 diff = self.diffopts.get('patch')
301 diff = self.diffopts.get('patch')
302 diffopts = patch.diffallopts(self.ui, self.diffopts)
302 diffopts = patch.diffallopts(self.ui, self.diffopts)
303 if stat:
303 if stat:
304 self._differ.showdiff(self.ui, ctx, diffopts, stat=True)
304 self._differ.showdiff(self.ui, ctx, diffopts, stat=True)
305 if stat and diff:
305 if stat and diff:
306 self.ui.write("\n")
306 self.ui.write("\n")
307 if diff:
307 if diff:
308 self._differ.showdiff(self.ui, ctx, diffopts, stat=False)
308 self._differ.showdiff(self.ui, ctx, diffopts, stat=False)
309 if stat or diff:
309 if stat or diff:
310 self.ui.write("\n")
310 self.ui.write("\n")
311
311
312 class jsonchangeset(changesetprinter):
312 class jsonchangeset(changesetprinter):
313 '''format changeset information.'''
313 '''format changeset information.'''
314
314
315 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
315 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
316 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
316 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
317 self.cache = {}
317 self.cache = {}
318 self._first = True
318 self._first = True
319
319
320 def close(self):
320 def close(self):
321 if not self._first:
321 if not self._first:
322 self.ui.write("\n]\n")
322 self.ui.write("\n]\n")
323 else:
323 else:
324 self.ui.write("[]\n")
324 self.ui.write("[]\n")
325
325
326 def _show(self, ctx, copies, props):
326 def _show(self, ctx, copies, props):
327 '''show a single changeset or file revision'''
327 '''show a single changeset or file revision'''
328 rev = ctx.rev()
328 rev = ctx.rev()
329 if rev is None:
329 if rev is None:
330 jrev = jnode = 'null'
330 jrev = jnode = 'null'
331 else:
331 else:
332 jrev = '%d' % rev
332 jrev = '%d' % rev
333 jnode = '"%s"' % hex(ctx.node())
333 jnode = '"%s"' % hex(ctx.node())
334 j = encoding.jsonescape
334 j = encoding.jsonescape
335
335
336 if self._first:
336 if self._first:
337 self.ui.write("[\n {")
337 self.ui.write("[\n {")
338 self._first = False
338 self._first = False
339 else:
339 else:
340 self.ui.write(",\n {")
340 self.ui.write(",\n {")
341
341
342 if self.ui.quiet:
342 if self.ui.quiet:
343 self.ui.write(('\n "rev": %s') % jrev)
343 self.ui.write(('\n "rev": %s') % jrev)
344 self.ui.write((',\n "node": %s') % jnode)
344 self.ui.write((',\n "node": %s') % jnode)
345 self.ui.write('\n }')
345 self.ui.write('\n }')
346 return
346 return
347
347
348 self.ui.write(('\n "rev": %s') % jrev)
348 self.ui.write(('\n "rev": %s') % jrev)
349 self.ui.write((',\n "node": %s') % jnode)
349 self.ui.write((',\n "node": %s') % jnode)
350 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
350 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
351 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
351 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
352 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
352 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
353 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
353 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
354 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
354 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
355
355
356 self.ui.write((',\n "bookmarks": [%s]') %
356 self.ui.write((',\n "bookmarks": [%s]') %
357 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
357 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
358 self.ui.write((',\n "tags": [%s]') %
358 self.ui.write((',\n "tags": [%s]') %
359 ", ".join('"%s"' % j(t) for t in ctx.tags()))
359 ", ".join('"%s"' % j(t) for t in ctx.tags()))
360 self.ui.write((',\n "parents": [%s]') %
360 self.ui.write((',\n "parents": [%s]') %
361 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
361 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
362
362
363 if self.ui.debugflag:
363 if self.ui.debugflag:
364 if rev is None:
364 if rev is None:
365 jmanifestnode = 'null'
365 jmanifestnode = 'null'
366 else:
366 else:
367 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
367 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
368 self.ui.write((',\n "manifest": %s') % jmanifestnode)
368 self.ui.write((',\n "manifest": %s') % jmanifestnode)
369
369
370 self.ui.write((',\n "extra": {%s}') %
370 self.ui.write((',\n "extra": {%s}') %
371 ", ".join('"%s": "%s"' % (j(k), j(v))
371 ", ".join('"%s": "%s"' % (j(k), j(v))
372 for k, v in ctx.extra().items()))
372 for k, v in ctx.extra().items()))
373
373
374 files = ctx.p1().status(ctx)
374 files = ctx.p1().status(ctx)
375 self.ui.write((',\n "modified": [%s]') %
375 self.ui.write((',\n "modified": [%s]') %
376 ", ".join('"%s"' % j(f) for f in files[0]))
376 ", ".join('"%s"' % j(f) for f in files[0]))
377 self.ui.write((',\n "added": [%s]') %
377 self.ui.write((',\n "added": [%s]') %
378 ", ".join('"%s"' % j(f) for f in files[1]))
378 ", ".join('"%s"' % j(f) for f in files[1]))
379 self.ui.write((',\n "removed": [%s]') %
379 self.ui.write((',\n "removed": [%s]') %
380 ", ".join('"%s"' % j(f) for f in files[2]))
380 ", ".join('"%s"' % j(f) for f in files[2]))
381
381
382 elif self.ui.verbose:
382 elif self.ui.verbose:
383 self.ui.write((',\n "files": [%s]') %
383 self.ui.write((',\n "files": [%s]') %
384 ", ".join('"%s"' % j(f) for f in ctx.files()))
384 ", ".join('"%s"' % j(f) for f in ctx.files()))
385
385
386 if copies:
386 if copies:
387 self.ui.write((',\n "copies": {%s}') %
387 self.ui.write((',\n "copies": {%s}') %
388 ", ".join('"%s": "%s"' % (j(k), j(v))
388 ", ".join('"%s": "%s"' % (j(k), j(v))
389 for k, v in copies))
389 for k, v in copies))
390
390
391 stat = self.diffopts.get('stat')
391 stat = self.diffopts.get('stat')
392 diff = self.diffopts.get('patch')
392 diff = self.diffopts.get('patch')
393 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
393 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
394 if stat:
394 if stat:
395 self.ui.pushbuffer()
395 self.ui.pushbuffer()
396 self._differ.showdiff(self.ui, ctx, diffopts, stat=True)
396 self._differ.showdiff(self.ui, ctx, diffopts, stat=True)
397 self.ui.write((',\n "diffstat": "%s"')
397 self.ui.write((',\n "diffstat": "%s"')
398 % j(self.ui.popbuffer()))
398 % j(self.ui.popbuffer()))
399 if diff:
399 if diff:
400 self.ui.pushbuffer()
400 self.ui.pushbuffer()
401 self._differ.showdiff(self.ui, ctx, diffopts, stat=False)
401 self._differ.showdiff(self.ui, ctx, diffopts, stat=False)
402 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
402 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
403
403
404 self.ui.write("\n }")
404 self.ui.write("\n }")
405
405
406 class changesettemplater(changesetprinter):
406 class changesettemplater(changesetprinter):
407 '''format changeset information.
407 '''format changeset information.
408
408
409 Note: there are a variety of convenience functions to build a
409 Note: there are a variety of convenience functions to build a
410 changesettemplater for common cases. See functions such as:
410 changesettemplater for common cases. See functions such as:
411 maketemplater, changesetdisplayer, buildcommittemplate, or other
411 maketemplater, changesetdisplayer, buildcommittemplate, or other
412 functions that use changesest_templater.
412 functions that use changesest_templater.
413 '''
413 '''
414
414
415 # Arguments before "buffered" used to be positional. Consider not
415 # Arguments before "buffered" used to be positional. Consider not
416 # adding/removing arguments before "buffered" to not break callers.
416 # adding/removing arguments before "buffered" to not break callers.
417 def __init__(self, ui, repo, tmplspec, differ=None, diffopts=None,
417 def __init__(self, ui, repo, tmplspec, differ=None, diffopts=None,
418 buffered=False):
418 buffered=False):
419 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
419 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
420 tres = formatter.templateresources(ui, repo)
420 tres = formatter.templateresources(ui, repo)
421 self.t = formatter.loadtemplater(ui, tmplspec,
421 self.t = formatter.loadtemplater(ui, tmplspec,
422 defaults=templatekw.keywords,
422 defaults=templatekw.keywords,
423 resources=tres,
423 resources=tres,
424 cache=templatekw.defaulttempl)
424 cache=templatekw.defaulttempl)
425 self._counter = itertools.count()
425 self._counter = itertools.count()
426 self.cache = tres['cache'] # shared with _graphnodeformatter()
426 self._getcache = tres['cache'] # shared with _graphnodeformatter()
427
427
428 self._tref = tmplspec.ref
428 self._tref = tmplspec.ref
429 self._parts = {'header': '', 'footer': '',
429 self._parts = {'header': '', 'footer': '',
430 tmplspec.ref: tmplspec.ref,
430 tmplspec.ref: tmplspec.ref,
431 'docheader': '', 'docfooter': '',
431 'docheader': '', 'docfooter': '',
432 'separator': ''}
432 'separator': ''}
433 if tmplspec.mapfile:
433 if tmplspec.mapfile:
434 # find correct templates for current mode, for backward
434 # find correct templates for current mode, for backward
435 # compatibility with 'log -v/-q/--debug' using a mapfile
435 # compatibility with 'log -v/-q/--debug' using a mapfile
436 tmplmodes = [
436 tmplmodes = [
437 (True, ''),
437 (True, ''),
438 (self.ui.verbose, '_verbose'),
438 (self.ui.verbose, '_verbose'),
439 (self.ui.quiet, '_quiet'),
439 (self.ui.quiet, '_quiet'),
440 (self.ui.debugflag, '_debug'),
440 (self.ui.debugflag, '_debug'),
441 ]
441 ]
442 for mode, postfix in tmplmodes:
442 for mode, postfix in tmplmodes:
443 for t in self._parts:
443 for t in self._parts:
444 cur = t + postfix
444 cur = t + postfix
445 if mode and cur in self.t:
445 if mode and cur in self.t:
446 self._parts[t] = cur
446 self._parts[t] = cur
447 else:
447 else:
448 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
448 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
449 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
449 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
450 self._parts.update(m)
450 self._parts.update(m)
451
451
452 if self._parts['docheader']:
452 if self._parts['docheader']:
453 self.ui.write(
453 self.ui.write(
454 templateutil.stringify(self.t(self._parts['docheader'])))
454 templateutil.stringify(self.t(self._parts['docheader'])))
455
455
456 def close(self):
456 def close(self):
457 if self._parts['docfooter']:
457 if self._parts['docfooter']:
458 if not self.footer:
458 if not self.footer:
459 self.footer = ""
459 self.footer = ""
460 self.footer += templateutil.stringify(
460 self.footer += templateutil.stringify(
461 self.t(self._parts['docfooter']))
461 self.t(self._parts['docfooter']))
462 return super(changesettemplater, self).close()
462 return super(changesettemplater, self).close()
463
463
464 def _show(self, ctx, copies, props):
464 def _show(self, ctx, copies, props):
465 '''show a single changeset or file revision'''
465 '''show a single changeset or file revision'''
466 props = props.copy()
466 props = props.copy()
467 props['ctx'] = ctx
467 props['ctx'] = ctx
468 props['index'] = index = next(self._counter)
468 props['index'] = index = next(self._counter)
469 props['revcache'] = {'copies': copies}
469 props['revcache'] = {'copies': copies}
470 props = pycompat.strkwargs(props)
470 props = pycompat.strkwargs(props)
471
471
472 # write separator, which wouldn't work well with the header part below
472 # write separator, which wouldn't work well with the header part below
473 # since there's inherently a conflict between header (across items) and
473 # since there's inherently a conflict between header (across items) and
474 # separator (per item)
474 # separator (per item)
475 if self._parts['separator'] and index > 0:
475 if self._parts['separator'] and index > 0:
476 self.ui.write(
476 self.ui.write(
477 templateutil.stringify(self.t(self._parts['separator'])))
477 templateutil.stringify(self.t(self._parts['separator'])))
478
478
479 # write header
479 # write header
480 if self._parts['header']:
480 if self._parts['header']:
481 h = templateutil.stringify(self.t(self._parts['header'], **props))
481 h = templateutil.stringify(self.t(self._parts['header'], **props))
482 if self.buffered:
482 if self.buffered:
483 self.header[ctx.rev()] = h
483 self.header[ctx.rev()] = h
484 else:
484 else:
485 if self.lastheader != h:
485 if self.lastheader != h:
486 self.lastheader = h
486 self.lastheader = h
487 self.ui.write(h)
487 self.ui.write(h)
488
488
489 # write changeset metadata, then patch if requested
489 # write changeset metadata, then patch if requested
490 key = self._parts[self._tref]
490 key = self._parts[self._tref]
491 self.ui.write(templateutil.stringify(self.t(key, **props)))
491 self.ui.write(templateutil.stringify(self.t(key, **props)))
492 self._showpatch(ctx)
492 self._showpatch(ctx)
493
493
494 if self._parts['footer']:
494 if self._parts['footer']:
495 if not self.footer:
495 if not self.footer:
496 self.footer = templateutil.stringify(
496 self.footer = templateutil.stringify(
497 self.t(self._parts['footer'], **props))
497 self.t(self._parts['footer'], **props))
498
498
499 def templatespec(tmpl, mapfile):
499 def templatespec(tmpl, mapfile):
500 if mapfile:
500 if mapfile:
501 return formatter.templatespec('changeset', tmpl, mapfile)
501 return formatter.templatespec('changeset', tmpl, mapfile)
502 else:
502 else:
503 return formatter.templatespec('', tmpl, None)
503 return formatter.templatespec('', tmpl, None)
504
504
505 def _lookuptemplate(ui, tmpl, style):
505 def _lookuptemplate(ui, tmpl, style):
506 """Find the template matching the given template spec or style
506 """Find the template matching the given template spec or style
507
507
508 See formatter.lookuptemplate() for details.
508 See formatter.lookuptemplate() for details.
509 """
509 """
510
510
511 # ui settings
511 # ui settings
512 if not tmpl and not style: # template are stronger than style
512 if not tmpl and not style: # template are stronger than style
513 tmpl = ui.config('ui', 'logtemplate')
513 tmpl = ui.config('ui', 'logtemplate')
514 if tmpl:
514 if tmpl:
515 return templatespec(templater.unquotestring(tmpl), None)
515 return templatespec(templater.unquotestring(tmpl), None)
516 else:
516 else:
517 style = util.expandpath(ui.config('ui', 'style'))
517 style = util.expandpath(ui.config('ui', 'style'))
518
518
519 if not tmpl and style:
519 if not tmpl and style:
520 mapfile = style
520 mapfile = style
521 if not os.path.split(mapfile)[0]:
521 if not os.path.split(mapfile)[0]:
522 mapname = (templater.templatepath('map-cmdline.' + mapfile)
522 mapname = (templater.templatepath('map-cmdline.' + mapfile)
523 or templater.templatepath(mapfile))
523 or templater.templatepath(mapfile))
524 if mapname:
524 if mapname:
525 mapfile = mapname
525 mapfile = mapname
526 return templatespec(None, mapfile)
526 return templatespec(None, mapfile)
527
527
528 if not tmpl:
528 if not tmpl:
529 return templatespec(None, None)
529 return templatespec(None, None)
530
530
531 return formatter.lookuptemplate(ui, 'changeset', tmpl)
531 return formatter.lookuptemplate(ui, 'changeset', tmpl)
532
532
533 def maketemplater(ui, repo, tmpl, buffered=False):
533 def maketemplater(ui, repo, tmpl, buffered=False):
534 """Create a changesettemplater from a literal template 'tmpl'
534 """Create a changesettemplater from a literal template 'tmpl'
535 byte-string."""
535 byte-string."""
536 spec = templatespec(tmpl, None)
536 spec = templatespec(tmpl, None)
537 return changesettemplater(ui, repo, spec, buffered=buffered)
537 return changesettemplater(ui, repo, spec, buffered=buffered)
538
538
539 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
539 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
540 """show one changeset using template or regular display.
540 """show one changeset using template or regular display.
541
541
542 Display format will be the first non-empty hit of:
542 Display format will be the first non-empty hit of:
543 1. option 'template'
543 1. option 'template'
544 2. option 'style'
544 2. option 'style'
545 3. [ui] setting 'logtemplate'
545 3. [ui] setting 'logtemplate'
546 4. [ui] setting 'style'
546 4. [ui] setting 'style'
547 If all of these values are either the unset or the empty string,
547 If all of these values are either the unset or the empty string,
548 regular display via changesetprinter() is done.
548 regular display via changesetprinter() is done.
549 """
549 """
550 postargs = (differ, opts, buffered)
550 postargs = (differ, opts, buffered)
551 if opts.get('template') == 'json':
551 if opts.get('template') == 'json':
552 return jsonchangeset(ui, repo, *postargs)
552 return jsonchangeset(ui, repo, *postargs)
553
553
554 spec = _lookuptemplate(ui, opts.get('template'), opts.get('style'))
554 spec = _lookuptemplate(ui, opts.get('template'), opts.get('style'))
555
555
556 if not spec.ref and not spec.tmpl and not spec.mapfile:
556 if not spec.ref and not spec.tmpl and not spec.mapfile:
557 return changesetprinter(ui, repo, *postargs)
557 return changesetprinter(ui, repo, *postargs)
558
558
559 return changesettemplater(ui, repo, spec, *postargs)
559 return changesettemplater(ui, repo, spec, *postargs)
560
560
561 def _makematcher(repo, revs, pats, opts):
561 def _makematcher(repo, revs, pats, opts):
562 """Build matcher and expanded patterns from log options
562 """Build matcher and expanded patterns from log options
563
563
564 If --follow, revs are the revisions to follow from.
564 If --follow, revs are the revisions to follow from.
565
565
566 Returns (match, pats, slowpath) where
566 Returns (match, pats, slowpath) where
567 - match: a matcher built from the given pats and -I/-X opts
567 - match: a matcher built from the given pats and -I/-X opts
568 - pats: patterns used (globs are expanded on Windows)
568 - pats: patterns used (globs are expanded on Windows)
569 - slowpath: True if patterns aren't as simple as scanning filelogs
569 - slowpath: True if patterns aren't as simple as scanning filelogs
570 """
570 """
571 # pats/include/exclude are passed to match.match() directly in
571 # pats/include/exclude are passed to match.match() directly in
572 # _matchfiles() revset but walkchangerevs() builds its matcher with
572 # _matchfiles() revset but walkchangerevs() builds its matcher with
573 # scmutil.match(). The difference is input pats are globbed on
573 # scmutil.match(). The difference is input pats are globbed on
574 # platforms without shell expansion (windows).
574 # platforms without shell expansion (windows).
575 wctx = repo[None]
575 wctx = repo[None]
576 match, pats = scmutil.matchandpats(wctx, pats, opts)
576 match, pats = scmutil.matchandpats(wctx, pats, opts)
577 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
577 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
578 if not slowpath:
578 if not slowpath:
579 follow = opts.get('follow') or opts.get('follow_first')
579 follow = opts.get('follow') or opts.get('follow_first')
580 startctxs = []
580 startctxs = []
581 if follow and opts.get('rev'):
581 if follow and opts.get('rev'):
582 startctxs = [repo[r] for r in revs]
582 startctxs = [repo[r] for r in revs]
583 for f in match.files():
583 for f in match.files():
584 if follow and startctxs:
584 if follow and startctxs:
585 # No idea if the path was a directory at that revision, so
585 # No idea if the path was a directory at that revision, so
586 # take the slow path.
586 # take the slow path.
587 if any(f not in c for c in startctxs):
587 if any(f not in c for c in startctxs):
588 slowpath = True
588 slowpath = True
589 continue
589 continue
590 elif follow and f not in wctx:
590 elif follow and f not in wctx:
591 # If the file exists, it may be a directory, so let it
591 # If the file exists, it may be a directory, so let it
592 # take the slow path.
592 # take the slow path.
593 if os.path.exists(repo.wjoin(f)):
593 if os.path.exists(repo.wjoin(f)):
594 slowpath = True
594 slowpath = True
595 continue
595 continue
596 else:
596 else:
597 raise error.Abort(_('cannot follow file not in parent '
597 raise error.Abort(_('cannot follow file not in parent '
598 'revision: "%s"') % f)
598 'revision: "%s"') % f)
599 filelog = repo.file(f)
599 filelog = repo.file(f)
600 if not filelog:
600 if not filelog:
601 # A zero count may be a directory or deleted file, so
601 # A zero count may be a directory or deleted file, so
602 # try to find matching entries on the slow path.
602 # try to find matching entries on the slow path.
603 if follow:
603 if follow:
604 raise error.Abort(
604 raise error.Abort(
605 _('cannot follow nonexistent file: "%s"') % f)
605 _('cannot follow nonexistent file: "%s"') % f)
606 slowpath = True
606 slowpath = True
607
607
608 # We decided to fall back to the slowpath because at least one
608 # We decided to fall back to the slowpath because at least one
609 # of the paths was not a file. Check to see if at least one of them
609 # of the paths was not a file. Check to see if at least one of them
610 # existed in history - in that case, we'll continue down the
610 # existed in history - in that case, we'll continue down the
611 # slowpath; otherwise, we can turn off the slowpath
611 # slowpath; otherwise, we can turn off the slowpath
612 if slowpath:
612 if slowpath:
613 for path in match.files():
613 for path in match.files():
614 if path == '.' or path in repo.store:
614 if path == '.' or path in repo.store:
615 break
615 break
616 else:
616 else:
617 slowpath = False
617 slowpath = False
618
618
619 return match, pats, slowpath
619 return match, pats, slowpath
620
620
621 def _fileancestors(repo, revs, match, followfirst):
621 def _fileancestors(repo, revs, match, followfirst):
622 fctxs = []
622 fctxs = []
623 for r in revs:
623 for r in revs:
624 ctx = repo[r]
624 ctx = repo[r]
625 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
625 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
626
626
627 # When displaying a revision with --patch --follow FILE, we have
627 # When displaying a revision with --patch --follow FILE, we have
628 # to know which file of the revision must be diffed. With
628 # to know which file of the revision must be diffed. With
629 # --follow, we want the names of the ancestors of FILE in the
629 # --follow, we want the names of the ancestors of FILE in the
630 # revision, stored in "fcache". "fcache" is populated as a side effect
630 # revision, stored in "fcache". "fcache" is populated as a side effect
631 # of the graph traversal.
631 # of the graph traversal.
632 fcache = {}
632 fcache = {}
633 def filematcher(ctx):
633 def filematcher(ctx):
634 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
634 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
635
635
636 def revgen():
636 def revgen():
637 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
637 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
638 fcache[rev] = [c.path() for c in cs]
638 fcache[rev] = [c.path() for c in cs]
639 yield rev
639 yield rev
640 return smartset.generatorset(revgen(), iterasc=False), filematcher
640 return smartset.generatorset(revgen(), iterasc=False), filematcher
641
641
642 def _makenofollowfilematcher(repo, pats, opts):
642 def _makenofollowfilematcher(repo, pats, opts):
643 '''hook for extensions to override the filematcher for non-follow cases'''
643 '''hook for extensions to override the filematcher for non-follow cases'''
644 return None
644 return None
645
645
646 _opt2logrevset = {
646 _opt2logrevset = {
647 'no_merges': ('not merge()', None),
647 'no_merges': ('not merge()', None),
648 'only_merges': ('merge()', None),
648 'only_merges': ('merge()', None),
649 '_matchfiles': (None, '_matchfiles(%ps)'),
649 '_matchfiles': (None, '_matchfiles(%ps)'),
650 'date': ('date(%s)', None),
650 'date': ('date(%s)', None),
651 'branch': ('branch(%s)', '%lr'),
651 'branch': ('branch(%s)', '%lr'),
652 '_patslog': ('filelog(%s)', '%lr'),
652 '_patslog': ('filelog(%s)', '%lr'),
653 'keyword': ('keyword(%s)', '%lr'),
653 'keyword': ('keyword(%s)', '%lr'),
654 'prune': ('ancestors(%s)', 'not %lr'),
654 'prune': ('ancestors(%s)', 'not %lr'),
655 'user': ('user(%s)', '%lr'),
655 'user': ('user(%s)', '%lr'),
656 }
656 }
657
657
658 def _makerevset(repo, match, pats, slowpath, opts):
658 def _makerevset(repo, match, pats, slowpath, opts):
659 """Return a revset string built from log options and file patterns"""
659 """Return a revset string built from log options and file patterns"""
660 opts = dict(opts)
660 opts = dict(opts)
661 # follow or not follow?
661 # follow or not follow?
662 follow = opts.get('follow') or opts.get('follow_first')
662 follow = opts.get('follow') or opts.get('follow_first')
663
663
664 # branch and only_branch are really aliases and must be handled at
664 # branch and only_branch are really aliases and must be handled at
665 # the same time
665 # the same time
666 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
666 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
667 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
667 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
668
668
669 if slowpath:
669 if slowpath:
670 # See walkchangerevs() slow path.
670 # See walkchangerevs() slow path.
671 #
671 #
672 # pats/include/exclude cannot be represented as separate
672 # pats/include/exclude cannot be represented as separate
673 # revset expressions as their filtering logic applies at file
673 # revset expressions as their filtering logic applies at file
674 # level. For instance "-I a -X b" matches a revision touching
674 # level. For instance "-I a -X b" matches a revision touching
675 # "a" and "b" while "file(a) and not file(b)" does
675 # "a" and "b" while "file(a) and not file(b)" does
676 # not. Besides, filesets are evaluated against the working
676 # not. Besides, filesets are evaluated against the working
677 # directory.
677 # directory.
678 matchargs = ['r:', 'd:relpath']
678 matchargs = ['r:', 'd:relpath']
679 for p in pats:
679 for p in pats:
680 matchargs.append('p:' + p)
680 matchargs.append('p:' + p)
681 for p in opts.get('include', []):
681 for p in opts.get('include', []):
682 matchargs.append('i:' + p)
682 matchargs.append('i:' + p)
683 for p in opts.get('exclude', []):
683 for p in opts.get('exclude', []):
684 matchargs.append('x:' + p)
684 matchargs.append('x:' + p)
685 opts['_matchfiles'] = matchargs
685 opts['_matchfiles'] = matchargs
686 elif not follow:
686 elif not follow:
687 opts['_patslog'] = list(pats)
687 opts['_patslog'] = list(pats)
688
688
689 expr = []
689 expr = []
690 for op, val in sorted(opts.iteritems()):
690 for op, val in sorted(opts.iteritems()):
691 if not val:
691 if not val:
692 continue
692 continue
693 if op not in _opt2logrevset:
693 if op not in _opt2logrevset:
694 continue
694 continue
695 revop, listop = _opt2logrevset[op]
695 revop, listop = _opt2logrevset[op]
696 if revop and '%' not in revop:
696 if revop and '%' not in revop:
697 expr.append(revop)
697 expr.append(revop)
698 elif not listop:
698 elif not listop:
699 expr.append(revsetlang.formatspec(revop, val))
699 expr.append(revsetlang.formatspec(revop, val))
700 else:
700 else:
701 if revop:
701 if revop:
702 val = [revsetlang.formatspec(revop, v) for v in val]
702 val = [revsetlang.formatspec(revop, v) for v in val]
703 expr.append(revsetlang.formatspec(listop, val))
703 expr.append(revsetlang.formatspec(listop, val))
704
704
705 if expr:
705 if expr:
706 expr = '(' + ' and '.join(expr) + ')'
706 expr = '(' + ' and '.join(expr) + ')'
707 else:
707 else:
708 expr = None
708 expr = None
709 return expr
709 return expr
710
710
711 def _initialrevs(repo, opts):
711 def _initialrevs(repo, opts):
712 """Return the initial set of revisions to be filtered or followed"""
712 """Return the initial set of revisions to be filtered or followed"""
713 follow = opts.get('follow') or opts.get('follow_first')
713 follow = opts.get('follow') or opts.get('follow_first')
714 if opts.get('rev'):
714 if opts.get('rev'):
715 revs = scmutil.revrange(repo, opts['rev'])
715 revs = scmutil.revrange(repo, opts['rev'])
716 elif follow and repo.dirstate.p1() == nullid:
716 elif follow and repo.dirstate.p1() == nullid:
717 revs = smartset.baseset()
717 revs = smartset.baseset()
718 elif follow:
718 elif follow:
719 revs = repo.revs('.')
719 revs = repo.revs('.')
720 else:
720 else:
721 revs = smartset.spanset(repo)
721 revs = smartset.spanset(repo)
722 revs.reverse()
722 revs.reverse()
723 return revs
723 return revs
724
724
725 def getrevs(repo, pats, opts):
725 def getrevs(repo, pats, opts):
726 """Return (revs, differ) where revs is a smartset
726 """Return (revs, differ) where revs is a smartset
727
727
728 differ is a changesetdiffer with pre-configured file matcher.
728 differ is a changesetdiffer with pre-configured file matcher.
729 """
729 """
730 follow = opts.get('follow') or opts.get('follow_first')
730 follow = opts.get('follow') or opts.get('follow_first')
731 followfirst = opts.get('follow_first')
731 followfirst = opts.get('follow_first')
732 limit = getlimit(opts)
732 limit = getlimit(opts)
733 revs = _initialrevs(repo, opts)
733 revs = _initialrevs(repo, opts)
734 if not revs:
734 if not revs:
735 return smartset.baseset(), None
735 return smartset.baseset(), None
736 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
736 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
737 filematcher = None
737 filematcher = None
738 if follow:
738 if follow:
739 if slowpath or match.always():
739 if slowpath or match.always():
740 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
740 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
741 else:
741 else:
742 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
742 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
743 revs.reverse()
743 revs.reverse()
744 if filematcher is None:
744 if filematcher is None:
745 filematcher = _makenofollowfilematcher(repo, pats, opts)
745 filematcher = _makenofollowfilematcher(repo, pats, opts)
746 if filematcher is None:
746 if filematcher is None:
747 def filematcher(ctx):
747 def filematcher(ctx):
748 return match
748 return match
749
749
750 expr = _makerevset(repo, match, pats, slowpath, opts)
750 expr = _makerevset(repo, match, pats, slowpath, opts)
751 if opts.get('graph') and opts.get('rev'):
751 if opts.get('graph') and opts.get('rev'):
752 # User-specified revs might be unsorted, but don't sort before
752 # User-specified revs might be unsorted, but don't sort before
753 # _makerevset because it might depend on the order of revs
753 # _makerevset because it might depend on the order of revs
754 if not (revs.isdescending() or revs.istopo()):
754 if not (revs.isdescending() or revs.istopo()):
755 revs.sort(reverse=True)
755 revs.sort(reverse=True)
756 if expr:
756 if expr:
757 matcher = revset.match(None, expr)
757 matcher = revset.match(None, expr)
758 revs = matcher(repo, revs)
758 revs = matcher(repo, revs)
759 if limit is not None:
759 if limit is not None:
760 revs = revs.slice(0, limit)
760 revs = revs.slice(0, limit)
761
761
762 differ = changesetdiffer()
762 differ = changesetdiffer()
763 differ._makefilematcher = filematcher
763 differ._makefilematcher = filematcher
764 return revs, differ
764 return revs, differ
765
765
766 def _parselinerangeopt(repo, opts):
766 def _parselinerangeopt(repo, opts):
767 """Parse --line-range log option and return a list of tuples (filename,
767 """Parse --line-range log option and return a list of tuples (filename,
768 (fromline, toline)).
768 (fromline, toline)).
769 """
769 """
770 linerangebyfname = []
770 linerangebyfname = []
771 for pat in opts.get('line_range', []):
771 for pat in opts.get('line_range', []):
772 try:
772 try:
773 pat, linerange = pat.rsplit(',', 1)
773 pat, linerange = pat.rsplit(',', 1)
774 except ValueError:
774 except ValueError:
775 raise error.Abort(_('malformatted line-range pattern %s') % pat)
775 raise error.Abort(_('malformatted line-range pattern %s') % pat)
776 try:
776 try:
777 fromline, toline = map(int, linerange.split(':'))
777 fromline, toline = map(int, linerange.split(':'))
778 except ValueError:
778 except ValueError:
779 raise error.Abort(_("invalid line range for %s") % pat)
779 raise error.Abort(_("invalid line range for %s") % pat)
780 msg = _("line range pattern '%s' must match exactly one file") % pat
780 msg = _("line range pattern '%s' must match exactly one file") % pat
781 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
781 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
782 linerangebyfname.append(
782 linerangebyfname.append(
783 (fname, util.processlinerange(fromline, toline)))
783 (fname, util.processlinerange(fromline, toline)))
784 return linerangebyfname
784 return linerangebyfname
785
785
786 def getlinerangerevs(repo, userrevs, opts):
786 def getlinerangerevs(repo, userrevs, opts):
787 """Return (revs, differ).
787 """Return (revs, differ).
788
788
789 "revs" are revisions obtained by processing "line-range" log options and
789 "revs" are revisions obtained by processing "line-range" log options and
790 walking block ancestors of each specified file/line-range.
790 walking block ancestors of each specified file/line-range.
791
791
792 "differ" is a changesetdiffer with pre-configured file matcher and hunks
792 "differ" is a changesetdiffer with pre-configured file matcher and hunks
793 filter.
793 filter.
794 """
794 """
795 wctx = repo[None]
795 wctx = repo[None]
796
796
797 # Two-levels map of "rev -> file ctx -> [line range]".
797 # Two-levels map of "rev -> file ctx -> [line range]".
798 linerangesbyrev = {}
798 linerangesbyrev = {}
799 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
799 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
800 if fname not in wctx:
800 if fname not in wctx:
801 raise error.Abort(_('cannot follow file not in parent '
801 raise error.Abort(_('cannot follow file not in parent '
802 'revision: "%s"') % fname)
802 'revision: "%s"') % fname)
803 fctx = wctx.filectx(fname)
803 fctx = wctx.filectx(fname)
804 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
804 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
805 rev = fctx.introrev()
805 rev = fctx.introrev()
806 if rev not in userrevs:
806 if rev not in userrevs:
807 continue
807 continue
808 linerangesbyrev.setdefault(
808 linerangesbyrev.setdefault(
809 rev, {}).setdefault(
809 rev, {}).setdefault(
810 fctx.path(), []).append(linerange)
810 fctx.path(), []).append(linerange)
811
811
812 def nofilterhunksfn(fctx, hunks):
812 def nofilterhunksfn(fctx, hunks):
813 return hunks
813 return hunks
814
814
815 def hunksfilter(ctx):
815 def hunksfilter(ctx):
816 fctxlineranges = linerangesbyrev.get(ctx.rev())
816 fctxlineranges = linerangesbyrev.get(ctx.rev())
817 if fctxlineranges is None:
817 if fctxlineranges is None:
818 return nofilterhunksfn
818 return nofilterhunksfn
819
819
820 def filterfn(fctx, hunks):
820 def filterfn(fctx, hunks):
821 lineranges = fctxlineranges.get(fctx.path())
821 lineranges = fctxlineranges.get(fctx.path())
822 if lineranges is not None:
822 if lineranges is not None:
823 for hr, lines in hunks:
823 for hr, lines in hunks:
824 if hr is None: # binary
824 if hr is None: # binary
825 yield hr, lines
825 yield hr, lines
826 continue
826 continue
827 if any(mdiff.hunkinrange(hr[2:], lr)
827 if any(mdiff.hunkinrange(hr[2:], lr)
828 for lr in lineranges):
828 for lr in lineranges):
829 yield hr, lines
829 yield hr, lines
830 else:
830 else:
831 for hunk in hunks:
831 for hunk in hunks:
832 yield hunk
832 yield hunk
833
833
834 return filterfn
834 return filterfn
835
835
836 def filematcher(ctx):
836 def filematcher(ctx):
837 files = list(linerangesbyrev.get(ctx.rev(), []))
837 files = list(linerangesbyrev.get(ctx.rev(), []))
838 return scmutil.matchfiles(repo, files)
838 return scmutil.matchfiles(repo, files)
839
839
840 revs = sorted(linerangesbyrev, reverse=True)
840 revs = sorted(linerangesbyrev, reverse=True)
841
841
842 differ = changesetdiffer()
842 differ = changesetdiffer()
843 differ._makefilematcher = filematcher
843 differ._makefilematcher = filematcher
844 differ._makehunksfilter = hunksfilter
844 differ._makehunksfilter = hunksfilter
845 return revs, differ
845 return revs, differ
846
846
847 def _graphnodeformatter(ui, displayer):
847 def _graphnodeformatter(ui, displayer):
848 spec = ui.config('ui', 'graphnodetemplate')
848 spec = ui.config('ui', 'graphnodetemplate')
849 if not spec:
849 if not spec:
850 return templatekw.getgraphnode # fast path for "{graphnode}"
850 return templatekw.getgraphnode # fast path for "{graphnode}"
851
851
852 spec = templater.unquotestring(spec)
852 spec = templater.unquotestring(spec)
853 tres = formatter.templateresources(ui)
853 tres = formatter.templateresources(ui)
854 if isinstance(displayer, changesettemplater):
854 if isinstance(displayer, changesettemplater):
855 tres['cache'] = displayer.cache # reuse cache of slow templates
855 # reuse cache of slow templates
856 tres['cache'] = displayer._getcache
856 templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
857 templ = formatter.maketemplater(ui, spec, defaults=templatekw.keywords,
857 resources=tres)
858 resources=tres)
858 def formatnode(repo, ctx):
859 def formatnode(repo, ctx):
859 props = {'ctx': ctx, 'repo': repo, 'revcache': {}}
860 props = {'ctx': ctx, 'repo': repo, 'revcache': {}}
860 return templ.render(props)
861 return templ.render(props)
861 return formatnode
862 return formatnode
862
863
863 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None, props=None):
864 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None, props=None):
864 props = props or {}
865 props = props or {}
865 formatnode = _graphnodeformatter(ui, displayer)
866 formatnode = _graphnodeformatter(ui, displayer)
866 state = graphmod.asciistate()
867 state = graphmod.asciistate()
867 styles = state['styles']
868 styles = state['styles']
868
869
869 # only set graph styling if HGPLAIN is not set.
870 # only set graph styling if HGPLAIN is not set.
870 if ui.plain('graph'):
871 if ui.plain('graph'):
871 # set all edge styles to |, the default pre-3.8 behaviour
872 # set all edge styles to |, the default pre-3.8 behaviour
872 styles.update(dict.fromkeys(styles, '|'))
873 styles.update(dict.fromkeys(styles, '|'))
873 else:
874 else:
874 edgetypes = {
875 edgetypes = {
875 'parent': graphmod.PARENT,
876 'parent': graphmod.PARENT,
876 'grandparent': graphmod.GRANDPARENT,
877 'grandparent': graphmod.GRANDPARENT,
877 'missing': graphmod.MISSINGPARENT
878 'missing': graphmod.MISSINGPARENT
878 }
879 }
879 for name, key in edgetypes.items():
880 for name, key in edgetypes.items():
880 # experimental config: experimental.graphstyle.*
881 # experimental config: experimental.graphstyle.*
881 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
882 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
882 styles[key])
883 styles[key])
883 if not styles[key]:
884 if not styles[key]:
884 styles[key] = None
885 styles[key] = None
885
886
886 # experimental config: experimental.graphshorten
887 # experimental config: experimental.graphshorten
887 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
888 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
888
889
889 for rev, type, ctx, parents in dag:
890 for rev, type, ctx, parents in dag:
890 char = formatnode(repo, ctx)
891 char = formatnode(repo, ctx)
891 copies = None
892 copies = None
892 if getrenamed and ctx.rev():
893 if getrenamed and ctx.rev():
893 copies = []
894 copies = []
894 for fn in ctx.files():
895 for fn in ctx.files():
895 rename = getrenamed(fn, ctx.rev())
896 rename = getrenamed(fn, ctx.rev())
896 if rename:
897 if rename:
897 copies.append((fn, rename[0]))
898 copies.append((fn, rename[0]))
898 edges = edgefn(type, char, state, rev, parents)
899 edges = edgefn(type, char, state, rev, parents)
899 firstedge = next(edges)
900 firstedge = next(edges)
900 width = firstedge[2]
901 width = firstedge[2]
901 displayer.show(ctx, copies=copies,
902 displayer.show(ctx, copies=copies,
902 graphwidth=width, **pycompat.strkwargs(props))
903 graphwidth=width, **pycompat.strkwargs(props))
903 lines = displayer.hunk.pop(rev).split('\n')
904 lines = displayer.hunk.pop(rev).split('\n')
904 if not lines[-1]:
905 if not lines[-1]:
905 del lines[-1]
906 del lines[-1]
906 displayer.flush(ctx)
907 displayer.flush(ctx)
907 for type, char, width, coldata in itertools.chain([firstedge], edges):
908 for type, char, width, coldata in itertools.chain([firstedge], edges):
908 graphmod.ascii(ui, state, type, char, lines, coldata)
909 graphmod.ascii(ui, state, type, char, lines, coldata)
909 lines = []
910 lines = []
910 displayer.close()
911 displayer.close()
911
912
912 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
913 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
913 revdag = graphmod.dagwalker(repo, revs)
914 revdag = graphmod.dagwalker(repo, revs)
914 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
915 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
915
916
916 def displayrevs(ui, repo, revs, displayer, getrenamed):
917 def displayrevs(ui, repo, revs, displayer, getrenamed):
917 for rev in revs:
918 for rev in revs:
918 ctx = repo[rev]
919 ctx = repo[rev]
919 copies = None
920 copies = None
920 if getrenamed is not None and rev:
921 if getrenamed is not None and rev:
921 copies = []
922 copies = []
922 for fn in ctx.files():
923 for fn in ctx.files():
923 rename = getrenamed(fn, rev)
924 rename = getrenamed(fn, rev)
924 if rename:
925 if rename:
925 copies.append((fn, rename[0]))
926 copies.append((fn, rename[0]))
926 displayer.show(ctx, copies=copies)
927 displayer.show(ctx, copies=copies)
927 displayer.flush(ctx)
928 displayer.flush(ctx)
928 displayer.close()
929 displayer.close()
929
930
930 def checkunsupportedgraphflags(pats, opts):
931 def checkunsupportedgraphflags(pats, opts):
931 for op in ["newest_first"]:
932 for op in ["newest_first"]:
932 if op in opts and opts[op]:
933 if op in opts and opts[op]:
933 raise error.Abort(_("-G/--graph option is incompatible with --%s")
934 raise error.Abort(_("-G/--graph option is incompatible with --%s")
934 % op.replace("_", "-"))
935 % op.replace("_", "-"))
935
936
936 def graphrevs(repo, nodes, opts):
937 def graphrevs(repo, nodes, opts):
937 limit = getlimit(opts)
938 limit = getlimit(opts)
938 nodes.reverse()
939 nodes.reverse()
939 if limit is not None:
940 if limit is not None:
940 nodes = nodes[:limit]
941 nodes = nodes[:limit]
941 return graphmod.nodes(repo, nodes)
942 return graphmod.nodes(repo, nodes)
@@ -1,799 +1,800
1 # templater.py - template expansion for output
1 # templater.py - template expansion for output
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import os
10 import os
11
11
12 from .i18n import _
12 from .i18n import _
13 from . import (
13 from . import (
14 config,
14 config,
15 encoding,
15 encoding,
16 error,
16 error,
17 parser,
17 parser,
18 pycompat,
18 pycompat,
19 templatefilters,
19 templatefilters,
20 templatefuncs,
20 templatefuncs,
21 templateutil,
21 templateutil,
22 util,
22 util,
23 )
23 )
24
24
25 # template parsing
25 # template parsing
26
26
27 elements = {
27 elements = {
28 # token-type: binding-strength, primary, prefix, infix, suffix
28 # token-type: binding-strength, primary, prefix, infix, suffix
29 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
29 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
30 ".": (18, None, None, (".", 18), None),
30 ".": (18, None, None, (".", 18), None),
31 "%": (15, None, None, ("%", 15), None),
31 "%": (15, None, None, ("%", 15), None),
32 "|": (15, None, None, ("|", 15), None),
32 "|": (15, None, None, ("|", 15), None),
33 "*": (5, None, None, ("*", 5), None),
33 "*": (5, None, None, ("*", 5), None),
34 "/": (5, None, None, ("/", 5), None),
34 "/": (5, None, None, ("/", 5), None),
35 "+": (4, None, None, ("+", 4), None),
35 "+": (4, None, None, ("+", 4), None),
36 "-": (4, None, ("negate", 19), ("-", 4), None),
36 "-": (4, None, ("negate", 19), ("-", 4), None),
37 "=": (3, None, None, ("keyvalue", 3), None),
37 "=": (3, None, None, ("keyvalue", 3), None),
38 ",": (2, None, None, ("list", 2), None),
38 ",": (2, None, None, ("list", 2), None),
39 ")": (0, None, None, None, None),
39 ")": (0, None, None, None, None),
40 "integer": (0, "integer", None, None, None),
40 "integer": (0, "integer", None, None, None),
41 "symbol": (0, "symbol", None, None, None),
41 "symbol": (0, "symbol", None, None, None),
42 "string": (0, "string", None, None, None),
42 "string": (0, "string", None, None, None),
43 "template": (0, "template", None, None, None),
43 "template": (0, "template", None, None, None),
44 "end": (0, None, None, None, None),
44 "end": (0, None, None, None, None),
45 }
45 }
46
46
47 def tokenize(program, start, end, term=None):
47 def tokenize(program, start, end, term=None):
48 """Parse a template expression into a stream of tokens, which must end
48 """Parse a template expression into a stream of tokens, which must end
49 with term if specified"""
49 with term if specified"""
50 pos = start
50 pos = start
51 program = pycompat.bytestr(program)
51 program = pycompat.bytestr(program)
52 while pos < end:
52 while pos < end:
53 c = program[pos]
53 c = program[pos]
54 if c.isspace(): # skip inter-token whitespace
54 if c.isspace(): # skip inter-token whitespace
55 pass
55 pass
56 elif c in "(=,).%|+-*/": # handle simple operators
56 elif c in "(=,).%|+-*/": # handle simple operators
57 yield (c, None, pos)
57 yield (c, None, pos)
58 elif c in '"\'': # handle quoted templates
58 elif c in '"\'': # handle quoted templates
59 s = pos + 1
59 s = pos + 1
60 data, pos = _parsetemplate(program, s, end, c)
60 data, pos = _parsetemplate(program, s, end, c)
61 yield ('template', data, s)
61 yield ('template', data, s)
62 pos -= 1
62 pos -= 1
63 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
63 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
64 # handle quoted strings
64 # handle quoted strings
65 c = program[pos + 1]
65 c = program[pos + 1]
66 s = pos = pos + 2
66 s = pos = pos + 2
67 while pos < end: # find closing quote
67 while pos < end: # find closing quote
68 d = program[pos]
68 d = program[pos]
69 if d == '\\': # skip over escaped characters
69 if d == '\\': # skip over escaped characters
70 pos += 2
70 pos += 2
71 continue
71 continue
72 if d == c:
72 if d == c:
73 yield ('string', program[s:pos], s)
73 yield ('string', program[s:pos], s)
74 break
74 break
75 pos += 1
75 pos += 1
76 else:
76 else:
77 raise error.ParseError(_("unterminated string"), s)
77 raise error.ParseError(_("unterminated string"), s)
78 elif c.isdigit():
78 elif c.isdigit():
79 s = pos
79 s = pos
80 while pos < end:
80 while pos < end:
81 d = program[pos]
81 d = program[pos]
82 if not d.isdigit():
82 if not d.isdigit():
83 break
83 break
84 pos += 1
84 pos += 1
85 yield ('integer', program[s:pos], s)
85 yield ('integer', program[s:pos], s)
86 pos -= 1
86 pos -= 1
87 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
87 elif (c == '\\' and program[pos:pos + 2] in (br"\'", br'\"')
88 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
88 or c == 'r' and program[pos:pos + 3] in (br"r\'", br'r\"')):
89 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
89 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
90 # where some of nested templates were preprocessed as strings and
90 # where some of nested templates were preprocessed as strings and
91 # then compiled. therefore, \"...\" was allowed. (issue4733)
91 # then compiled. therefore, \"...\" was allowed. (issue4733)
92 #
92 #
93 # processing flow of _evalifliteral() at 5ab28a2e9962:
93 # processing flow of _evalifliteral() at 5ab28a2e9962:
94 # outer template string -> stringify() -> compiletemplate()
94 # outer template string -> stringify() -> compiletemplate()
95 # ------------------------ ------------ ------------------
95 # ------------------------ ------------ ------------------
96 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
96 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
97 # ~~~~~~~~
97 # ~~~~~~~~
98 # escaped quoted string
98 # escaped quoted string
99 if c == 'r':
99 if c == 'r':
100 pos += 1
100 pos += 1
101 token = 'string'
101 token = 'string'
102 else:
102 else:
103 token = 'template'
103 token = 'template'
104 quote = program[pos:pos + 2]
104 quote = program[pos:pos + 2]
105 s = pos = pos + 2
105 s = pos = pos + 2
106 while pos < end: # find closing escaped quote
106 while pos < end: # find closing escaped quote
107 if program.startswith('\\\\\\', pos, end):
107 if program.startswith('\\\\\\', pos, end):
108 pos += 4 # skip over double escaped characters
108 pos += 4 # skip over double escaped characters
109 continue
109 continue
110 if program.startswith(quote, pos, end):
110 if program.startswith(quote, pos, end):
111 # interpret as if it were a part of an outer string
111 # interpret as if it were a part of an outer string
112 data = parser.unescapestr(program[s:pos])
112 data = parser.unescapestr(program[s:pos])
113 if token == 'template':
113 if token == 'template':
114 data = _parsetemplate(data, 0, len(data))[0]
114 data = _parsetemplate(data, 0, len(data))[0]
115 yield (token, data, s)
115 yield (token, data, s)
116 pos += 1
116 pos += 1
117 break
117 break
118 pos += 1
118 pos += 1
119 else:
119 else:
120 raise error.ParseError(_("unterminated string"), s)
120 raise error.ParseError(_("unterminated string"), s)
121 elif c.isalnum() or c in '_':
121 elif c.isalnum() or c in '_':
122 s = pos
122 s = pos
123 pos += 1
123 pos += 1
124 while pos < end: # find end of symbol
124 while pos < end: # find end of symbol
125 d = program[pos]
125 d = program[pos]
126 if not (d.isalnum() or d == "_"):
126 if not (d.isalnum() or d == "_"):
127 break
127 break
128 pos += 1
128 pos += 1
129 sym = program[s:pos]
129 sym = program[s:pos]
130 yield ('symbol', sym, s)
130 yield ('symbol', sym, s)
131 pos -= 1
131 pos -= 1
132 elif c == term:
132 elif c == term:
133 yield ('end', None, pos)
133 yield ('end', None, pos)
134 return
134 return
135 else:
135 else:
136 raise error.ParseError(_("syntax error"), pos)
136 raise error.ParseError(_("syntax error"), pos)
137 pos += 1
137 pos += 1
138 if term:
138 if term:
139 raise error.ParseError(_("unterminated template expansion"), start)
139 raise error.ParseError(_("unterminated template expansion"), start)
140 yield ('end', None, pos)
140 yield ('end', None, pos)
141
141
142 def _parsetemplate(tmpl, start, stop, quote=''):
142 def _parsetemplate(tmpl, start, stop, quote=''):
143 r"""
143 r"""
144 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
144 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
145 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
145 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
146 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
146 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
147 ([('string', 'foo'), ('symbol', 'bar')], 9)
147 ([('string', 'foo'), ('symbol', 'bar')], 9)
148 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
148 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
149 ([('string', 'foo')], 4)
149 ([('string', 'foo')], 4)
150 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
150 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
151 ([('string', 'foo"'), ('string', 'bar')], 9)
151 ([('string', 'foo"'), ('string', 'bar')], 9)
152 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
152 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
153 ([('string', 'foo\\')], 6)
153 ([('string', 'foo\\')], 6)
154 """
154 """
155 parsed = []
155 parsed = []
156 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
156 for typ, val, pos in _scantemplate(tmpl, start, stop, quote):
157 if typ == 'string':
157 if typ == 'string':
158 parsed.append((typ, val))
158 parsed.append((typ, val))
159 elif typ == 'template':
159 elif typ == 'template':
160 parsed.append(val)
160 parsed.append(val)
161 elif typ == 'end':
161 elif typ == 'end':
162 return parsed, pos
162 return parsed, pos
163 else:
163 else:
164 raise error.ProgrammingError('unexpected type: %s' % typ)
164 raise error.ProgrammingError('unexpected type: %s' % typ)
165 raise error.ProgrammingError('unterminated scanning of template')
165 raise error.ProgrammingError('unterminated scanning of template')
166
166
167 def scantemplate(tmpl, raw=False):
167 def scantemplate(tmpl, raw=False):
168 r"""Scan (type, start, end) positions of outermost elements in template
168 r"""Scan (type, start, end) positions of outermost elements in template
169
169
170 If raw=True, a backslash is not taken as an escape character just like
170 If raw=True, a backslash is not taken as an escape character just like
171 r'' string in Python. Note that this is different from r'' literal in
171 r'' string in Python. Note that this is different from r'' literal in
172 template in that no template fragment can appear in r'', e.g. r'{foo}'
172 template in that no template fragment can appear in r'', e.g. r'{foo}'
173 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
173 is a literal '{foo}', but ('{foo}', raw=True) is a template expression
174 'foo'.
174 'foo'.
175
175
176 >>> list(scantemplate(b'foo{bar}"baz'))
176 >>> list(scantemplate(b'foo{bar}"baz'))
177 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
177 [('string', 0, 3), ('template', 3, 8), ('string', 8, 12)]
178 >>> list(scantemplate(b'outer{"inner"}outer'))
178 >>> list(scantemplate(b'outer{"inner"}outer'))
179 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
179 [('string', 0, 5), ('template', 5, 14), ('string', 14, 19)]
180 >>> list(scantemplate(b'foo\\{escaped}'))
180 >>> list(scantemplate(b'foo\\{escaped}'))
181 [('string', 0, 5), ('string', 5, 13)]
181 [('string', 0, 5), ('string', 5, 13)]
182 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
182 >>> list(scantemplate(b'foo\\{escaped}', raw=True))
183 [('string', 0, 4), ('template', 4, 13)]
183 [('string', 0, 4), ('template', 4, 13)]
184 """
184 """
185 last = None
185 last = None
186 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
186 for typ, val, pos in _scantemplate(tmpl, 0, len(tmpl), raw=raw):
187 if last:
187 if last:
188 yield last + (pos,)
188 yield last + (pos,)
189 if typ == 'end':
189 if typ == 'end':
190 return
190 return
191 else:
191 else:
192 last = (typ, pos)
192 last = (typ, pos)
193 raise error.ProgrammingError('unterminated scanning of template')
193 raise error.ProgrammingError('unterminated scanning of template')
194
194
195 def _scantemplate(tmpl, start, stop, quote='', raw=False):
195 def _scantemplate(tmpl, start, stop, quote='', raw=False):
196 """Parse template string into chunks of strings and template expressions"""
196 """Parse template string into chunks of strings and template expressions"""
197 sepchars = '{' + quote
197 sepchars = '{' + quote
198 unescape = [parser.unescapestr, pycompat.identity][raw]
198 unescape = [parser.unescapestr, pycompat.identity][raw]
199 pos = start
199 pos = start
200 p = parser.parser(elements)
200 p = parser.parser(elements)
201 try:
201 try:
202 while pos < stop:
202 while pos < stop:
203 n = min((tmpl.find(c, pos, stop) for c in sepchars),
203 n = min((tmpl.find(c, pos, stop) for c in sepchars),
204 key=lambda n: (n < 0, n))
204 key=lambda n: (n < 0, n))
205 if n < 0:
205 if n < 0:
206 yield ('string', unescape(tmpl[pos:stop]), pos)
206 yield ('string', unescape(tmpl[pos:stop]), pos)
207 pos = stop
207 pos = stop
208 break
208 break
209 c = tmpl[n:n + 1]
209 c = tmpl[n:n + 1]
210 bs = 0 # count leading backslashes
210 bs = 0 # count leading backslashes
211 if not raw:
211 if not raw:
212 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
212 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
213 if bs % 2 == 1:
213 if bs % 2 == 1:
214 # escaped (e.g. '\{', '\\\{', but not '\\{')
214 # escaped (e.g. '\{', '\\\{', but not '\\{')
215 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
215 yield ('string', unescape(tmpl[pos:n - 1]) + c, pos)
216 pos = n + 1
216 pos = n + 1
217 continue
217 continue
218 if n > pos:
218 if n > pos:
219 yield ('string', unescape(tmpl[pos:n]), pos)
219 yield ('string', unescape(tmpl[pos:n]), pos)
220 if c == quote:
220 if c == quote:
221 yield ('end', None, n + 1)
221 yield ('end', None, n + 1)
222 return
222 return
223
223
224 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
224 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
225 if not tmpl.startswith('}', pos):
225 if not tmpl.startswith('}', pos):
226 raise error.ParseError(_("invalid token"), pos)
226 raise error.ParseError(_("invalid token"), pos)
227 yield ('template', parseres, n)
227 yield ('template', parseres, n)
228 pos += 1
228 pos += 1
229
229
230 if quote:
230 if quote:
231 raise error.ParseError(_("unterminated string"), start)
231 raise error.ParseError(_("unterminated string"), start)
232 except error.ParseError as inst:
232 except error.ParseError as inst:
233 if len(inst.args) > 1: # has location
233 if len(inst.args) > 1: # has location
234 loc = inst.args[1]
234 loc = inst.args[1]
235 # Offset the caret location by the number of newlines before the
235 # Offset the caret location by the number of newlines before the
236 # location of the error, since we will replace one-char newlines
236 # location of the error, since we will replace one-char newlines
237 # with the two-char literal r'\n'.
237 # with the two-char literal r'\n'.
238 offset = tmpl[:loc].count('\n')
238 offset = tmpl[:loc].count('\n')
239 tmpl = tmpl.replace('\n', br'\n')
239 tmpl = tmpl.replace('\n', br'\n')
240 # We want the caret to point to the place in the template that
240 # We want the caret to point to the place in the template that
241 # failed to parse, but in a hint we get a open paren at the
241 # failed to parse, but in a hint we get a open paren at the
242 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
242 # start. Therefore, we print "loc + 1" spaces (instead of "loc")
243 # to line up the caret with the location of the error.
243 # to line up the caret with the location of the error.
244 inst.hint = (tmpl + '\n'
244 inst.hint = (tmpl + '\n'
245 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
245 + ' ' * (loc + 1 + offset) + '^ ' + _('here'))
246 raise
246 raise
247 yield ('end', None, pos)
247 yield ('end', None, pos)
248
248
249 def _unnesttemplatelist(tree):
249 def _unnesttemplatelist(tree):
250 """Expand list of templates to node tuple
250 """Expand list of templates to node tuple
251
251
252 >>> def f(tree):
252 >>> def f(tree):
253 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
253 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
254 >>> f((b'template', []))
254 >>> f((b'template', []))
255 (string '')
255 (string '')
256 >>> f((b'template', [(b'string', b'foo')]))
256 >>> f((b'template', [(b'string', b'foo')]))
257 (string 'foo')
257 (string 'foo')
258 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
258 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
259 (template
259 (template
260 (string 'foo')
260 (string 'foo')
261 (symbol 'rev'))
261 (symbol 'rev'))
262 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
262 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
263 (template
263 (template
264 (symbol 'rev'))
264 (symbol 'rev'))
265 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
265 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
266 (string 'foo')
266 (string 'foo')
267 """
267 """
268 if not isinstance(tree, tuple):
268 if not isinstance(tree, tuple):
269 return tree
269 return tree
270 op = tree[0]
270 op = tree[0]
271 if op != 'template':
271 if op != 'template':
272 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
272 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
273
273
274 assert len(tree) == 2
274 assert len(tree) == 2
275 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
275 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
276 if not xs:
276 if not xs:
277 return ('string', '') # empty template ""
277 return ('string', '') # empty template ""
278 elif len(xs) == 1 and xs[0][0] == 'string':
278 elif len(xs) == 1 and xs[0][0] == 'string':
279 return xs[0] # fast path for string with no template fragment "x"
279 return xs[0] # fast path for string with no template fragment "x"
280 else:
280 else:
281 return (op,) + xs
281 return (op,) + xs
282
282
283 def parse(tmpl):
283 def parse(tmpl):
284 """Parse template string into tree"""
284 """Parse template string into tree"""
285 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
285 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
286 assert pos == len(tmpl), 'unquoted template should be consumed'
286 assert pos == len(tmpl), 'unquoted template should be consumed'
287 return _unnesttemplatelist(('template', parsed))
287 return _unnesttemplatelist(('template', parsed))
288
288
289 def _parseexpr(expr):
289 def _parseexpr(expr):
290 """Parse a template expression into tree
290 """Parse a template expression into tree
291
291
292 >>> _parseexpr(b'"foo"')
292 >>> _parseexpr(b'"foo"')
293 ('string', 'foo')
293 ('string', 'foo')
294 >>> _parseexpr(b'foo(bar)')
294 >>> _parseexpr(b'foo(bar)')
295 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
295 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
296 >>> _parseexpr(b'foo(')
296 >>> _parseexpr(b'foo(')
297 Traceback (most recent call last):
297 Traceback (most recent call last):
298 ...
298 ...
299 ParseError: ('not a prefix: end', 4)
299 ParseError: ('not a prefix: end', 4)
300 >>> _parseexpr(b'"foo" "bar"')
300 >>> _parseexpr(b'"foo" "bar"')
301 Traceback (most recent call last):
301 Traceback (most recent call last):
302 ...
302 ...
303 ParseError: ('invalid token', 7)
303 ParseError: ('invalid token', 7)
304 """
304 """
305 p = parser.parser(elements)
305 p = parser.parser(elements)
306 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
306 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
307 if pos != len(expr):
307 if pos != len(expr):
308 raise error.ParseError(_('invalid token'), pos)
308 raise error.ParseError(_('invalid token'), pos)
309 return _unnesttemplatelist(tree)
309 return _unnesttemplatelist(tree)
310
310
311 def prettyformat(tree):
311 def prettyformat(tree):
312 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
312 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
313
313
314 def compileexp(exp, context, curmethods):
314 def compileexp(exp, context, curmethods):
315 """Compile parsed template tree to (func, data) pair"""
315 """Compile parsed template tree to (func, data) pair"""
316 if not exp:
316 if not exp:
317 raise error.ParseError(_("missing argument"))
317 raise error.ParseError(_("missing argument"))
318 t = exp[0]
318 t = exp[0]
319 if t in curmethods:
319 if t in curmethods:
320 return curmethods[t](exp, context)
320 return curmethods[t](exp, context)
321 raise error.ParseError(_("unknown method '%s'") % t)
321 raise error.ParseError(_("unknown method '%s'") % t)
322
322
323 # template evaluation
323 # template evaluation
324
324
325 def getsymbol(exp):
325 def getsymbol(exp):
326 if exp[0] == 'symbol':
326 if exp[0] == 'symbol':
327 return exp[1]
327 return exp[1]
328 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
328 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
329
329
330 def getlist(x):
330 def getlist(x):
331 if not x:
331 if not x:
332 return []
332 return []
333 if x[0] == 'list':
333 if x[0] == 'list':
334 return getlist(x[1]) + [x[2]]
334 return getlist(x[1]) + [x[2]]
335 return [x]
335 return [x]
336
336
337 def gettemplate(exp, context):
337 def gettemplate(exp, context):
338 """Compile given template tree or load named template from map file;
338 """Compile given template tree or load named template from map file;
339 returns (func, data) pair"""
339 returns (func, data) pair"""
340 if exp[0] in ('template', 'string'):
340 if exp[0] in ('template', 'string'):
341 return compileexp(exp, context, methods)
341 return compileexp(exp, context, methods)
342 if exp[0] == 'symbol':
342 if exp[0] == 'symbol':
343 # unlike runsymbol(), here 'symbol' is always taken as template name
343 # unlike runsymbol(), here 'symbol' is always taken as template name
344 # even if it exists in mapping. this allows us to override mapping
344 # even if it exists in mapping. this allows us to override mapping
345 # by web templates, e.g. 'changelogtag' is redefined in map file.
345 # by web templates, e.g. 'changelogtag' is redefined in map file.
346 return context._load(exp[1])
346 return context._load(exp[1])
347 raise error.ParseError(_("expected template specifier"))
347 raise error.ParseError(_("expected template specifier"))
348
348
349 def _runrecursivesymbol(context, mapping, key):
349 def _runrecursivesymbol(context, mapping, key):
350 raise error.Abort(_("recursive reference '%s' in template") % key)
350 raise error.Abort(_("recursive reference '%s' in template") % key)
351
351
352 def buildtemplate(exp, context):
352 def buildtemplate(exp, context):
353 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
353 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
354 return (templateutil.runtemplate, ctmpl)
354 return (templateutil.runtemplate, ctmpl)
355
355
356 def buildfilter(exp, context):
356 def buildfilter(exp, context):
357 n = getsymbol(exp[2])
357 n = getsymbol(exp[2])
358 if n in context._filters:
358 if n in context._filters:
359 filt = context._filters[n]
359 filt = context._filters[n]
360 arg = compileexp(exp[1], context, methods)
360 arg = compileexp(exp[1], context, methods)
361 return (templateutil.runfilter, (arg, filt))
361 return (templateutil.runfilter, (arg, filt))
362 if n in context._funcs:
362 if n in context._funcs:
363 f = context._funcs[n]
363 f = context._funcs[n]
364 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
364 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
365 return (f, args)
365 return (f, args)
366 raise error.ParseError(_("unknown function '%s'") % n)
366 raise error.ParseError(_("unknown function '%s'") % n)
367
367
368 def buildmap(exp, context):
368 def buildmap(exp, context):
369 darg = compileexp(exp[1], context, methods)
369 darg = compileexp(exp[1], context, methods)
370 targ = gettemplate(exp[2], context)
370 targ = gettemplate(exp[2], context)
371 return (templateutil.runmap, (darg, targ))
371 return (templateutil.runmap, (darg, targ))
372
372
373 def buildmember(exp, context):
373 def buildmember(exp, context):
374 darg = compileexp(exp[1], context, methods)
374 darg = compileexp(exp[1], context, methods)
375 memb = getsymbol(exp[2])
375 memb = getsymbol(exp[2])
376 return (templateutil.runmember, (darg, memb))
376 return (templateutil.runmember, (darg, memb))
377
377
378 def buildnegate(exp, context):
378 def buildnegate(exp, context):
379 arg = compileexp(exp[1], context, exprmethods)
379 arg = compileexp(exp[1], context, exprmethods)
380 return (templateutil.runnegate, arg)
380 return (templateutil.runnegate, arg)
381
381
382 def buildarithmetic(exp, context, func):
382 def buildarithmetic(exp, context, func):
383 left = compileexp(exp[1], context, exprmethods)
383 left = compileexp(exp[1], context, exprmethods)
384 right = compileexp(exp[2], context, exprmethods)
384 right = compileexp(exp[2], context, exprmethods)
385 return (templateutil.runarithmetic, (func, left, right))
385 return (templateutil.runarithmetic, (func, left, right))
386
386
387 def buildfunc(exp, context):
387 def buildfunc(exp, context):
388 n = getsymbol(exp[1])
388 n = getsymbol(exp[1])
389 if n in context._funcs:
389 if n in context._funcs:
390 f = context._funcs[n]
390 f = context._funcs[n]
391 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
391 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
392 return (f, args)
392 return (f, args)
393 if n in context._filters:
393 if n in context._filters:
394 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
394 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
395 if len(args) != 1:
395 if len(args) != 1:
396 raise error.ParseError(_("filter %s expects one argument") % n)
396 raise error.ParseError(_("filter %s expects one argument") % n)
397 f = context._filters[n]
397 f = context._filters[n]
398 return (templateutil.runfilter, (args[0], f))
398 return (templateutil.runfilter, (args[0], f))
399 raise error.ParseError(_("unknown function '%s'") % n)
399 raise error.ParseError(_("unknown function '%s'") % n)
400
400
401 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
401 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
402 """Compile parsed tree of function arguments into list or dict of
402 """Compile parsed tree of function arguments into list or dict of
403 (func, data) pairs
403 (func, data) pairs
404
404
405 >>> context = engine(lambda t: (runsymbol, t))
405 >>> context = engine(lambda t: (runsymbol, t))
406 >>> def fargs(expr, argspec):
406 >>> def fargs(expr, argspec):
407 ... x = _parseexpr(expr)
407 ... x = _parseexpr(expr)
408 ... n = getsymbol(x[1])
408 ... n = getsymbol(x[1])
409 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
409 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
410 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
410 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
411 ['l', 'k']
411 ['l', 'k']
412 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
412 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
413 >>> list(args.keys()), list(args[b'opts'].keys())
413 >>> list(args.keys()), list(args[b'opts'].keys())
414 (['opts'], ['opts', 'k'])
414 (['opts'], ['opts', 'k'])
415 """
415 """
416 def compiledict(xs):
416 def compiledict(xs):
417 return util.sortdict((k, compileexp(x, context, curmethods))
417 return util.sortdict((k, compileexp(x, context, curmethods))
418 for k, x in xs.iteritems())
418 for k, x in xs.iteritems())
419 def compilelist(xs):
419 def compilelist(xs):
420 return [compileexp(x, context, curmethods) for x in xs]
420 return [compileexp(x, context, curmethods) for x in xs]
421
421
422 if not argspec:
422 if not argspec:
423 # filter or function with no argspec: return list of positional args
423 # filter or function with no argspec: return list of positional args
424 return compilelist(getlist(exp))
424 return compilelist(getlist(exp))
425
425
426 # function with argspec: return dict of named args
426 # function with argspec: return dict of named args
427 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
427 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
428 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
428 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
429 keyvaluenode='keyvalue', keynode='symbol')
429 keyvaluenode='keyvalue', keynode='symbol')
430 compargs = util.sortdict()
430 compargs = util.sortdict()
431 if varkey:
431 if varkey:
432 compargs[varkey] = compilelist(treeargs.pop(varkey))
432 compargs[varkey] = compilelist(treeargs.pop(varkey))
433 if optkey:
433 if optkey:
434 compargs[optkey] = compiledict(treeargs.pop(optkey))
434 compargs[optkey] = compiledict(treeargs.pop(optkey))
435 compargs.update(compiledict(treeargs))
435 compargs.update(compiledict(treeargs))
436 return compargs
436 return compargs
437
437
438 def buildkeyvaluepair(exp, content):
438 def buildkeyvaluepair(exp, content):
439 raise error.ParseError(_("can't use a key-value pair in this context"))
439 raise error.ParseError(_("can't use a key-value pair in this context"))
440
440
441 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
441 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
442 exprmethods = {
442 exprmethods = {
443 "integer": lambda e, c: (templateutil.runinteger, e[1]),
443 "integer": lambda e, c: (templateutil.runinteger, e[1]),
444 "string": lambda e, c: (templateutil.runstring, e[1]),
444 "string": lambda e, c: (templateutil.runstring, e[1]),
445 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
445 "symbol": lambda e, c: (templateutil.runsymbol, e[1]),
446 "template": buildtemplate,
446 "template": buildtemplate,
447 "group": lambda e, c: compileexp(e[1], c, exprmethods),
447 "group": lambda e, c: compileexp(e[1], c, exprmethods),
448 ".": buildmember,
448 ".": buildmember,
449 "|": buildfilter,
449 "|": buildfilter,
450 "%": buildmap,
450 "%": buildmap,
451 "func": buildfunc,
451 "func": buildfunc,
452 "keyvalue": buildkeyvaluepair,
452 "keyvalue": buildkeyvaluepair,
453 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
453 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
454 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
454 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
455 "negate": buildnegate,
455 "negate": buildnegate,
456 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
456 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
457 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
457 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
458 }
458 }
459
459
460 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
460 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
461 methods = exprmethods.copy()
461 methods = exprmethods.copy()
462 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
462 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
463
463
464 class _aliasrules(parser.basealiasrules):
464 class _aliasrules(parser.basealiasrules):
465 """Parsing and expansion rule set of template aliases"""
465 """Parsing and expansion rule set of template aliases"""
466 _section = _('template alias')
466 _section = _('template alias')
467 _parse = staticmethod(_parseexpr)
467 _parse = staticmethod(_parseexpr)
468
468
469 @staticmethod
469 @staticmethod
470 def _trygetfunc(tree):
470 def _trygetfunc(tree):
471 """Return (name, args) if tree is func(...) or ...|filter; otherwise
471 """Return (name, args) if tree is func(...) or ...|filter; otherwise
472 None"""
472 None"""
473 if tree[0] == 'func' and tree[1][0] == 'symbol':
473 if tree[0] == 'func' and tree[1][0] == 'symbol':
474 return tree[1][1], getlist(tree[2])
474 return tree[1][1], getlist(tree[2])
475 if tree[0] == '|' and tree[2][0] == 'symbol':
475 if tree[0] == '|' and tree[2][0] == 'symbol':
476 return tree[2][1], [tree[1]]
476 return tree[2][1], [tree[1]]
477
477
478 def expandaliases(tree, aliases):
478 def expandaliases(tree, aliases):
479 """Return new tree of aliases are expanded"""
479 """Return new tree of aliases are expanded"""
480 aliasmap = _aliasrules.buildmap(aliases)
480 aliasmap = _aliasrules.buildmap(aliases)
481 return _aliasrules.expand(aliasmap, tree)
481 return _aliasrules.expand(aliasmap, tree)
482
482
483 # template engine
483 # template engine
484
484
485 def _flatten(thing):
485 def _flatten(thing):
486 '''yield a single stream from a possibly nested set of iterators'''
486 '''yield a single stream from a possibly nested set of iterators'''
487 thing = templateutil.unwraphybrid(thing)
487 thing = templateutil.unwraphybrid(thing)
488 if isinstance(thing, bytes):
488 if isinstance(thing, bytes):
489 yield thing
489 yield thing
490 elif isinstance(thing, str):
490 elif isinstance(thing, str):
491 # We can only hit this on Python 3, and it's here to guard
491 # We can only hit this on Python 3, and it's here to guard
492 # against infinite recursion.
492 # against infinite recursion.
493 raise error.ProgrammingError('Mercurial IO including templates is done'
493 raise error.ProgrammingError('Mercurial IO including templates is done'
494 ' with bytes, not strings, got %r' % thing)
494 ' with bytes, not strings, got %r' % thing)
495 elif thing is None:
495 elif thing is None:
496 pass
496 pass
497 elif not util.safehasattr(thing, '__iter__'):
497 elif not util.safehasattr(thing, '__iter__'):
498 yield pycompat.bytestr(thing)
498 yield pycompat.bytestr(thing)
499 else:
499 else:
500 for i in thing:
500 for i in thing:
501 i = templateutil.unwraphybrid(i)
501 i = templateutil.unwraphybrid(i)
502 if isinstance(i, bytes):
502 if isinstance(i, bytes):
503 yield i
503 yield i
504 elif i is None:
504 elif i is None:
505 pass
505 pass
506 elif not util.safehasattr(i, '__iter__'):
506 elif not util.safehasattr(i, '__iter__'):
507 yield pycompat.bytestr(i)
507 yield pycompat.bytestr(i)
508 else:
508 else:
509 for j in _flatten(i):
509 for j in _flatten(i):
510 yield j
510 yield j
511
511
512 def unquotestring(s):
512 def unquotestring(s):
513 '''unwrap quotes if any; otherwise returns unmodified string'''
513 '''unwrap quotes if any; otherwise returns unmodified string'''
514 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
514 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
515 return s
515 return s
516 return s[1:-1]
516 return s[1:-1]
517
517
518 class engine(object):
518 class engine(object):
519 '''template expansion engine.
519 '''template expansion engine.
520
520
521 template expansion works like this. a map file contains key=value
521 template expansion works like this. a map file contains key=value
522 pairs. if value is quoted, it is treated as string. otherwise, it
522 pairs. if value is quoted, it is treated as string. otherwise, it
523 is treated as name of template file.
523 is treated as name of template file.
524
524
525 templater is asked to expand a key in map. it looks up key, and
525 templater is asked to expand a key in map. it looks up key, and
526 looks for strings like this: {foo}. it expands {foo} by looking up
526 looks for strings like this: {foo}. it expands {foo} by looking up
527 foo in map, and substituting it. expansion is recursive: it stops
527 foo in map, and substituting it. expansion is recursive: it stops
528 when there is no more {foo} to replace.
528 when there is no more {foo} to replace.
529
529
530 expansion also allows formatting and filtering.
530 expansion also allows formatting and filtering.
531
531
532 format uses key to expand each item in list. syntax is
532 format uses key to expand each item in list. syntax is
533 {key%format}.
533 {key%format}.
534
534
535 filter uses function to transform value. syntax is
535 filter uses function to transform value. syntax is
536 {key|filter1|filter2|...}.'''
536 {key|filter1|filter2|...}.'''
537
537
538 def __init__(self, loader, filters=None, defaults=None, resources=None,
538 def __init__(self, loader, filters=None, defaults=None, resources=None,
539 aliases=()):
539 aliases=()):
540 self._loader = loader
540 self._loader = loader
541 if filters is None:
541 if filters is None:
542 filters = {}
542 filters = {}
543 self._filters = filters
543 self._filters = filters
544 self._funcs = templatefuncs.funcs # make this a parameter if needed
544 self._funcs = templatefuncs.funcs # make this a parameter if needed
545 if defaults is None:
545 if defaults is None:
546 defaults = {}
546 defaults = {}
547 if resources is None:
547 if resources is None:
548 resources = {}
548 resources = {}
549 self._defaults = defaults
549 self._defaults = defaults
550 self._resources = resources
550 self._resources = resources
551 self._aliasmap = _aliasrules.buildmap(aliases)
551 self._aliasmap = _aliasrules.buildmap(aliases)
552 self._cache = {} # key: (func, data)
552 self._cache = {} # key: (func, data)
553
553
554 def symbol(self, mapping, key):
554 def symbol(self, mapping, key):
555 """Resolve symbol to value or function; None if nothing found"""
555 """Resolve symbol to value or function; None if nothing found"""
556 v = None
556 v = None
557 if key not in self._resources:
557 if key not in self._resources:
558 v = mapping.get(key)
558 v = mapping.get(key)
559 if v is None:
559 if v is None:
560 v = self._defaults.get(key)
560 v = self._defaults.get(key)
561 return v
561 return v
562
562
563 def resource(self, mapping, key):
563 def resource(self, mapping, key):
564 """Return internal data (e.g. cache) used for keyword/function
564 """Return internal data (e.g. cache) used for keyword/function
565 evaluation"""
565 evaluation"""
566 v = None
566 v = None
567 if key in self._resources:
567 if key in self._resources:
568 v = mapping.get(key)
568 v = mapping.get(key)
569 if v is None:
569 if v is None and key in self._resources:
570 v = self._resources.get(key)
570 v = self._resources[key](self, mapping, key)
571 if v is None:
571 if v is None:
572 raise templateutil.ResourceUnavailable(
572 raise templateutil.ResourceUnavailable(
573 _('template resource not available: %s') % key)
573 _('template resource not available: %s') % key)
574 return v
574 return v
575
575
576 def _load(self, t):
576 def _load(self, t):
577 '''load, parse, and cache a template'''
577 '''load, parse, and cache a template'''
578 if t not in self._cache:
578 if t not in self._cache:
579 # put poison to cut recursion while compiling 't'
579 # put poison to cut recursion while compiling 't'
580 self._cache[t] = (_runrecursivesymbol, t)
580 self._cache[t] = (_runrecursivesymbol, t)
581 try:
581 try:
582 x = parse(self._loader(t))
582 x = parse(self._loader(t))
583 if self._aliasmap:
583 if self._aliasmap:
584 x = _aliasrules.expand(self._aliasmap, x)
584 x = _aliasrules.expand(self._aliasmap, x)
585 self._cache[t] = compileexp(x, self, methods)
585 self._cache[t] = compileexp(x, self, methods)
586 except: # re-raises
586 except: # re-raises
587 del self._cache[t]
587 del self._cache[t]
588 raise
588 raise
589 return self._cache[t]
589 return self._cache[t]
590
590
591 def process(self, t, mapping):
591 def process(self, t, mapping):
592 '''Perform expansion. t is name of map element to expand.
592 '''Perform expansion. t is name of map element to expand.
593 mapping contains added elements for use during expansion. Is a
593 mapping contains added elements for use during expansion. Is a
594 generator.'''
594 generator.'''
595 func, data = self._load(t)
595 func, data = self._load(t)
596 return _flatten(func(self, mapping, data))
596 return _flatten(func(self, mapping, data))
597
597
598 engines = {'default': engine}
598 engines = {'default': engine}
599
599
600 def stylelist():
600 def stylelist():
601 paths = templatepaths()
601 paths = templatepaths()
602 if not paths:
602 if not paths:
603 return _('no templates found, try `hg debuginstall` for more info')
603 return _('no templates found, try `hg debuginstall` for more info')
604 dirlist = os.listdir(paths[0])
604 dirlist = os.listdir(paths[0])
605 stylelist = []
605 stylelist = []
606 for file in dirlist:
606 for file in dirlist:
607 split = file.split(".")
607 split = file.split(".")
608 if split[-1] in ('orig', 'rej'):
608 if split[-1] in ('orig', 'rej'):
609 continue
609 continue
610 if split[0] == "map-cmdline":
610 if split[0] == "map-cmdline":
611 stylelist.append(split[1])
611 stylelist.append(split[1])
612 return ", ".join(sorted(stylelist))
612 return ", ".join(sorted(stylelist))
613
613
614 def _readmapfile(mapfile):
614 def _readmapfile(mapfile):
615 """Load template elements from the given map file"""
615 """Load template elements from the given map file"""
616 if not os.path.exists(mapfile):
616 if not os.path.exists(mapfile):
617 raise error.Abort(_("style '%s' not found") % mapfile,
617 raise error.Abort(_("style '%s' not found") % mapfile,
618 hint=_("available styles: %s") % stylelist())
618 hint=_("available styles: %s") % stylelist())
619
619
620 base = os.path.dirname(mapfile)
620 base = os.path.dirname(mapfile)
621 conf = config.config(includepaths=templatepaths())
621 conf = config.config(includepaths=templatepaths())
622 conf.read(mapfile, remap={'': 'templates'})
622 conf.read(mapfile, remap={'': 'templates'})
623
623
624 cache = {}
624 cache = {}
625 tmap = {}
625 tmap = {}
626 aliases = []
626 aliases = []
627
627
628 val = conf.get('templates', '__base__')
628 val = conf.get('templates', '__base__')
629 if val and val[0] not in "'\"":
629 if val and val[0] not in "'\"":
630 # treat as a pointer to a base class for this style
630 # treat as a pointer to a base class for this style
631 path = util.normpath(os.path.join(base, val))
631 path = util.normpath(os.path.join(base, val))
632
632
633 # fallback check in template paths
633 # fallback check in template paths
634 if not os.path.exists(path):
634 if not os.path.exists(path):
635 for p in templatepaths():
635 for p in templatepaths():
636 p2 = util.normpath(os.path.join(p, val))
636 p2 = util.normpath(os.path.join(p, val))
637 if os.path.isfile(p2):
637 if os.path.isfile(p2):
638 path = p2
638 path = p2
639 break
639 break
640 p3 = util.normpath(os.path.join(p2, "map"))
640 p3 = util.normpath(os.path.join(p2, "map"))
641 if os.path.isfile(p3):
641 if os.path.isfile(p3):
642 path = p3
642 path = p3
643 break
643 break
644
644
645 cache, tmap, aliases = _readmapfile(path)
645 cache, tmap, aliases = _readmapfile(path)
646
646
647 for key, val in conf['templates'].items():
647 for key, val in conf['templates'].items():
648 if not val:
648 if not val:
649 raise error.ParseError(_('missing value'),
649 raise error.ParseError(_('missing value'),
650 conf.source('templates', key))
650 conf.source('templates', key))
651 if val[0] in "'\"":
651 if val[0] in "'\"":
652 if val[0] != val[-1]:
652 if val[0] != val[-1]:
653 raise error.ParseError(_('unmatched quotes'),
653 raise error.ParseError(_('unmatched quotes'),
654 conf.source('templates', key))
654 conf.source('templates', key))
655 cache[key] = unquotestring(val)
655 cache[key] = unquotestring(val)
656 elif key != '__base__':
656 elif key != '__base__':
657 val = 'default', val
657 val = 'default', val
658 if ':' in val[1]:
658 if ':' in val[1]:
659 val = val[1].split(':', 1)
659 val = val[1].split(':', 1)
660 tmap[key] = val[0], os.path.join(base, val[1])
660 tmap[key] = val[0], os.path.join(base, val[1])
661 aliases.extend(conf['templatealias'].items())
661 aliases.extend(conf['templatealias'].items())
662 return cache, tmap, aliases
662 return cache, tmap, aliases
663
663
664 class templater(object):
664 class templater(object):
665
665
666 def __init__(self, filters=None, defaults=None, resources=None,
666 def __init__(self, filters=None, defaults=None, resources=None,
667 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
667 cache=None, aliases=(), minchunk=1024, maxchunk=65536):
668 """Create template engine optionally with preloaded template fragments
668 """Create template engine optionally with preloaded template fragments
669
669
670 - ``filters``: a dict of functions to transform a value into another.
670 - ``filters``: a dict of functions to transform a value into another.
671 - ``defaults``: a dict of symbol values/functions; may be overridden
671 - ``defaults``: a dict of symbol values/functions; may be overridden
672 by a ``mapping`` dict.
672 by a ``mapping`` dict.
673 - ``resources``: a dict of internal data (e.g. cache), inaccessible
673 - ``resources``: a dict of functions returning internal data
674 from user template; may be overridden by a ``mapping`` dict.
674 (e.g. cache), inaccessible from user template; may be overridden by
675 a ``mapping`` dict.
675 - ``cache``: a dict of preloaded template fragments.
676 - ``cache``: a dict of preloaded template fragments.
676 - ``aliases``: a list of alias (name, replacement) pairs.
677 - ``aliases``: a list of alias (name, replacement) pairs.
677
678
678 self.cache may be updated later to register additional template
679 self.cache may be updated later to register additional template
679 fragments.
680 fragments.
680 """
681 """
681 if filters is None:
682 if filters is None:
682 filters = {}
683 filters = {}
683 if defaults is None:
684 if defaults is None:
684 defaults = {}
685 defaults = {}
685 if resources is None:
686 if resources is None:
686 resources = {}
687 resources = {}
687 if cache is None:
688 if cache is None:
688 cache = {}
689 cache = {}
689 self.cache = cache.copy()
690 self.cache = cache.copy()
690 self.map = {}
691 self.map = {}
691 self.filters = templatefilters.filters.copy()
692 self.filters = templatefilters.filters.copy()
692 self.filters.update(filters)
693 self.filters.update(filters)
693 self.defaults = defaults
694 self.defaults = defaults
694 self._resources = {'templ': self}
695 self._resources = {'templ': lambda context, mapping, key: self}
695 self._resources.update(resources)
696 self._resources.update(resources)
696 self._aliases = aliases
697 self._aliases = aliases
697 self.minchunk, self.maxchunk = minchunk, maxchunk
698 self.minchunk, self.maxchunk = minchunk, maxchunk
698 self.ecache = {}
699 self.ecache = {}
699
700
700 @classmethod
701 @classmethod
701 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
702 def frommapfile(cls, mapfile, filters=None, defaults=None, resources=None,
702 cache=None, minchunk=1024, maxchunk=65536):
703 cache=None, minchunk=1024, maxchunk=65536):
703 """Create templater from the specified map file"""
704 """Create templater from the specified map file"""
704 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
705 t = cls(filters, defaults, resources, cache, [], minchunk, maxchunk)
705 cache, tmap, aliases = _readmapfile(mapfile)
706 cache, tmap, aliases = _readmapfile(mapfile)
706 t.cache.update(cache)
707 t.cache.update(cache)
707 t.map = tmap
708 t.map = tmap
708 t._aliases = aliases
709 t._aliases = aliases
709 return t
710 return t
710
711
711 def __contains__(self, key):
712 def __contains__(self, key):
712 return key in self.cache or key in self.map
713 return key in self.cache or key in self.map
713
714
714 def load(self, t):
715 def load(self, t):
715 '''Get the template for the given template name. Use a local cache.'''
716 '''Get the template for the given template name. Use a local cache.'''
716 if t not in self.cache:
717 if t not in self.cache:
717 try:
718 try:
718 self.cache[t] = util.readfile(self.map[t][1])
719 self.cache[t] = util.readfile(self.map[t][1])
719 except KeyError as inst:
720 except KeyError as inst:
720 raise templateutil.TemplateNotFound(
721 raise templateutil.TemplateNotFound(
721 _('"%s" not in template map') % inst.args[0])
722 _('"%s" not in template map') % inst.args[0])
722 except IOError as inst:
723 except IOError as inst:
723 reason = (_('template file %s: %s')
724 reason = (_('template file %s: %s')
724 % (self.map[t][1], util.forcebytestr(inst.args[1])))
725 % (self.map[t][1], util.forcebytestr(inst.args[1])))
725 raise IOError(inst.args[0], encoding.strfromlocal(reason))
726 raise IOError(inst.args[0], encoding.strfromlocal(reason))
726 return self.cache[t]
727 return self.cache[t]
727
728
728 def render(self, mapping):
729 def render(self, mapping):
729 """Render the default unnamed template and return result as string"""
730 """Render the default unnamed template and return result as string"""
730 mapping = pycompat.strkwargs(mapping)
731 mapping = pycompat.strkwargs(mapping)
731 return templateutil.stringify(self('', **mapping))
732 return templateutil.stringify(self('', **mapping))
732
733
733 def __call__(self, t, **mapping):
734 def __call__(self, t, **mapping):
734 mapping = pycompat.byteskwargs(mapping)
735 mapping = pycompat.byteskwargs(mapping)
735 ttype = t in self.map and self.map[t][0] or 'default'
736 ttype = t in self.map and self.map[t][0] or 'default'
736 if ttype not in self.ecache:
737 if ttype not in self.ecache:
737 try:
738 try:
738 ecls = engines[ttype]
739 ecls = engines[ttype]
739 except KeyError:
740 except KeyError:
740 raise error.Abort(_('invalid template engine: %s') % ttype)
741 raise error.Abort(_('invalid template engine: %s') % ttype)
741 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
742 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
742 self._resources, self._aliases)
743 self._resources, self._aliases)
743 proc = self.ecache[ttype]
744 proc = self.ecache[ttype]
744
745
745 stream = proc.process(t, mapping)
746 stream = proc.process(t, mapping)
746 if self.minchunk:
747 if self.minchunk:
747 stream = util.increasingchunks(stream, min=self.minchunk,
748 stream = util.increasingchunks(stream, min=self.minchunk,
748 max=self.maxchunk)
749 max=self.maxchunk)
749 return stream
750 return stream
750
751
751 def templatepaths():
752 def templatepaths():
752 '''return locations used for template files.'''
753 '''return locations used for template files.'''
753 pathsrel = ['templates']
754 pathsrel = ['templates']
754 paths = [os.path.normpath(os.path.join(util.datapath, f))
755 paths = [os.path.normpath(os.path.join(util.datapath, f))
755 for f in pathsrel]
756 for f in pathsrel]
756 return [p for p in paths if os.path.isdir(p)]
757 return [p for p in paths if os.path.isdir(p)]
757
758
758 def templatepath(name):
759 def templatepath(name):
759 '''return location of template file. returns None if not found.'''
760 '''return location of template file. returns None if not found.'''
760 for p in templatepaths():
761 for p in templatepaths():
761 f = os.path.join(p, name)
762 f = os.path.join(p, name)
762 if os.path.exists(f):
763 if os.path.exists(f):
763 return f
764 return f
764 return None
765 return None
765
766
766 def stylemap(styles, paths=None):
767 def stylemap(styles, paths=None):
767 """Return path to mapfile for a given style.
768 """Return path to mapfile for a given style.
768
769
769 Searches mapfile in the following locations:
770 Searches mapfile in the following locations:
770 1. templatepath/style/map
771 1. templatepath/style/map
771 2. templatepath/map-style
772 2. templatepath/map-style
772 3. templatepath/map
773 3. templatepath/map
773 """
774 """
774
775
775 if paths is None:
776 if paths is None:
776 paths = templatepaths()
777 paths = templatepaths()
777 elif isinstance(paths, bytes):
778 elif isinstance(paths, bytes):
778 paths = [paths]
779 paths = [paths]
779
780
780 if isinstance(styles, bytes):
781 if isinstance(styles, bytes):
781 styles = [styles]
782 styles = [styles]
782
783
783 for style in styles:
784 for style in styles:
784 # only plain name is allowed to honor template paths
785 # only plain name is allowed to honor template paths
785 if (not style
786 if (not style
786 or style in (pycompat.oscurdir, pycompat.ospardir)
787 or style in (pycompat.oscurdir, pycompat.ospardir)
787 or pycompat.ossep in style
788 or pycompat.ossep in style
788 or pycompat.osaltsep and pycompat.osaltsep in style):
789 or pycompat.osaltsep and pycompat.osaltsep in style):
789 continue
790 continue
790 locations = [os.path.join(style, 'map'), 'map-' + style]
791 locations = [os.path.join(style, 'map'), 'map-' + style]
791 locations.append('map')
792 locations.append('map')
792
793
793 for path in paths:
794 for path in paths:
794 for location in locations:
795 for location in locations:
795 mapfile = os.path.join(path, location)
796 mapfile = os.path.join(path, location)
796 if os.path.isfile(mapfile):
797 if os.path.isfile(mapfile):
797 return style, mapfile
798 return style, mapfile
798
799
799 raise RuntimeError("No hgweb templates found in %r" % paths)
800 raise RuntimeError("No hgweb templates found in %r" % paths)
@@ -1,448 +1,449
1 # templateutil.py - utility for template evaluation
1 # templateutil.py - utility for template evaluation
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import types
10 import types
11
11
12 from .i18n import _
12 from .i18n import _
13 from . import (
13 from . import (
14 error,
14 error,
15 pycompat,
15 pycompat,
16 util,
16 util,
17 )
17 )
18
18
19 class ResourceUnavailable(error.Abort):
19 class ResourceUnavailable(error.Abort):
20 pass
20 pass
21
21
22 class TemplateNotFound(error.Abort):
22 class TemplateNotFound(error.Abort):
23 pass
23 pass
24
24
25 class hybrid(object):
25 class hybrid(object):
26 """Wrapper for list or dict to support legacy template
26 """Wrapper for list or dict to support legacy template
27
27
28 This class allows us to handle both:
28 This class allows us to handle both:
29 - "{files}" (legacy command-line-specific list hack) and
29 - "{files}" (legacy command-line-specific list hack) and
30 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
30 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
31 and to access raw values:
31 and to access raw values:
32 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
32 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
33 - "{get(extras, key)}"
33 - "{get(extras, key)}"
34 - "{files|json}"
34 - "{files|json}"
35 """
35 """
36
36
37 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
37 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
38 if gen is not None:
38 if gen is not None:
39 self.gen = gen # generator or function returning generator
39 self.gen = gen # generator or function returning generator
40 self._values = values
40 self._values = values
41 self._makemap = makemap
41 self._makemap = makemap
42 self.joinfmt = joinfmt
42 self.joinfmt = joinfmt
43 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
43 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
44 def gen(self):
44 def gen(self):
45 """Default generator to stringify this as {join(self, ' ')}"""
45 """Default generator to stringify this as {join(self, ' ')}"""
46 for i, x in enumerate(self._values):
46 for i, x in enumerate(self._values):
47 if i > 0:
47 if i > 0:
48 yield ' '
48 yield ' '
49 yield self.joinfmt(x)
49 yield self.joinfmt(x)
50 def itermaps(self):
50 def itermaps(self):
51 makemap = self._makemap
51 makemap = self._makemap
52 for x in self._values:
52 for x in self._values:
53 yield makemap(x)
53 yield makemap(x)
54 def __contains__(self, x):
54 def __contains__(self, x):
55 return x in self._values
55 return x in self._values
56 def __getitem__(self, key):
56 def __getitem__(self, key):
57 return self._values[key]
57 return self._values[key]
58 def __len__(self):
58 def __len__(self):
59 return len(self._values)
59 return len(self._values)
60 def __iter__(self):
60 def __iter__(self):
61 return iter(self._values)
61 return iter(self._values)
62 def __getattr__(self, name):
62 def __getattr__(self, name):
63 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
63 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
64 r'itervalues', r'keys', r'values'):
64 r'itervalues', r'keys', r'values'):
65 raise AttributeError(name)
65 raise AttributeError(name)
66 return getattr(self._values, name)
66 return getattr(self._values, name)
67
67
68 class mappable(object):
68 class mappable(object):
69 """Wrapper for non-list/dict object to support map operation
69 """Wrapper for non-list/dict object to support map operation
70
70
71 This class allows us to handle both:
71 This class allows us to handle both:
72 - "{manifest}"
72 - "{manifest}"
73 - "{manifest % '{rev}:{node}'}"
73 - "{manifest % '{rev}:{node}'}"
74 - "{manifest.rev}"
74 - "{manifest.rev}"
75
75
76 Unlike a hybrid, this does not simulate the behavior of the underling
76 Unlike a hybrid, this does not simulate the behavior of the underling
77 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
77 value. Use unwrapvalue() or unwraphybrid() to obtain the inner object.
78 """
78 """
79
79
80 def __init__(self, gen, key, value, makemap):
80 def __init__(self, gen, key, value, makemap):
81 if gen is not None:
81 if gen is not None:
82 self.gen = gen # generator or function returning generator
82 self.gen = gen # generator or function returning generator
83 self._key = key
83 self._key = key
84 self._value = value # may be generator of strings
84 self._value = value # may be generator of strings
85 self._makemap = makemap
85 self._makemap = makemap
86
86
87 def gen(self):
87 def gen(self):
88 yield pycompat.bytestr(self._value)
88 yield pycompat.bytestr(self._value)
89
89
90 def tomap(self):
90 def tomap(self):
91 return self._makemap(self._key)
91 return self._makemap(self._key)
92
92
93 def itermaps(self):
93 def itermaps(self):
94 yield self.tomap()
94 yield self.tomap()
95
95
96 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
96 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
97 """Wrap data to support both dict-like and string-like operations"""
97 """Wrap data to support both dict-like and string-like operations"""
98 prefmt = pycompat.identity
98 prefmt = pycompat.identity
99 if fmt is None:
99 if fmt is None:
100 fmt = '%s=%s'
100 fmt = '%s=%s'
101 prefmt = pycompat.bytestr
101 prefmt = pycompat.bytestr
102 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
102 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
103 lambda k: fmt % (prefmt(k), prefmt(data[k])))
103 lambda k: fmt % (prefmt(k), prefmt(data[k])))
104
104
105 def hybridlist(data, name, fmt=None, gen=None):
105 def hybridlist(data, name, fmt=None, gen=None):
106 """Wrap data to support both list-like and string-like operations"""
106 """Wrap data to support both list-like and string-like operations"""
107 prefmt = pycompat.identity
107 prefmt = pycompat.identity
108 if fmt is None:
108 if fmt is None:
109 fmt = '%s'
109 fmt = '%s'
110 prefmt = pycompat.bytestr
110 prefmt = pycompat.bytestr
111 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
111 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
112
112
113 def unwraphybrid(thing):
113 def unwraphybrid(thing):
114 """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
115 template"""
115 template"""
116 gen = getattr(thing, 'gen', None)
116 gen = getattr(thing, 'gen', None)
117 if gen is None:
117 if gen is None:
118 return thing
118 return thing
119 if callable(gen):
119 if callable(gen):
120 return gen()
120 return gen()
121 return gen
121 return gen
122
122
123 def unwrapvalue(thing):
123 def unwrapvalue(thing):
124 """Move the inner value object out of the wrapper"""
124 """Move the inner value object out of the wrapper"""
125 if not util.safehasattr(thing, '_value'):
125 if not util.safehasattr(thing, '_value'):
126 return thing
126 return thing
127 return thing._value
127 return thing._value
128
128
129 def wraphybridvalue(container, key, value):
129 def wraphybridvalue(container, key, value):
130 """Wrap an element of hybrid container to be mappable
130 """Wrap an element of hybrid container to be mappable
131
131
132 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
133 should be an item generated by iter(container).
133 should be an item generated by iter(container).
134 """
134 """
135 makemap = getattr(container, '_makemap', None)
135 makemap = getattr(container, '_makemap', None)
136 if makemap is None:
136 if makemap is None:
137 return value
137 return value
138 if util.safehasattr(value, '_makemap'):
138 if util.safehasattr(value, '_makemap'):
139 # 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
140 return value
140 return value
141 return mappable(None, key, value, makemap)
141 return mappable(None, key, value, makemap)
142
142
143 def compatdict(context, mapping, name, data, key='key', value='value',
143 def compatdict(context, mapping, name, data, key='key', value='value',
144 fmt=None, plural=None, separator=' '):
144 fmt=None, plural=None, separator=' '):
145 """Wrap data like hybriddict(), but also supports old-style list template
145 """Wrap data like hybriddict(), but also supports old-style list template
146
146
147 This exists for backward compatibility with the old-style template. Use
147 This exists for backward compatibility with the old-style template. Use
148 hybriddict() for new template keywords.
148 hybriddict() for new template keywords.
149 """
149 """
150 c = [{key: k, value: v} for k, v in data.iteritems()]
150 c = [{key: k, value: v} for k, v in data.iteritems()]
151 t = context.resource(mapping, 'templ')
151 t = context.resource(mapping, 'templ')
152 f = _showlist(name, c, t, mapping, plural, separator)
152 f = _showlist(name, c, t, mapping, plural, separator)
153 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
153 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
154
154
155 def compatlist(context, mapping, name, data, element=None, fmt=None,
155 def compatlist(context, mapping, name, data, element=None, fmt=None,
156 plural=None, separator=' '):
156 plural=None, separator=' '):
157 """Wrap data like hybridlist(), but also supports old-style list template
157 """Wrap data like hybridlist(), but also supports old-style list template
158
158
159 This exists for backward compatibility with the old-style template. Use
159 This exists for backward compatibility with the old-style template. Use
160 hybridlist() for new template keywords.
160 hybridlist() for new template keywords.
161 """
161 """
162 t = context.resource(mapping, 'templ')
162 t = context.resource(mapping, 'templ')
163 f = _showlist(name, data, t, mapping, plural, separator)
163 f = _showlist(name, data, t, mapping, plural, separator)
164 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
164 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
165
165
166 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
166 def _showlist(name, values, templ, mapping, plural=None, separator=' '):
167 '''expand set of values.
167 '''expand set of values.
168 name is name of key in template map.
168 name is name of key in template map.
169 values is list of strings or dicts.
169 values is list of strings or dicts.
170 plural is plural of name, if not simply name + 's'.
170 plural is plural of name, if not simply name + 's'.
171 separator is used to join values as a string
171 separator is used to join values as a string
172
172
173 expansion works like this, given name 'foo'.
173 expansion works like this, given name 'foo'.
174
174
175 if values is empty, expand 'no_foos'.
175 if values is empty, expand 'no_foos'.
176
176
177 if 'foo' not in template map, return values as a string,
177 if 'foo' not in template map, return values as a string,
178 joined by 'separator'.
178 joined by 'separator'.
179
179
180 expand 'start_foos'.
180 expand 'start_foos'.
181
181
182 for each value, expand 'foo'. if 'last_foo' in template
182 for each value, expand 'foo'. if 'last_foo' in template
183 map, expand it instead of 'foo' for last key.
183 map, expand it instead of 'foo' for last key.
184
184
185 expand 'end_foos'.
185 expand 'end_foos'.
186 '''
186 '''
187 strmapping = pycompat.strkwargs(mapping)
187 strmapping = pycompat.strkwargs(mapping)
188 if not plural:
188 if not plural:
189 plural = name + 's'
189 plural = name + 's'
190 if not values:
190 if not values:
191 noname = 'no_' + plural
191 noname = 'no_' + plural
192 if noname in templ:
192 if noname in templ:
193 yield templ(noname, **strmapping)
193 yield templ(noname, **strmapping)
194 return
194 return
195 if name not in templ:
195 if name not in templ:
196 if isinstance(values[0], bytes):
196 if isinstance(values[0], bytes):
197 yield separator.join(values)
197 yield separator.join(values)
198 else:
198 else:
199 for v in values:
199 for v in values:
200 r = dict(v)
200 r = dict(v)
201 r.update(mapping)
201 r.update(mapping)
202 yield r
202 yield r
203 return
203 return
204 startname = 'start_' + plural
204 startname = 'start_' + plural
205 if startname in templ:
205 if startname in templ:
206 yield templ(startname, **strmapping)
206 yield templ(startname, **strmapping)
207 vmapping = mapping.copy()
207 vmapping = mapping.copy()
208 def one(v, tag=name):
208 def one(v, tag=name):
209 try:
209 try:
210 vmapping.update(v)
210 vmapping.update(v)
211 # Python 2 raises ValueError if the type of v is wrong. Python
211 # Python 2 raises ValueError if the type of v is wrong. Python
212 # 3 raises TypeError.
212 # 3 raises TypeError.
213 except (AttributeError, TypeError, ValueError):
213 except (AttributeError, TypeError, ValueError):
214 try:
214 try:
215 # Python 2 raises ValueError trying to destructure an e.g.
215 # Python 2 raises ValueError trying to destructure an e.g.
216 # bytes. Python 3 raises TypeError.
216 # bytes. Python 3 raises TypeError.
217 for a, b in v:
217 for a, b in v:
218 vmapping[a] = b
218 vmapping[a] = b
219 except (TypeError, ValueError):
219 except (TypeError, ValueError):
220 vmapping[name] = v
220 vmapping[name] = v
221 return templ(tag, **pycompat.strkwargs(vmapping))
221 return templ(tag, **pycompat.strkwargs(vmapping))
222 lastname = 'last_' + name
222 lastname = 'last_' + name
223 if lastname in templ:
223 if lastname in templ:
224 last = values.pop()
224 last = values.pop()
225 else:
225 else:
226 last = None
226 last = None
227 for v in values:
227 for v in values:
228 yield one(v)
228 yield one(v)
229 if last is not None:
229 if last is not None:
230 yield one(last, tag=lastname)
230 yield one(last, tag=lastname)
231 endname = 'end_' + plural
231 endname = 'end_' + plural
232 if endname in templ:
232 if endname in templ:
233 yield templ(endname, **strmapping)
233 yield templ(endname, **strmapping)
234
234
235 def stringify(thing):
235 def stringify(thing):
236 """Turn values into bytes by converting into text and concatenating them"""
236 """Turn values into bytes by converting into text and concatenating them"""
237 thing = unwraphybrid(thing)
237 thing = unwraphybrid(thing)
238 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
238 if util.safehasattr(thing, '__iter__') and not isinstance(thing, bytes):
239 if isinstance(thing, str):
239 if isinstance(thing, str):
240 # This is only reachable on Python 3 (otherwise
240 # This is only reachable on Python 3 (otherwise
241 # isinstance(thing, bytes) would have been true), and is
241 # isinstance(thing, bytes) would have been true), and is
242 # here to prevent infinite recursion bugs on Python 3.
242 # here to prevent infinite recursion bugs on Python 3.
243 raise error.ProgrammingError(
243 raise error.ProgrammingError(
244 'stringify got unexpected unicode string: %r' % thing)
244 'stringify got unexpected unicode string: %r' % thing)
245 return "".join([stringify(t) for t in thing if t is not None])
245 return "".join([stringify(t) for t in thing if t is not None])
246 if thing is None:
246 if thing is None:
247 return ""
247 return ""
248 return pycompat.bytestr(thing)
248 return pycompat.bytestr(thing)
249
249
250 def findsymbolicname(arg):
250 def findsymbolicname(arg):
251 """Find symbolic name for the given compiled expression; returns None
251 """Find symbolic name for the given compiled expression; returns None
252 if nothing found reliably"""
252 if nothing found reliably"""
253 while True:
253 while True:
254 func, data = arg
254 func, data = arg
255 if func is runsymbol:
255 if func is runsymbol:
256 return data
256 return data
257 elif func is runfilter:
257 elif func is runfilter:
258 arg = data[0]
258 arg = data[0]
259 else:
259 else:
260 return None
260 return None
261
261
262 def evalrawexp(context, mapping, arg):
262 def evalrawexp(context, mapping, arg):
263 """Evaluate given argument as a bare template object which may require
263 """Evaluate given argument as a bare template object which may require
264 further processing (such as folding generator of strings)"""
264 further processing (such as folding generator of strings)"""
265 func, data = arg
265 func, data = arg
266 return func(context, mapping, data)
266 return func(context, mapping, data)
267
267
268 def evalfuncarg(context, mapping, arg):
268 def evalfuncarg(context, mapping, arg):
269 """Evaluate given argument as value type"""
269 """Evaluate given argument as value type"""
270 thing = evalrawexp(context, mapping, arg)
270 thing = evalrawexp(context, mapping, arg)
271 thing = unwrapvalue(thing)
271 thing = unwrapvalue(thing)
272 # evalrawexp() may return string, generator of strings or arbitrary object
272 # evalrawexp() may return string, generator of strings or arbitrary object
273 # such as date tuple, but filter does not want generator.
273 # such as date tuple, but filter does not want generator.
274 if isinstance(thing, types.GeneratorType):
274 if isinstance(thing, types.GeneratorType):
275 thing = stringify(thing)
275 thing = stringify(thing)
276 return thing
276 return thing
277
277
278 def evalboolean(context, mapping, arg):
278 def evalboolean(context, mapping, arg):
279 """Evaluate given argument as boolean, but also takes boolean literals"""
279 """Evaluate given argument as boolean, but also takes boolean literals"""
280 func, data = arg
280 func, data = arg
281 if func is runsymbol:
281 if func is runsymbol:
282 thing = func(context, mapping, data, default=None)
282 thing = func(context, mapping, data, default=None)
283 if thing is None:
283 if thing is None:
284 # not a template keyword, takes as a boolean literal
284 # not a template keyword, takes as a boolean literal
285 thing = util.parsebool(data)
285 thing = util.parsebool(data)
286 else:
286 else:
287 thing = func(context, mapping, data)
287 thing = func(context, mapping, data)
288 thing = unwrapvalue(thing)
288 thing = unwrapvalue(thing)
289 if isinstance(thing, bool):
289 if isinstance(thing, bool):
290 return thing
290 return thing
291 # other objects are evaluated as strings, which means 0 is True, but
291 # other objects are evaluated as strings, which means 0 is True, but
292 # empty dict/list should be False as they are expected to be ''
292 # empty dict/list should be False as they are expected to be ''
293 return bool(stringify(thing))
293 return bool(stringify(thing))
294
294
295 def evalinteger(context, mapping, arg, err=None):
295 def evalinteger(context, mapping, arg, err=None):
296 v = evalfuncarg(context, mapping, arg)
296 v = evalfuncarg(context, mapping, arg)
297 try:
297 try:
298 return int(v)
298 return int(v)
299 except (TypeError, ValueError):
299 except (TypeError, ValueError):
300 raise error.ParseError(err or _('not an integer'))
300 raise error.ParseError(err or _('not an integer'))
301
301
302 def evalstring(context, mapping, arg):
302 def evalstring(context, mapping, arg):
303 return stringify(evalrawexp(context, mapping, arg))
303 return stringify(evalrawexp(context, mapping, arg))
304
304
305 def evalstringliteral(context, mapping, arg):
305 def evalstringliteral(context, mapping, arg):
306 """Evaluate given argument as string template, but returns symbol name
306 """Evaluate given argument as string template, but returns symbol name
307 if it is unknown"""
307 if it is unknown"""
308 func, data = arg
308 func, data = arg
309 if func is runsymbol:
309 if func is runsymbol:
310 thing = func(context, mapping, data, default=data)
310 thing = func(context, mapping, data, default=data)
311 else:
311 else:
312 thing = func(context, mapping, data)
312 thing = func(context, mapping, data)
313 return stringify(thing)
313 return stringify(thing)
314
314
315 _evalfuncbytype = {
315 _evalfuncbytype = {
316 bool: evalboolean,
316 bool: evalboolean,
317 bytes: evalstring,
317 bytes: evalstring,
318 int: evalinteger,
318 int: evalinteger,
319 }
319 }
320
320
321 def evalastype(context, mapping, arg, typ):
321 def evalastype(context, mapping, arg, typ):
322 """Evaluate given argument and coerce its type"""
322 """Evaluate given argument and coerce its type"""
323 try:
323 try:
324 f = _evalfuncbytype[typ]
324 f = _evalfuncbytype[typ]
325 except KeyError:
325 except KeyError:
326 raise error.ProgrammingError('invalid type specified: %r' % typ)
326 raise error.ProgrammingError('invalid type specified: %r' % typ)
327 return f(context, mapping, arg)
327 return f(context, mapping, arg)
328
328
329 def runinteger(context, mapping, data):
329 def runinteger(context, mapping, data):
330 return int(data)
330 return int(data)
331
331
332 def runstring(context, mapping, data):
332 def runstring(context, mapping, data):
333 return data
333 return data
334
334
335 def _recursivesymbolblocker(key):
335 def _recursivesymbolblocker(key):
336 def showrecursion(**args):
336 def showrecursion(**args):
337 raise error.Abort(_("recursive reference '%s' in template") % key)
337 raise error.Abort(_("recursive reference '%s' in template") % key)
338 return showrecursion
338 return showrecursion
339
339
340 def runsymbol(context, mapping, key, default=''):
340 def runsymbol(context, mapping, key, default=''):
341 v = context.symbol(mapping, key)
341 v = context.symbol(mapping, key)
342 if v is None:
342 if v is None:
343 # put poison to cut recursion. we can't move this to parsing phase
343 # put poison to cut recursion. we can't move this to parsing phase
344 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
344 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
345 safemapping = mapping.copy()
345 safemapping = mapping.copy()
346 safemapping[key] = _recursivesymbolblocker(key)
346 safemapping[key] = _recursivesymbolblocker(key)
347 try:
347 try:
348 v = context.process(key, safemapping)
348 v = context.process(key, safemapping)
349 except TemplateNotFound:
349 except TemplateNotFound:
350 v = default
350 v = default
351 if callable(v) and getattr(v, '_requires', None) is None:
351 if callable(v) and getattr(v, '_requires', None) is None:
352 # old templatekw: expand all keywords and resources
352 # old templatekw: expand all keywords and resources
353 props = context._resources.copy()
353 props = {k: f(context, mapping, k)
354 for k, f in context._resources.items()}
354 props.update(mapping)
355 props.update(mapping)
355 return v(**pycompat.strkwargs(props))
356 return v(**pycompat.strkwargs(props))
356 if callable(v):
357 if callable(v):
357 # new templatekw
358 # new templatekw
358 try:
359 try:
359 return v(context, mapping)
360 return v(context, mapping)
360 except ResourceUnavailable:
361 except ResourceUnavailable:
361 # unsupported keyword is mapped to empty just like unknown keyword
362 # unsupported keyword is mapped to empty just like unknown keyword
362 return None
363 return None
363 return v
364 return v
364
365
365 def runtemplate(context, mapping, template):
366 def runtemplate(context, mapping, template):
366 for arg in template:
367 for arg in template:
367 yield evalrawexp(context, mapping, arg)
368 yield evalrawexp(context, mapping, arg)
368
369
369 def runfilter(context, mapping, data):
370 def runfilter(context, mapping, data):
370 arg, filt = data
371 arg, filt = data
371 thing = evalfuncarg(context, mapping, arg)
372 thing = evalfuncarg(context, mapping, arg)
372 try:
373 try:
373 return filt(thing)
374 return filt(thing)
374 except (ValueError, AttributeError, TypeError):
375 except (ValueError, AttributeError, TypeError):
375 sym = findsymbolicname(arg)
376 sym = findsymbolicname(arg)
376 if sym:
377 if sym:
377 msg = (_("template filter '%s' is not compatible with keyword '%s'")
378 msg = (_("template filter '%s' is not compatible with keyword '%s'")
378 % (pycompat.sysbytes(filt.__name__), sym))
379 % (pycompat.sysbytes(filt.__name__), sym))
379 else:
380 else:
380 msg = (_("incompatible use of template filter '%s'")
381 msg = (_("incompatible use of template filter '%s'")
381 % pycompat.sysbytes(filt.__name__))
382 % pycompat.sysbytes(filt.__name__))
382 raise error.Abort(msg)
383 raise error.Abort(msg)
383
384
384 def runmap(context, mapping, data):
385 def runmap(context, mapping, data):
385 darg, targ = data
386 darg, targ = data
386 d = evalrawexp(context, mapping, darg)
387 d = evalrawexp(context, mapping, darg)
387 if util.safehasattr(d, 'itermaps'):
388 if util.safehasattr(d, 'itermaps'):
388 diter = d.itermaps()
389 diter = d.itermaps()
389 else:
390 else:
390 try:
391 try:
391 diter = iter(d)
392 diter = iter(d)
392 except TypeError:
393 except TypeError:
393 sym = findsymbolicname(darg)
394 sym = findsymbolicname(darg)
394 if sym:
395 if sym:
395 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
396 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
396 else:
397 else:
397 raise error.ParseError(_("%r is not iterable") % d)
398 raise error.ParseError(_("%r is not iterable") % d)
398
399
399 for i, v in enumerate(diter):
400 for i, v in enumerate(diter):
400 lm = mapping.copy()
401 lm = mapping.copy()
401 lm['index'] = i
402 lm['index'] = i
402 if isinstance(v, dict):
403 if isinstance(v, dict):
403 lm.update(v)
404 lm.update(v)
404 lm['originalnode'] = mapping.get('node')
405 lm['originalnode'] = mapping.get('node')
405 yield evalrawexp(context, lm, targ)
406 yield evalrawexp(context, lm, targ)
406 else:
407 else:
407 # v is not an iterable of dicts, this happen when 'key'
408 # v is not an iterable of dicts, this happen when 'key'
408 # has been fully expanded already and format is useless.
409 # has been fully expanded already and format is useless.
409 # If so, return the expanded value.
410 # If so, return the expanded value.
410 yield v
411 yield v
411
412
412 def runmember(context, mapping, data):
413 def runmember(context, mapping, data):
413 darg, memb = data
414 darg, memb = data
414 d = evalrawexp(context, mapping, darg)
415 d = evalrawexp(context, mapping, darg)
415 if util.safehasattr(d, 'tomap'):
416 if util.safehasattr(d, 'tomap'):
416 lm = mapping.copy()
417 lm = mapping.copy()
417 lm.update(d.tomap())
418 lm.update(d.tomap())
418 return runsymbol(context, lm, memb)
419 return runsymbol(context, lm, memb)
419 if util.safehasattr(d, 'get'):
420 if util.safehasattr(d, 'get'):
420 return getdictitem(d, memb)
421 return getdictitem(d, memb)
421
422
422 sym = findsymbolicname(darg)
423 sym = findsymbolicname(darg)
423 if sym:
424 if sym:
424 raise error.ParseError(_("keyword '%s' has no member") % sym)
425 raise error.ParseError(_("keyword '%s' has no member") % sym)
425 else:
426 else:
426 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
427 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
427
428
428 def runnegate(context, mapping, data):
429 def runnegate(context, mapping, data):
429 data = evalinteger(context, mapping, data,
430 data = evalinteger(context, mapping, data,
430 _('negation needs an integer argument'))
431 _('negation needs an integer argument'))
431 return -data
432 return -data
432
433
433 def runarithmetic(context, mapping, data):
434 def runarithmetic(context, mapping, data):
434 func, left, right = data
435 func, left, right = data
435 left = evalinteger(context, mapping, left,
436 left = evalinteger(context, mapping, left,
436 _('arithmetic only defined on integers'))
437 _('arithmetic only defined on integers'))
437 right = evalinteger(context, mapping, right,
438 right = evalinteger(context, mapping, right,
438 _('arithmetic only defined on integers'))
439 _('arithmetic only defined on integers'))
439 try:
440 try:
440 return func(left, right)
441 return func(left, right)
441 except ZeroDivisionError:
442 except ZeroDivisionError:
442 raise error.Abort(_('division by zero is not defined'))
443 raise error.Abort(_('division by zero is not defined'))
443
444
444 def getdictitem(dictarg, key):
445 def getdictitem(dictarg, key):
445 val = dictarg.get(key)
446 val = dictarg.get(key)
446 if val is None:
447 if val is None:
447 return
448 return
448 return wraphybridvalue(dictarg, key, val)
449 return wraphybridvalue(dictarg, key, val)
General Comments 0
You need to be logged in to leave comments. Login now