##// END OF EJS Templates
templatespec: move check for non-unicode to lower-level function...
Martin von Zweigbergk -
r45826:215f08c8 default
parent child Browse files
Show More
@@ -1,858 +1,860 b''
1 # formatter.py - generic output formatting for mercurial
1 # formatter.py - generic output formatting for mercurial
2 #
2 #
3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """Generic output formatting for Mercurial
8 """Generic output formatting for Mercurial
9
9
10 The formatter provides API to show data in various ways. The following
10 The formatter provides API to show data in various ways. The following
11 functions should be used in place of ui.write():
11 functions should be used in place of ui.write():
12
12
13 - fm.write() for unconditional output
13 - fm.write() for unconditional output
14 - fm.condwrite() to show some extra data conditionally in plain output
14 - fm.condwrite() to show some extra data conditionally in plain output
15 - fm.context() to provide changectx to template output
15 - fm.context() to provide changectx to template output
16 - fm.data() to provide extra data to JSON or template output
16 - fm.data() to provide extra data to JSON or template output
17 - fm.plain() to show raw text that isn't provided to JSON or template output
17 - fm.plain() to show raw text that isn't provided to JSON or template output
18
18
19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 beforehand so the data is converted to the appropriate data type. Use
20 beforehand so the data is converted to the appropriate data type. Use
21 fm.isplain() if you need to convert or format data conditionally which isn't
21 fm.isplain() if you need to convert or format data conditionally which isn't
22 supported by the formatter API.
22 supported by the formatter API.
23
23
24 To build nested structure (i.e. a list of dicts), use fm.nested().
24 To build nested structure (i.e. a list of dicts), use fm.nested().
25
25
26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27
27
28 fm.condwrite() vs 'if cond:':
28 fm.condwrite() vs 'if cond:':
29
29
30 In most cases, use fm.condwrite() so users can selectively show the data
30 In most cases, use fm.condwrite() so users can selectively show the data
31 in template output. If it's costly to build data, use plain 'if cond:' with
31 in template output. If it's costly to build data, use plain 'if cond:' with
32 fm.write().
32 fm.write().
33
33
34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35
35
36 fm.nested() should be used to form a tree structure (a list of dicts of
36 fm.nested() should be used to form a tree structure (a list of dicts of
37 lists of dicts...) which can be accessed through template keywords, e.g.
37 lists of dicts...) which can be accessed through template keywords, e.g.
38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 exports a dict-type object to template, which can be accessed by e.g.
39 exports a dict-type object to template, which can be accessed by e.g.
40 "{get(foo, key)}" function.
40 "{get(foo, key)}" function.
41
41
42 Doctest helper:
42 Doctest helper:
43
43
44 >>> def show(fn, verbose=False, **opts):
44 >>> def show(fn, verbose=False, **opts):
45 ... import sys
45 ... import sys
46 ... from . import ui as uimod
46 ... from . import ui as uimod
47 ... ui = uimod.ui()
47 ... ui = uimod.ui()
48 ... ui.verbose = verbose
48 ... ui.verbose = verbose
49 ... ui.pushbuffer()
49 ... ui.pushbuffer()
50 ... try:
50 ... try:
51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
51 ... return fn(ui, ui.formatter(pycompat.sysbytes(fn.__name__),
52 ... pycompat.byteskwargs(opts)))
52 ... pycompat.byteskwargs(opts)))
53 ... finally:
53 ... finally:
54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
54 ... print(pycompat.sysstr(ui.popbuffer()), end='')
55
55
56 Basic example:
56 Basic example:
57
57
58 >>> def files(ui, fm):
58 >>> def files(ui, fm):
59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
59 ... files = [(b'foo', 123, (0, 0)), (b'bar', 456, (1, 0))]
60 ... for f in files:
60 ... for f in files:
61 ... fm.startitem()
61 ... fm.startitem()
62 ... fm.write(b'path', b'%s', f[0])
62 ... fm.write(b'path', b'%s', f[0])
63 ... fm.condwrite(ui.verbose, b'date', b' %s',
63 ... fm.condwrite(ui.verbose, b'date', b' %s',
64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
64 ... fm.formatdate(f[2], b'%Y-%m-%d %H:%M:%S'))
65 ... fm.data(size=f[1])
65 ... fm.data(size=f[1])
66 ... fm.plain(b'\\n')
66 ... fm.plain(b'\\n')
67 ... fm.end()
67 ... fm.end()
68 >>> show(files)
68 >>> show(files)
69 foo
69 foo
70 bar
70 bar
71 >>> show(files, verbose=True)
71 >>> show(files, verbose=True)
72 foo 1970-01-01 00:00:00
72 foo 1970-01-01 00:00:00
73 bar 1970-01-01 00:00:01
73 bar 1970-01-01 00:00:01
74 >>> show(files, template=b'json')
74 >>> show(files, template=b'json')
75 [
75 [
76 {
76 {
77 "date": [0, 0],
77 "date": [0, 0],
78 "path": "foo",
78 "path": "foo",
79 "size": 123
79 "size": 123
80 },
80 },
81 {
81 {
82 "date": [1, 0],
82 "date": [1, 0],
83 "path": "bar",
83 "path": "bar",
84 "size": 456
84 "size": 456
85 }
85 }
86 ]
86 ]
87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
87 >>> show(files, template=b'path: {path}\\ndate: {date|rfc3339date}\\n')
88 path: foo
88 path: foo
89 date: 1970-01-01T00:00:00+00:00
89 date: 1970-01-01T00:00:00+00:00
90 path: bar
90 path: bar
91 date: 1970-01-01T00:00:01+00:00
91 date: 1970-01-01T00:00:01+00:00
92
92
93 Nested example:
93 Nested example:
94
94
95 >>> def subrepos(ui, fm):
95 >>> def subrepos(ui, fm):
96 ... fm.startitem()
96 ... fm.startitem()
97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
97 ... fm.write(b'reponame', b'[%s]\\n', b'baz')
98 ... files(ui, fm.nested(b'files', tmpl=b'{reponame}'))
98 ... files(ui, fm.nested(b'files', tmpl=b'{reponame}'))
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 contextlib
110 import contextlib
111 import itertools
111 import itertools
112 import os
112 import os
113
113
114 from .i18n import _
114 from .i18n import _
115 from .node import (
115 from .node import (
116 hex,
116 hex,
117 short,
117 short,
118 )
118 )
119 from .thirdparty import attr
119 from .thirdparty import attr
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 (
130 from .utils import (
131 cborutil,
131 cborutil,
132 dateutil,
132 dateutil,
133 stringutil,
133 stringutil,
134 )
134 )
135
135
136 pickle = util.pickle
136 pickle = util.pickle
137
137
138
138
139 def isprintable(obj):
139 def isprintable(obj):
140 """Check if the given object can be directly passed in to formatter's
140 """Check if the given object can be directly passed in to formatter's
141 write() and data() functions
141 write() and data() functions
142
142
143 Returns False if the object is unsupported or must be pre-processed by
143 Returns False if the object is unsupported or must be pre-processed by
144 formatdate(), formatdict(), or formatlist().
144 formatdate(), formatdict(), or formatlist().
145 """
145 """
146 return isinstance(obj, (type(None), bool, int, pycompat.long, float, bytes))
146 return isinstance(obj, (type(None), bool, int, pycompat.long, float, bytes))
147
147
148
148
149 class _nullconverter(object):
149 class _nullconverter(object):
150 '''convert non-primitive data types to be processed by formatter'''
150 '''convert non-primitive data types to be processed by formatter'''
151
151
152 # set to True if context object should be stored as item
152 # set to True if context object should be stored as item
153 storecontext = False
153 storecontext = False
154
154
155 @staticmethod
155 @staticmethod
156 def wrapnested(data, tmpl, sep):
156 def wrapnested(data, tmpl, sep):
157 '''wrap nested data by appropriate type'''
157 '''wrap nested data by appropriate type'''
158 return data
158 return data
159
159
160 @staticmethod
160 @staticmethod
161 def formatdate(date, fmt):
161 def formatdate(date, fmt):
162 '''convert date tuple to appropriate format'''
162 '''convert date tuple to appropriate format'''
163 # timestamp can be float, but the canonical form should be int
163 # timestamp can be float, but the canonical form should be int
164 ts, tz = date
164 ts, tz = date
165 return (int(ts), tz)
165 return (int(ts), tz)
166
166
167 @staticmethod
167 @staticmethod
168 def formatdict(data, key, value, fmt, sep):
168 def formatdict(data, key, value, fmt, sep):
169 '''convert dict or key-value pairs to appropriate dict format'''
169 '''convert dict or key-value pairs to appropriate dict format'''
170 # use plain dict instead of util.sortdict so that data can be
170 # use plain dict instead of util.sortdict so that data can be
171 # serialized as a builtin dict in pickle output
171 # serialized as a builtin dict in pickle output
172 return dict(data)
172 return dict(data)
173
173
174 @staticmethod
174 @staticmethod
175 def formatlist(data, name, fmt, sep):
175 def formatlist(data, name, fmt, sep):
176 '''convert iterable to appropriate list format'''
176 '''convert iterable to appropriate list format'''
177 return list(data)
177 return list(data)
178
178
179
179
180 class baseformatter(object):
180 class baseformatter(object):
181 def __init__(self, ui, topic, opts, converter):
181 def __init__(self, ui, topic, opts, converter):
182 self._ui = ui
182 self._ui = ui
183 self._topic = topic
183 self._topic = topic
184 self._opts = opts
184 self._opts = opts
185 self._converter = converter
185 self._converter = converter
186 self._item = None
186 self._item = None
187 # function to convert node to string suitable for this output
187 # function to convert node to string suitable for this output
188 self.hexfunc = hex
188 self.hexfunc = hex
189
189
190 def __enter__(self):
190 def __enter__(self):
191 return self
191 return self
192
192
193 def __exit__(self, exctype, excvalue, traceback):
193 def __exit__(self, exctype, excvalue, traceback):
194 if exctype is None:
194 if exctype is None:
195 self.end()
195 self.end()
196
196
197 def _showitem(self):
197 def _showitem(self):
198 '''show a formatted item once all data is collected'''
198 '''show a formatted item once all data is collected'''
199
199
200 def startitem(self):
200 def startitem(self):
201 '''begin an item in the format list'''
201 '''begin an item in the format list'''
202 if self._item is not None:
202 if self._item is not None:
203 self._showitem()
203 self._showitem()
204 self._item = {}
204 self._item = {}
205
205
206 def formatdate(self, date, fmt=b'%a %b %d %H:%M:%S %Y %1%2'):
206 def formatdate(self, date, fmt=b'%a %b %d %H:%M:%S %Y %1%2'):
207 '''convert date tuple to appropriate format'''
207 '''convert date tuple to appropriate format'''
208 return self._converter.formatdate(date, fmt)
208 return self._converter.formatdate(date, fmt)
209
209
210 def formatdict(self, data, key=b'key', value=b'value', fmt=None, sep=b' '):
210 def formatdict(self, data, key=b'key', value=b'value', fmt=None, sep=b' '):
211 '''convert dict or key-value pairs to appropriate dict format'''
211 '''convert dict or key-value pairs to appropriate dict format'''
212 return self._converter.formatdict(data, key, value, fmt, sep)
212 return self._converter.formatdict(data, key, value, fmt, sep)
213
213
214 def formatlist(self, data, name, fmt=None, sep=b' '):
214 def formatlist(self, data, name, fmt=None, sep=b' '):
215 '''convert iterable to appropriate list format'''
215 '''convert iterable to appropriate list format'''
216 # name is mandatory argument for now, but it could be optional if
216 # name is mandatory argument for now, but it could be optional if
217 # we have default template keyword, e.g. {item}
217 # we have default template keyword, e.g. {item}
218 return self._converter.formatlist(data, name, fmt, sep)
218 return self._converter.formatlist(data, name, fmt, sep)
219
219
220 def context(self, **ctxs):
220 def context(self, **ctxs):
221 '''insert context objects to be used to render template keywords'''
221 '''insert context objects to be used to render template keywords'''
222 ctxs = pycompat.byteskwargs(ctxs)
222 ctxs = pycompat.byteskwargs(ctxs)
223 assert all(k in {b'repo', b'ctx', b'fctx'} for k in ctxs)
223 assert all(k in {b'repo', b'ctx', b'fctx'} for k in ctxs)
224 if self._converter.storecontext:
224 if self._converter.storecontext:
225 # populate missing resources in fctx -> ctx -> repo order
225 # populate missing resources in fctx -> ctx -> repo order
226 if b'fctx' in ctxs and b'ctx' not in ctxs:
226 if b'fctx' in ctxs and b'ctx' not in ctxs:
227 ctxs[b'ctx'] = ctxs[b'fctx'].changectx()
227 ctxs[b'ctx'] = ctxs[b'fctx'].changectx()
228 if b'ctx' in ctxs and b'repo' not in ctxs:
228 if b'ctx' in ctxs and b'repo' not in ctxs:
229 ctxs[b'repo'] = ctxs[b'ctx'].repo()
229 ctxs[b'repo'] = ctxs[b'ctx'].repo()
230 self._item.update(ctxs)
230 self._item.update(ctxs)
231
231
232 def datahint(self):
232 def datahint(self):
233 '''set of field names to be referenced'''
233 '''set of field names to be referenced'''
234 return set()
234 return set()
235
235
236 def data(self, **data):
236 def data(self, **data):
237 '''insert data into item that's not shown in default output'''
237 '''insert data into item that's not shown in default output'''
238 data = pycompat.byteskwargs(data)
238 data = pycompat.byteskwargs(data)
239 self._item.update(data)
239 self._item.update(data)
240
240
241 def write(self, fields, deftext, *fielddata, **opts):
241 def write(self, fields, deftext, *fielddata, **opts):
242 '''do default text output while assigning data to item'''
242 '''do default text output while assigning data to item'''
243 fieldkeys = fields.split()
243 fieldkeys = fields.split()
244 assert len(fieldkeys) == len(fielddata), (fieldkeys, fielddata)
244 assert len(fieldkeys) == len(fielddata), (fieldkeys, fielddata)
245 self._item.update(zip(fieldkeys, fielddata))
245 self._item.update(zip(fieldkeys, fielddata))
246
246
247 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
247 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
248 '''do conditional write (primarily for plain formatter)'''
248 '''do conditional write (primarily for plain formatter)'''
249 fieldkeys = fields.split()
249 fieldkeys = fields.split()
250 assert len(fieldkeys) == len(fielddata)
250 assert len(fieldkeys) == len(fielddata)
251 self._item.update(zip(fieldkeys, fielddata))
251 self._item.update(zip(fieldkeys, fielddata))
252
252
253 def plain(self, text, **opts):
253 def plain(self, text, **opts):
254 '''show raw text for non-templated mode'''
254 '''show raw text for non-templated mode'''
255
255
256 def isplain(self):
256 def isplain(self):
257 '''check for plain formatter usage'''
257 '''check for plain formatter usage'''
258 return False
258 return False
259
259
260 def nested(self, field, tmpl=None, sep=b''):
260 def nested(self, field, tmpl=None, sep=b''):
261 '''sub formatter to store nested data in the specified field'''
261 '''sub formatter to store nested data in the specified field'''
262 data = []
262 data = []
263 self._item[field] = self._converter.wrapnested(data, tmpl, sep)
263 self._item[field] = self._converter.wrapnested(data, tmpl, sep)
264 return _nestedformatter(self._ui, self._converter, data)
264 return _nestedformatter(self._ui, self._converter, data)
265
265
266 def end(self):
266 def end(self):
267 '''end output for the formatter'''
267 '''end output for the formatter'''
268 if self._item is not None:
268 if self._item is not None:
269 self._showitem()
269 self._showitem()
270
270
271
271
272 def nullformatter(ui, topic, opts):
272 def nullformatter(ui, topic, opts):
273 '''formatter that prints nothing'''
273 '''formatter that prints nothing'''
274 return baseformatter(ui, topic, opts, converter=_nullconverter)
274 return baseformatter(ui, topic, opts, converter=_nullconverter)
275
275
276
276
277 class _nestedformatter(baseformatter):
277 class _nestedformatter(baseformatter):
278 '''build sub items and store them in the parent formatter'''
278 '''build sub items and store them in the parent formatter'''
279
279
280 def __init__(self, ui, converter, data):
280 def __init__(self, ui, converter, data):
281 baseformatter.__init__(
281 baseformatter.__init__(
282 self, ui, topic=b'', opts={}, converter=converter
282 self, ui, topic=b'', opts={}, converter=converter
283 )
283 )
284 self._data = data
284 self._data = data
285
285
286 def _showitem(self):
286 def _showitem(self):
287 self._data.append(self._item)
287 self._data.append(self._item)
288
288
289
289
290 def _iteritems(data):
290 def _iteritems(data):
291 '''iterate key-value pairs in stable order'''
291 '''iterate key-value pairs in stable order'''
292 if isinstance(data, dict):
292 if isinstance(data, dict):
293 return sorted(pycompat.iteritems(data))
293 return sorted(pycompat.iteritems(data))
294 return data
294 return data
295
295
296
296
297 class _plainconverter(object):
297 class _plainconverter(object):
298 '''convert non-primitive data types to text'''
298 '''convert non-primitive data types to text'''
299
299
300 storecontext = False
300 storecontext = False
301
301
302 @staticmethod
302 @staticmethod
303 def wrapnested(data, tmpl, sep):
303 def wrapnested(data, tmpl, sep):
304 raise error.ProgrammingError(b'plainformatter should never be nested')
304 raise error.ProgrammingError(b'plainformatter should never be nested')
305
305
306 @staticmethod
306 @staticmethod
307 def formatdate(date, fmt):
307 def formatdate(date, fmt):
308 '''stringify date tuple in the given format'''
308 '''stringify date tuple in the given format'''
309 return dateutil.datestr(date, fmt)
309 return dateutil.datestr(date, fmt)
310
310
311 @staticmethod
311 @staticmethod
312 def formatdict(data, key, value, fmt, sep):
312 def formatdict(data, key, value, fmt, sep):
313 '''stringify key-value pairs separated by sep'''
313 '''stringify key-value pairs separated by sep'''
314 prefmt = pycompat.identity
314 prefmt = pycompat.identity
315 if fmt is None:
315 if fmt is None:
316 fmt = b'%s=%s'
316 fmt = b'%s=%s'
317 prefmt = pycompat.bytestr
317 prefmt = pycompat.bytestr
318 return sep.join(
318 return sep.join(
319 fmt % (prefmt(k), prefmt(v)) for k, v in _iteritems(data)
319 fmt % (prefmt(k), prefmt(v)) for k, v in _iteritems(data)
320 )
320 )
321
321
322 @staticmethod
322 @staticmethod
323 def formatlist(data, name, fmt, sep):
323 def formatlist(data, name, fmt, sep):
324 '''stringify iterable separated by sep'''
324 '''stringify iterable separated by sep'''
325 prefmt = pycompat.identity
325 prefmt = pycompat.identity
326 if fmt is None:
326 if fmt is None:
327 fmt = b'%s'
327 fmt = b'%s'
328 prefmt = pycompat.bytestr
328 prefmt = pycompat.bytestr
329 return sep.join(fmt % prefmt(e) for e in data)
329 return sep.join(fmt % prefmt(e) for e in data)
330
330
331
331
332 class plainformatter(baseformatter):
332 class plainformatter(baseformatter):
333 '''the default text output scheme'''
333 '''the default text output scheme'''
334
334
335 def __init__(self, ui, out, topic, opts):
335 def __init__(self, ui, out, topic, opts):
336 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
336 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
337 if ui.debugflag:
337 if ui.debugflag:
338 self.hexfunc = hex
338 self.hexfunc = hex
339 else:
339 else:
340 self.hexfunc = short
340 self.hexfunc = short
341 if ui is out:
341 if ui is out:
342 self._write = ui.write
342 self._write = ui.write
343 else:
343 else:
344 self._write = lambda s, **opts: out.write(s)
344 self._write = lambda s, **opts: out.write(s)
345
345
346 def startitem(self):
346 def startitem(self):
347 pass
347 pass
348
348
349 def data(self, **data):
349 def data(self, **data):
350 pass
350 pass
351
351
352 def write(self, fields, deftext, *fielddata, **opts):
352 def write(self, fields, deftext, *fielddata, **opts):
353 self._write(deftext % fielddata, **opts)
353 self._write(deftext % fielddata, **opts)
354
354
355 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
355 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
356 '''do conditional write'''
356 '''do conditional write'''
357 if cond:
357 if cond:
358 self._write(deftext % fielddata, **opts)
358 self._write(deftext % fielddata, **opts)
359
359
360 def plain(self, text, **opts):
360 def plain(self, text, **opts):
361 self._write(text, **opts)
361 self._write(text, **opts)
362
362
363 def isplain(self):
363 def isplain(self):
364 return True
364 return True
365
365
366 def nested(self, field, tmpl=None, sep=b''):
366 def nested(self, field, tmpl=None, sep=b''):
367 # nested data will be directly written to ui
367 # nested data will be directly written to ui
368 return self
368 return self
369
369
370 def end(self):
370 def end(self):
371 pass
371 pass
372
372
373
373
374 class debugformatter(baseformatter):
374 class debugformatter(baseformatter):
375 def __init__(self, ui, out, topic, opts):
375 def __init__(self, ui, out, topic, opts):
376 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
376 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
377 self._out = out
377 self._out = out
378 self._out.write(b"%s = [\n" % self._topic)
378 self._out.write(b"%s = [\n" % self._topic)
379
379
380 def _showitem(self):
380 def _showitem(self):
381 self._out.write(
381 self._out.write(
382 b' %s,\n' % stringutil.pprint(self._item, indent=4, level=1)
382 b' %s,\n' % stringutil.pprint(self._item, indent=4, level=1)
383 )
383 )
384
384
385 def end(self):
385 def end(self):
386 baseformatter.end(self)
386 baseformatter.end(self)
387 self._out.write(b"]\n")
387 self._out.write(b"]\n")
388
388
389
389
390 class pickleformatter(baseformatter):
390 class pickleformatter(baseformatter):
391 def __init__(self, ui, out, topic, opts):
391 def __init__(self, ui, out, topic, opts):
392 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
392 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
393 self._out = out
393 self._out = out
394 self._data = []
394 self._data = []
395
395
396 def _showitem(self):
396 def _showitem(self):
397 self._data.append(self._item)
397 self._data.append(self._item)
398
398
399 def end(self):
399 def end(self):
400 baseformatter.end(self)
400 baseformatter.end(self)
401 self._out.write(pickle.dumps(self._data))
401 self._out.write(pickle.dumps(self._data))
402
402
403
403
404 class cborformatter(baseformatter):
404 class cborformatter(baseformatter):
405 '''serialize items as an indefinite-length CBOR array'''
405 '''serialize items as an indefinite-length CBOR array'''
406
406
407 def __init__(self, ui, out, topic, opts):
407 def __init__(self, ui, out, topic, opts):
408 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
408 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
409 self._out = out
409 self._out = out
410 self._out.write(cborutil.BEGIN_INDEFINITE_ARRAY)
410 self._out.write(cborutil.BEGIN_INDEFINITE_ARRAY)
411
411
412 def _showitem(self):
412 def _showitem(self):
413 self._out.write(b''.join(cborutil.streamencode(self._item)))
413 self._out.write(b''.join(cborutil.streamencode(self._item)))
414
414
415 def end(self):
415 def end(self):
416 baseformatter.end(self)
416 baseformatter.end(self)
417 self._out.write(cborutil.BREAK)
417 self._out.write(cborutil.BREAK)
418
418
419
419
420 class jsonformatter(baseformatter):
420 class jsonformatter(baseformatter):
421 def __init__(self, ui, out, topic, opts):
421 def __init__(self, ui, out, topic, opts):
422 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
422 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
423 self._out = out
423 self._out = out
424 self._out.write(b"[")
424 self._out.write(b"[")
425 self._first = True
425 self._first = True
426
426
427 def _showitem(self):
427 def _showitem(self):
428 if self._first:
428 if self._first:
429 self._first = False
429 self._first = False
430 else:
430 else:
431 self._out.write(b",")
431 self._out.write(b",")
432
432
433 self._out.write(b"\n {\n")
433 self._out.write(b"\n {\n")
434 first = True
434 first = True
435 for k, v in sorted(self._item.items()):
435 for k, v in sorted(self._item.items()):
436 if first:
436 if first:
437 first = False
437 first = False
438 else:
438 else:
439 self._out.write(b",\n")
439 self._out.write(b",\n")
440 u = templatefilters.json(v, paranoid=False)
440 u = templatefilters.json(v, paranoid=False)
441 self._out.write(b' "%s": %s' % (k, u))
441 self._out.write(b' "%s": %s' % (k, u))
442 self._out.write(b"\n }")
442 self._out.write(b"\n }")
443
443
444 def end(self):
444 def end(self):
445 baseformatter.end(self)
445 baseformatter.end(self)
446 self._out.write(b"\n]\n")
446 self._out.write(b"\n]\n")
447
447
448
448
449 class _templateconverter(object):
449 class _templateconverter(object):
450 '''convert non-primitive data types to be processed by templater'''
450 '''convert non-primitive data types to be processed by templater'''
451
451
452 storecontext = True
452 storecontext = True
453
453
454 @staticmethod
454 @staticmethod
455 def wrapnested(data, tmpl, sep):
455 def wrapnested(data, tmpl, sep):
456 '''wrap nested data by templatable type'''
456 '''wrap nested data by templatable type'''
457 return templateutil.mappinglist(data, tmpl=tmpl, sep=sep)
457 return templateutil.mappinglist(data, tmpl=tmpl, sep=sep)
458
458
459 @staticmethod
459 @staticmethod
460 def formatdate(date, fmt):
460 def formatdate(date, fmt):
461 '''return date tuple'''
461 '''return date tuple'''
462 return templateutil.date(date)
462 return templateutil.date(date)
463
463
464 @staticmethod
464 @staticmethod
465 def formatdict(data, key, value, fmt, sep):
465 def formatdict(data, key, value, fmt, sep):
466 '''build object that can be evaluated as either plain string or dict'''
466 '''build object that can be evaluated as either plain string or dict'''
467 data = util.sortdict(_iteritems(data))
467 data = util.sortdict(_iteritems(data))
468
468
469 def f():
469 def f():
470 yield _plainconverter.formatdict(data, key, value, fmt, sep)
470 yield _plainconverter.formatdict(data, key, value, fmt, sep)
471
471
472 return templateutil.hybriddict(
472 return templateutil.hybriddict(
473 data, key=key, value=value, fmt=fmt, gen=f
473 data, key=key, value=value, fmt=fmt, gen=f
474 )
474 )
475
475
476 @staticmethod
476 @staticmethod
477 def formatlist(data, name, fmt, sep):
477 def formatlist(data, name, fmt, sep):
478 '''build object that can be evaluated as either plain string or list'''
478 '''build object that can be evaluated as either plain string or list'''
479 data = list(data)
479 data = list(data)
480
480
481 def f():
481 def f():
482 yield _plainconverter.formatlist(data, name, fmt, sep)
482 yield _plainconverter.formatlist(data, name, fmt, sep)
483
483
484 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
484 return templateutil.hybridlist(data, name=name, fmt=fmt, gen=f)
485
485
486
486
487 class templateformatter(baseformatter):
487 class templateformatter(baseformatter):
488 def __init__(self, ui, out, topic, opts, spec, overridetemplates=None):
488 def __init__(self, ui, out, topic, opts, spec, overridetemplates=None):
489 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
489 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
490 self._out = out
490 self._out = out
491 self._tref = spec.ref
491 self._tref = spec.ref
492 self._t = loadtemplater(
492 self._t = loadtemplater(
493 ui,
493 ui,
494 spec,
494 spec,
495 defaults=templatekw.keywords,
495 defaults=templatekw.keywords,
496 resources=templateresources(ui),
496 resources=templateresources(ui),
497 cache=templatekw.defaulttempl,
497 cache=templatekw.defaulttempl,
498 )
498 )
499 if overridetemplates:
499 if overridetemplates:
500 self._t.cache.update(overridetemplates)
500 self._t.cache.update(overridetemplates)
501 self._parts = templatepartsmap(
501 self._parts = templatepartsmap(
502 spec, self._t, [b'docheader', b'docfooter', b'separator']
502 spec, self._t, [b'docheader', b'docfooter', b'separator']
503 )
503 )
504 self._counter = itertools.count()
504 self._counter = itertools.count()
505 self._renderitem(b'docheader', {})
505 self._renderitem(b'docheader', {})
506
506
507 def _showitem(self):
507 def _showitem(self):
508 item = self._item.copy()
508 item = self._item.copy()
509 item[b'index'] = index = next(self._counter)
509 item[b'index'] = index = next(self._counter)
510 if index > 0:
510 if index > 0:
511 self._renderitem(b'separator', {})
511 self._renderitem(b'separator', {})
512 self._renderitem(self._tref, item)
512 self._renderitem(self._tref, item)
513
513
514 def _renderitem(self, part, item):
514 def _renderitem(self, part, item):
515 if part not in self._parts:
515 if part not in self._parts:
516 return
516 return
517 ref = self._parts[part]
517 ref = self._parts[part]
518 # None can't be put in the mapping dict since it means <unset>
518 # None can't be put in the mapping dict since it means <unset>
519 for k, v in item.items():
519 for k, v in item.items():
520 if v is None:
520 if v is None:
521 item[k] = templateutil.wrappedvalue(v)
521 item[k] = templateutil.wrappedvalue(v)
522 self._out.write(self._t.render(ref, item))
522 self._out.write(self._t.render(ref, item))
523
523
524 @util.propertycache
524 @util.propertycache
525 def _symbolsused(self):
525 def _symbolsused(self):
526 return self._t.symbolsused(self._tref)
526 return self._t.symbolsused(self._tref)
527
527
528 def datahint(self):
528 def datahint(self):
529 '''set of field names to be referenced from the template'''
529 '''set of field names to be referenced from the template'''
530 return self._symbolsused[0]
530 return self._symbolsused[0]
531
531
532 def end(self):
532 def end(self):
533 baseformatter.end(self)
533 baseformatter.end(self)
534 self._renderitem(b'docfooter', {})
534 self._renderitem(b'docfooter', {})
535
535
536
536
537 @attr.s(frozen=True)
537 @attr.s(frozen=True)
538 class templatespec(object):
538 class templatespec(object):
539 ref = attr.ib()
539 ref = attr.ib()
540 tmpl = attr.ib()
540 tmpl = attr.ib()
541 mapfile = attr.ib()
541 mapfile = attr.ib()
542 refargs = attr.ib(default=None)
542 refargs = attr.ib(default=None)
543
543
544
544
545 def empty_templatespec():
545 def empty_templatespec():
546 return templatespec(None, None, None)
546 return templatespec(None, None, None)
547
547
548
548
549 def reference_templatespec(ref, refargs=None):
549 def reference_templatespec(ref, refargs=None):
550 return templatespec(ref, None, None, refargs)
550 return templatespec(ref, None, None, refargs)
551
551
552
552
553 def literal_templatespec(tmpl):
553 def literal_templatespec(tmpl):
554 if pycompat.ispy3:
555 assert not isinstance(tmpl, str), b'tmpl must not be a str'
554 return templatespec(b'', tmpl, None)
556 return templatespec(b'', tmpl, None)
555
557
556
558
557 def mapfile_templatespec(topic, mapfile):
559 def mapfile_templatespec(topic, mapfile):
558 return templatespec(topic, None, mapfile)
560 return templatespec(topic, None, mapfile)
559
561
560
562
561 def lookuptemplate(ui, topic, tmpl):
563 def lookuptemplate(ui, topic, tmpl):
562 """Find the template matching the given -T/--template spec 'tmpl'
564 """Find the template matching the given -T/--template spec 'tmpl'
563
565
564 'tmpl' can be any of the following:
566 'tmpl' can be any of the following:
565
567
566 - a literal template (e.g. '{rev}')
568 - a literal template (e.g. '{rev}')
567 - a reference to built-in template (i.e. formatter)
569 - a reference to built-in template (i.e. formatter)
568 - a map-file name or path (e.g. 'changelog')
570 - a map-file name or path (e.g. 'changelog')
569 - a reference to [templates] in config file
571 - a reference to [templates] in config file
570 - a path to raw template file
572 - a path to raw template file
571
573
572 A map file defines a stand-alone template environment. If a map file
574 A map file defines a stand-alone template environment. If a map file
573 selected, all templates defined in the file will be loaded, and the
575 selected, all templates defined in the file will be loaded, and the
574 template matching the given topic will be rendered. Aliases won't be
576 template matching the given topic will be rendered. Aliases won't be
575 loaded from user config, but from the map file.
577 loaded from user config, but from the map file.
576
578
577 If no map file selected, all templates in [templates] section will be
579 If no map file selected, all templates in [templates] section will be
578 available as well as aliases in [templatealias].
580 available as well as aliases in [templatealias].
579 """
581 """
580
582
581 if not tmpl:
583 if not tmpl:
582 return empty_templatespec()
584 return empty_templatespec()
583
585
584 # looks like a literal template?
586 # looks like a literal template?
585 if b'{' in tmpl:
587 if b'{' in tmpl:
586 return literal_templatespec(tmpl)
588 return literal_templatespec(tmpl)
587
589
588 # a reference to built-in (formatter) template
590 # a reference to built-in (formatter) template
589 if tmpl in {b'cbor', b'json', b'pickle', b'debug'}:
591 if tmpl in {b'cbor', b'json', b'pickle', b'debug'}:
590 return reference_templatespec(tmpl)
592 return reference_templatespec(tmpl)
591
593
592 # a function-style reference to built-in template
594 # a function-style reference to built-in template
593 func, fsep, ftail = tmpl.partition(b'(')
595 func, fsep, ftail = tmpl.partition(b'(')
594 if func in {b'cbor', b'json'} and fsep and ftail.endswith(b')'):
596 if func in {b'cbor', b'json'} and fsep and ftail.endswith(b')'):
595 templater.parseexpr(tmpl) # make sure syntax errors are confined
597 templater.parseexpr(tmpl) # make sure syntax errors are confined
596 return reference_templatespec(func, refargs=ftail[:-1])
598 return reference_templatespec(func, refargs=ftail[:-1])
597
599
598 # perhaps a stock style?
600 # perhaps a stock style?
599 if not os.path.split(tmpl)[0]:
601 if not os.path.split(tmpl)[0]:
600 mapname = templater.templatepath(
602 mapname = templater.templatepath(
601 b'map-cmdline.' + tmpl
603 b'map-cmdline.' + tmpl
602 ) or templater.templatepath(tmpl)
604 ) or templater.templatepath(tmpl)
603 if mapname:
605 if mapname:
604 return mapfile_templatespec(topic, mapname)
606 return mapfile_templatespec(topic, mapname)
605
607
606 # perhaps it's a reference to [templates]
608 # perhaps it's a reference to [templates]
607 if ui.config(b'templates', tmpl):
609 if ui.config(b'templates', tmpl):
608 return reference_templatespec(tmpl)
610 return reference_templatespec(tmpl)
609
611
610 if tmpl == b'list':
612 if tmpl == b'list':
611 ui.write(_(b"available styles: %s\n") % templater.stylelist())
613 ui.write(_(b"available styles: %s\n") % templater.stylelist())
612 raise error.Abort(_(b"specify a template"))
614 raise error.Abort(_(b"specify a template"))
613
615
614 # perhaps it's a path to a map or a template
616 # perhaps it's a path to a map or a template
615 if (b'/' in tmpl or b'\\' in tmpl) and os.path.isfile(tmpl):
617 if (b'/' in tmpl or b'\\' in tmpl) and os.path.isfile(tmpl):
616 # is it a mapfile for a style?
618 # is it a mapfile for a style?
617 if os.path.basename(tmpl).startswith(b"map-"):
619 if os.path.basename(tmpl).startswith(b"map-"):
618 return mapfile_templatespec(topic, os.path.realpath(tmpl))
620 return mapfile_templatespec(topic, os.path.realpath(tmpl))
619 with util.posixfile(tmpl, b'rb') as f:
621 with util.posixfile(tmpl, b'rb') as f:
620 tmpl = f.read()
622 tmpl = f.read()
621 return literal_templatespec(tmpl)
623 return literal_templatespec(tmpl)
622
624
623 # constant string?
625 # constant string?
624 return literal_templatespec(tmpl)
626 return literal_templatespec(tmpl)
625
627
626
628
627 def templatepartsmap(spec, t, partnames):
629 def templatepartsmap(spec, t, partnames):
628 """Create a mapping of {part: ref}"""
630 """Create a mapping of {part: ref}"""
629 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
631 partsmap = {spec.ref: spec.ref} # initial ref must exist in t
630 if spec.mapfile:
632 if spec.mapfile:
631 partsmap.update((p, p) for p in partnames if p in t)
633 partsmap.update((p, p) for p in partnames if p in t)
632 elif spec.ref:
634 elif spec.ref:
633 for part in partnames:
635 for part in partnames:
634 ref = b'%s:%s' % (spec.ref, part) # select config sub-section
636 ref = b'%s:%s' % (spec.ref, part) # select config sub-section
635 if ref in t:
637 if ref in t:
636 partsmap[part] = ref
638 partsmap[part] = ref
637 return partsmap
639 return partsmap
638
640
639
641
640 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
642 def loadtemplater(ui, spec, defaults=None, resources=None, cache=None):
641 """Create a templater from either a literal template or loading from
643 """Create a templater from either a literal template or loading from
642 a map file"""
644 a map file"""
643 assert not (spec.tmpl and spec.mapfile)
645 assert not (spec.tmpl and spec.mapfile)
644 if spec.mapfile:
646 if spec.mapfile:
645 return templater.templater.frommapfile(
647 return templater.templater.frommapfile(
646 spec.mapfile, defaults=defaults, resources=resources, cache=cache
648 spec.mapfile, defaults=defaults, resources=resources, cache=cache
647 )
649 )
648 return maketemplater(
650 return maketemplater(
649 ui, spec.tmpl, defaults=defaults, resources=resources, cache=cache
651 ui, spec.tmpl, defaults=defaults, resources=resources, cache=cache
650 )
652 )
651
653
652
654
653 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
655 def maketemplater(ui, tmpl, defaults=None, resources=None, cache=None):
654 """Create a templater from a string template 'tmpl'"""
656 """Create a templater from a string template 'tmpl'"""
655 aliases = ui.configitems(b'templatealias')
657 aliases = ui.configitems(b'templatealias')
656 t = templater.templater(
658 t = templater.templater(
657 defaults=defaults, resources=resources, cache=cache, aliases=aliases
659 defaults=defaults, resources=resources, cache=cache, aliases=aliases
658 )
660 )
659 t.cache.update(
661 t.cache.update(
660 (k, templater.unquotestring(v)) for k, v in ui.configitems(b'templates')
662 (k, templater.unquotestring(v)) for k, v in ui.configitems(b'templates')
661 )
663 )
662 if tmpl:
664 if tmpl:
663 t.cache[b''] = tmpl
665 t.cache[b''] = tmpl
664 return t
666 return t
665
667
666
668
667 # marker to denote a resource to be loaded on demand based on mapping values
669 # marker to denote a resource to be loaded on demand based on mapping values
668 # (e.g. (ctx, path) -> fctx)
670 # (e.g. (ctx, path) -> fctx)
669 _placeholder = object()
671 _placeholder = object()
670
672
671
673
672 class templateresources(templater.resourcemapper):
674 class templateresources(templater.resourcemapper):
673 """Resource mapper designed for the default templatekw and function"""
675 """Resource mapper designed for the default templatekw and function"""
674
676
675 def __init__(self, ui, repo=None):
677 def __init__(self, ui, repo=None):
676 self._resmap = {
678 self._resmap = {
677 b'cache': {}, # for templatekw/funcs to store reusable data
679 b'cache': {}, # for templatekw/funcs to store reusable data
678 b'repo': repo,
680 b'repo': repo,
679 b'ui': ui,
681 b'ui': ui,
680 }
682 }
681
683
682 def availablekeys(self, mapping):
684 def availablekeys(self, mapping):
683 return {
685 return {
684 k for k in self.knownkeys() if self._getsome(mapping, k) is not None
686 k for k in self.knownkeys() if self._getsome(mapping, k) is not None
685 }
687 }
686
688
687 def knownkeys(self):
689 def knownkeys(self):
688 return {b'cache', b'ctx', b'fctx', b'repo', b'revcache', b'ui'}
690 return {b'cache', b'ctx', b'fctx', b'repo', b'revcache', b'ui'}
689
691
690 def lookup(self, mapping, key):
692 def lookup(self, mapping, key):
691 if key not in self.knownkeys():
693 if key not in self.knownkeys():
692 return None
694 return None
693 v = self._getsome(mapping, key)
695 v = self._getsome(mapping, key)
694 if v is _placeholder:
696 if v is _placeholder:
695 v = mapping[key] = self._loadermap[key](self, mapping)
697 v = mapping[key] = self._loadermap[key](self, mapping)
696 return v
698 return v
697
699
698 def populatemap(self, context, origmapping, newmapping):
700 def populatemap(self, context, origmapping, newmapping):
699 mapping = {}
701 mapping = {}
700 if self._hasnodespec(newmapping):
702 if self._hasnodespec(newmapping):
701 mapping[b'revcache'] = {} # per-ctx cache
703 mapping[b'revcache'] = {} # per-ctx cache
702 if self._hasnodespec(origmapping) and self._hasnodespec(newmapping):
704 if self._hasnodespec(origmapping) and self._hasnodespec(newmapping):
703 orignode = templateutil.runsymbol(context, origmapping, b'node')
705 orignode = templateutil.runsymbol(context, origmapping, b'node')
704 mapping[b'originalnode'] = orignode
706 mapping[b'originalnode'] = orignode
705 # put marker to override 'ctx'/'fctx' in mapping if any, and flag
707 # put marker to override 'ctx'/'fctx' in mapping if any, and flag
706 # its existence to be reported by availablekeys()
708 # its existence to be reported by availablekeys()
707 if b'ctx' not in newmapping and self._hasliteral(newmapping, b'node'):
709 if b'ctx' not in newmapping and self._hasliteral(newmapping, b'node'):
708 mapping[b'ctx'] = _placeholder
710 mapping[b'ctx'] = _placeholder
709 if b'fctx' not in newmapping and self._hasliteral(newmapping, b'path'):
711 if b'fctx' not in newmapping and self._hasliteral(newmapping, b'path'):
710 mapping[b'fctx'] = _placeholder
712 mapping[b'fctx'] = _placeholder
711 return mapping
713 return mapping
712
714
713 def _getsome(self, mapping, key):
715 def _getsome(self, mapping, key):
714 v = mapping.get(key)
716 v = mapping.get(key)
715 if v is not None:
717 if v is not None:
716 return v
718 return v
717 return self._resmap.get(key)
719 return self._resmap.get(key)
718
720
719 def _hasliteral(self, mapping, key):
721 def _hasliteral(self, mapping, key):
720 """Test if a literal value is set or unset in the given mapping"""
722 """Test if a literal value is set or unset in the given mapping"""
721 return key in mapping and not callable(mapping[key])
723 return key in mapping and not callable(mapping[key])
722
724
723 def _getliteral(self, mapping, key):
725 def _getliteral(self, mapping, key):
724 """Return value of the given name if it is a literal"""
726 """Return value of the given name if it is a literal"""
725 v = mapping.get(key)
727 v = mapping.get(key)
726 if callable(v):
728 if callable(v):
727 return None
729 return None
728 return v
730 return v
729
731
730 def _hasnodespec(self, mapping):
732 def _hasnodespec(self, mapping):
731 """Test if context revision is set or unset in the given mapping"""
733 """Test if context revision is set or unset in the given mapping"""
732 return b'node' in mapping or b'ctx' in mapping
734 return b'node' in mapping or b'ctx' in mapping
733
735
734 def _loadctx(self, mapping):
736 def _loadctx(self, mapping):
735 repo = self._getsome(mapping, b'repo')
737 repo = self._getsome(mapping, b'repo')
736 node = self._getliteral(mapping, b'node')
738 node = self._getliteral(mapping, b'node')
737 if repo is None or node is None:
739 if repo is None or node is None:
738 return
740 return
739 try:
741 try:
740 return repo[node]
742 return repo[node]
741 except error.RepoLookupError:
743 except error.RepoLookupError:
742 return None # maybe hidden/non-existent node
744 return None # maybe hidden/non-existent node
743
745
744 def _loadfctx(self, mapping):
746 def _loadfctx(self, mapping):
745 ctx = self._getsome(mapping, b'ctx')
747 ctx = self._getsome(mapping, b'ctx')
746 path = self._getliteral(mapping, b'path')
748 path = self._getliteral(mapping, b'path')
747 if ctx is None or path is None:
749 if ctx is None or path is None:
748 return None
750 return None
749 try:
751 try:
750 return ctx[path]
752 return ctx[path]
751 except error.LookupError:
753 except error.LookupError:
752 return None # maybe removed file?
754 return None # maybe removed file?
753
755
754 _loadermap = {
756 _loadermap = {
755 b'ctx': _loadctx,
757 b'ctx': _loadctx,
756 b'fctx': _loadfctx,
758 b'fctx': _loadfctx,
757 }
759 }
758
760
759
761
760 def _internaltemplateformatter(
762 def _internaltemplateformatter(
761 ui,
763 ui,
762 out,
764 out,
763 topic,
765 topic,
764 opts,
766 opts,
765 spec,
767 spec,
766 tmpl,
768 tmpl,
767 docheader=b'',
769 docheader=b'',
768 docfooter=b'',
770 docfooter=b'',
769 separator=b'',
771 separator=b'',
770 ):
772 ):
771 """Build template formatter that handles customizable built-in templates
773 """Build template formatter that handles customizable built-in templates
772 such as -Tjson(...)"""
774 such as -Tjson(...)"""
773 templates = {spec.ref: tmpl}
775 templates = {spec.ref: tmpl}
774 if docheader:
776 if docheader:
775 templates[b'%s:docheader' % spec.ref] = docheader
777 templates[b'%s:docheader' % spec.ref] = docheader
776 if docfooter:
778 if docfooter:
777 templates[b'%s:docfooter' % spec.ref] = docfooter
779 templates[b'%s:docfooter' % spec.ref] = docfooter
778 if separator:
780 if separator:
779 templates[b'%s:separator' % spec.ref] = separator
781 templates[b'%s:separator' % spec.ref] = separator
780 return templateformatter(
782 return templateformatter(
781 ui, out, topic, opts, spec, overridetemplates=templates
783 ui, out, topic, opts, spec, overridetemplates=templates
782 )
784 )
783
785
784
786
785 def formatter(ui, out, topic, opts):
787 def formatter(ui, out, topic, opts):
786 spec = lookuptemplate(ui, topic, opts.get(b'template', b''))
788 spec = lookuptemplate(ui, topic, opts.get(b'template', b''))
787 if spec.ref == b"cbor" and spec.refargs is not None:
789 if spec.ref == b"cbor" and spec.refargs is not None:
788 return _internaltemplateformatter(
790 return _internaltemplateformatter(
789 ui,
791 ui,
790 out,
792 out,
791 topic,
793 topic,
792 opts,
794 opts,
793 spec,
795 spec,
794 tmpl=b'{dict(%s)|cbor}' % spec.refargs,
796 tmpl=b'{dict(%s)|cbor}' % spec.refargs,
795 docheader=cborutil.BEGIN_INDEFINITE_ARRAY,
797 docheader=cborutil.BEGIN_INDEFINITE_ARRAY,
796 docfooter=cborutil.BREAK,
798 docfooter=cborutil.BREAK,
797 )
799 )
798 elif spec.ref == b"cbor":
800 elif spec.ref == b"cbor":
799 return cborformatter(ui, out, topic, opts)
801 return cborformatter(ui, out, topic, opts)
800 elif spec.ref == b"json" and spec.refargs is not None:
802 elif spec.ref == b"json" and spec.refargs is not None:
801 return _internaltemplateformatter(
803 return _internaltemplateformatter(
802 ui,
804 ui,
803 out,
805 out,
804 topic,
806 topic,
805 opts,
807 opts,
806 spec,
808 spec,
807 tmpl=b'{dict(%s)|json}' % spec.refargs,
809 tmpl=b'{dict(%s)|json}' % spec.refargs,
808 docheader=b'[\n ',
810 docheader=b'[\n ',
809 docfooter=b'\n]\n',
811 docfooter=b'\n]\n',
810 separator=b',\n ',
812 separator=b',\n ',
811 )
813 )
812 elif spec.ref == b"json":
814 elif spec.ref == b"json":
813 return jsonformatter(ui, out, topic, opts)
815 return jsonformatter(ui, out, topic, opts)
814 elif spec.ref == b"pickle":
816 elif spec.ref == b"pickle":
815 assert spec.refargs is None, r'function-style not supported'
817 assert spec.refargs is None, r'function-style not supported'
816 return pickleformatter(ui, out, topic, opts)
818 return pickleformatter(ui, out, topic, opts)
817 elif spec.ref == b"debug":
819 elif spec.ref == b"debug":
818 assert spec.refargs is None, r'function-style not supported'
820 assert spec.refargs is None, r'function-style not supported'
819 return debugformatter(ui, out, topic, opts)
821 return debugformatter(ui, out, topic, opts)
820 elif spec.ref or spec.tmpl or spec.mapfile:
822 elif spec.ref or spec.tmpl or spec.mapfile:
821 assert spec.refargs is None, r'function-style not supported'
823 assert spec.refargs is None, r'function-style not supported'
822 return templateformatter(ui, out, topic, opts, spec)
824 return templateformatter(ui, out, topic, opts, spec)
823 # developer config: ui.formatdebug
825 # developer config: ui.formatdebug
824 elif ui.configbool(b'ui', b'formatdebug'):
826 elif ui.configbool(b'ui', b'formatdebug'):
825 return debugformatter(ui, out, topic, opts)
827 return debugformatter(ui, out, topic, opts)
826 # deprecated config: ui.formatjson
828 # deprecated config: ui.formatjson
827 elif ui.configbool(b'ui', b'formatjson'):
829 elif ui.configbool(b'ui', b'formatjson'):
828 return jsonformatter(ui, out, topic, opts)
830 return jsonformatter(ui, out, topic, opts)
829 return plainformatter(ui, out, topic, opts)
831 return plainformatter(ui, out, topic, opts)
830
832
831
833
832 @contextlib.contextmanager
834 @contextlib.contextmanager
833 def openformatter(ui, filename, topic, opts):
835 def openformatter(ui, filename, topic, opts):
834 """Create a formatter that writes outputs to the specified file
836 """Create a formatter that writes outputs to the specified file
835
837
836 Must be invoked using the 'with' statement.
838 Must be invoked using the 'with' statement.
837 """
839 """
838 with util.posixfile(filename, b'wb') as out:
840 with util.posixfile(filename, b'wb') as out:
839 with formatter(ui, out, topic, opts) as fm:
841 with formatter(ui, out, topic, opts) as fm:
840 yield fm
842 yield fm
841
843
842
844
843 @contextlib.contextmanager
845 @contextlib.contextmanager
844 def _neverending(fm):
846 def _neverending(fm):
845 yield fm
847 yield fm
846
848
847
849
848 def maybereopen(fm, filename):
850 def maybereopen(fm, filename):
849 """Create a formatter backed by file if filename specified, else return
851 """Create a formatter backed by file if filename specified, else return
850 the given formatter
852 the given formatter
851
853
852 Must be invoked using the 'with' statement. This will never call fm.end()
854 Must be invoked using the 'with' statement. This will never call fm.end()
853 of the given formatter.
855 of the given formatter.
854 """
856 """
855 if filename:
857 if filename:
856 return openformatter(fm._ui, filename, fm._topic, fm._opts)
858 return openformatter(fm._ui, filename, fm._topic, fm._opts)
857 else:
859 else:
858 return _neverending(fm)
860 return _neverending(fm)
@@ -1,1086 +1,1084 b''
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 import posixpath
12 import posixpath
13
13
14 from .i18n import _
14 from .i18n import _
15 from .node import (
15 from .node import (
16 nullid,
16 nullid,
17 wdirid,
17 wdirid,
18 wdirrev,
18 wdirrev,
19 )
19 )
20
20
21 from . import (
21 from . import (
22 dagop,
22 dagop,
23 error,
23 error,
24 formatter,
24 formatter,
25 graphmod,
25 graphmod,
26 match as matchmod,
26 match as matchmod,
27 mdiff,
27 mdiff,
28 patch,
28 patch,
29 pathutil,
29 pathutil,
30 pycompat,
30 pycompat,
31 revset,
31 revset,
32 revsetlang,
32 revsetlang,
33 scmutil,
33 scmutil,
34 smartset,
34 smartset,
35 templatekw,
35 templatekw,
36 templater,
36 templater,
37 util,
37 util,
38 )
38 )
39 from .utils import (
39 from .utils import (
40 dateutil,
40 dateutil,
41 stringutil,
41 stringutil,
42 )
42 )
43
43
44
44
45 if pycompat.TYPE_CHECKING:
45 if pycompat.TYPE_CHECKING:
46 from typing import (
46 from typing import (
47 Any,
47 Any,
48 Optional,
48 Optional,
49 Tuple,
49 Tuple,
50 )
50 )
51
51
52 for t in (Any, Optional, Tuple):
52 for t in (Any, Optional, Tuple):
53 assert t
53 assert t
54
54
55
55
56 def getlimit(opts):
56 def getlimit(opts):
57 """get the log limit according to option -l/--limit"""
57 """get the log limit according to option -l/--limit"""
58 limit = opts.get(b'limit')
58 limit = opts.get(b'limit')
59 if limit:
59 if limit:
60 try:
60 try:
61 limit = int(limit)
61 limit = int(limit)
62 except ValueError:
62 except ValueError:
63 raise error.Abort(_(b'limit must be a positive integer'))
63 raise error.Abort(_(b'limit must be a positive integer'))
64 if limit <= 0:
64 if limit <= 0:
65 raise error.Abort(_(b'limit must be positive'))
65 raise error.Abort(_(b'limit must be positive'))
66 else:
66 else:
67 limit = None
67 limit = None
68 return limit
68 return limit
69
69
70
70
71 def diffordiffstat(
71 def diffordiffstat(
72 ui,
72 ui,
73 repo,
73 repo,
74 diffopts,
74 diffopts,
75 ctx1,
75 ctx1,
76 ctx2,
76 ctx2,
77 match,
77 match,
78 changes=None,
78 changes=None,
79 stat=False,
79 stat=False,
80 fp=None,
80 fp=None,
81 graphwidth=0,
81 graphwidth=0,
82 prefix=b'',
82 prefix=b'',
83 root=b'',
83 root=b'',
84 listsubrepos=False,
84 listsubrepos=False,
85 hunksfilterfn=None,
85 hunksfilterfn=None,
86 ):
86 ):
87 '''show diff or diffstat.'''
87 '''show diff or diffstat.'''
88 if root:
88 if root:
89 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
89 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
90 else:
90 else:
91 relroot = b''
91 relroot = b''
92 copysourcematch = None
92 copysourcematch = None
93
93
94 def compose(f, g):
94 def compose(f, g):
95 return lambda x: f(g(x))
95 return lambda x: f(g(x))
96
96
97 def pathfn(f):
97 def pathfn(f):
98 return posixpath.join(prefix, f)
98 return posixpath.join(prefix, f)
99
99
100 if relroot != b'':
100 if relroot != b'':
101 # XXX relative roots currently don't work if the root is within a
101 # XXX relative roots currently don't work if the root is within a
102 # subrepo
102 # subrepo
103 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
103 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
104 uirelroot = uipathfn(pathfn(relroot))
104 uirelroot = uipathfn(pathfn(relroot))
105 relroot += b'/'
105 relroot += b'/'
106 for matchroot in match.files():
106 for matchroot in match.files():
107 if not matchroot.startswith(relroot):
107 if not matchroot.startswith(relroot):
108 ui.warn(
108 ui.warn(
109 _(b'warning: %s not inside relative root %s\n')
109 _(b'warning: %s not inside relative root %s\n')
110 % (uipathfn(pathfn(matchroot)), uirelroot)
110 % (uipathfn(pathfn(matchroot)), uirelroot)
111 )
111 )
112
112
113 relrootmatch = scmutil.match(ctx2, pats=[relroot], default=b'path')
113 relrootmatch = scmutil.match(ctx2, pats=[relroot], default=b'path')
114 match = matchmod.intersectmatchers(match, relrootmatch)
114 match = matchmod.intersectmatchers(match, relrootmatch)
115 copysourcematch = relrootmatch
115 copysourcematch = relrootmatch
116
116
117 checkroot = repo.ui.configbool(
117 checkroot = repo.ui.configbool(
118 b'devel', b'all-warnings'
118 b'devel', b'all-warnings'
119 ) or repo.ui.configbool(b'devel', b'check-relroot')
119 ) or repo.ui.configbool(b'devel', b'check-relroot')
120
120
121 def relrootpathfn(f):
121 def relrootpathfn(f):
122 if checkroot and not f.startswith(relroot):
122 if checkroot and not f.startswith(relroot):
123 raise AssertionError(
123 raise AssertionError(
124 b"file %s doesn't start with relroot %s" % (f, relroot)
124 b"file %s doesn't start with relroot %s" % (f, relroot)
125 )
125 )
126 return f[len(relroot) :]
126 return f[len(relroot) :]
127
127
128 pathfn = compose(relrootpathfn, pathfn)
128 pathfn = compose(relrootpathfn, pathfn)
129
129
130 if stat:
130 if stat:
131 diffopts = diffopts.copy(context=0, noprefix=False)
131 diffopts = diffopts.copy(context=0, noprefix=False)
132 width = 80
132 width = 80
133 if not ui.plain():
133 if not ui.plain():
134 width = ui.termwidth() - graphwidth
134 width = ui.termwidth() - graphwidth
135 # If an explicit --root was given, don't respect ui.relative-paths
135 # If an explicit --root was given, don't respect ui.relative-paths
136 if not relroot:
136 if not relroot:
137 pathfn = compose(scmutil.getuipathfn(repo), pathfn)
137 pathfn = compose(scmutil.getuipathfn(repo), pathfn)
138
138
139 chunks = ctx2.diff(
139 chunks = ctx2.diff(
140 ctx1,
140 ctx1,
141 match,
141 match,
142 changes,
142 changes,
143 opts=diffopts,
143 opts=diffopts,
144 pathfn=pathfn,
144 pathfn=pathfn,
145 copysourcematch=copysourcematch,
145 copysourcematch=copysourcematch,
146 hunksfilterfn=hunksfilterfn,
146 hunksfilterfn=hunksfilterfn,
147 )
147 )
148
148
149 if fp is not None or ui.canwritewithoutlabels():
149 if fp is not None or ui.canwritewithoutlabels():
150 out = fp or ui
150 out = fp or ui
151 if stat:
151 if stat:
152 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
152 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
153 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
153 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
154 out.write(chunk)
154 out.write(chunk)
155 else:
155 else:
156 if stat:
156 if stat:
157 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
157 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
158 else:
158 else:
159 chunks = patch.difflabel(
159 chunks = patch.difflabel(
160 lambda chunks, **kwargs: chunks, chunks, opts=diffopts
160 lambda chunks, **kwargs: chunks, chunks, opts=diffopts
161 )
161 )
162 if ui.canbatchlabeledwrites():
162 if ui.canbatchlabeledwrites():
163
163
164 def gen():
164 def gen():
165 for chunk, label in chunks:
165 for chunk, label in chunks:
166 yield ui.label(chunk, label=label)
166 yield ui.label(chunk, label=label)
167
167
168 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
168 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
169 ui.write(chunk)
169 ui.write(chunk)
170 else:
170 else:
171 for chunk, label in chunks:
171 for chunk, label in chunks:
172 ui.write(chunk, label=label)
172 ui.write(chunk, label=label)
173
173
174 node2 = ctx2.node()
174 node2 = ctx2.node()
175 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
175 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
176 tempnode2 = node2
176 tempnode2 = node2
177 try:
177 try:
178 if node2 is not None:
178 if node2 is not None:
179 tempnode2 = ctx2.substate[subpath][1]
179 tempnode2 = ctx2.substate[subpath][1]
180 except KeyError:
180 except KeyError:
181 # A subrepo that existed in node1 was deleted between node1 and
181 # A subrepo that existed in node1 was deleted between node1 and
182 # node2 (inclusive). Thus, ctx2's substate won't contain that
182 # node2 (inclusive). Thus, ctx2's substate won't contain that
183 # subpath. The best we can do is to ignore it.
183 # subpath. The best we can do is to ignore it.
184 tempnode2 = None
184 tempnode2 = None
185 submatch = matchmod.subdirmatcher(subpath, match)
185 submatch = matchmod.subdirmatcher(subpath, match)
186 subprefix = repo.wvfs.reljoin(prefix, subpath)
186 subprefix = repo.wvfs.reljoin(prefix, subpath)
187 if listsubrepos or match.exact(subpath) or any(submatch.files()):
187 if listsubrepos or match.exact(subpath) or any(submatch.files()):
188 sub.diff(
188 sub.diff(
189 ui,
189 ui,
190 diffopts,
190 diffopts,
191 tempnode2,
191 tempnode2,
192 submatch,
192 submatch,
193 changes=changes,
193 changes=changes,
194 stat=stat,
194 stat=stat,
195 fp=fp,
195 fp=fp,
196 prefix=subprefix,
196 prefix=subprefix,
197 )
197 )
198
198
199
199
200 class changesetdiffer(object):
200 class changesetdiffer(object):
201 """Generate diff of changeset with pre-configured filtering functions"""
201 """Generate diff of changeset with pre-configured filtering functions"""
202
202
203 def _makefilematcher(self, ctx):
203 def _makefilematcher(self, ctx):
204 return scmutil.matchall(ctx.repo())
204 return scmutil.matchall(ctx.repo())
205
205
206 def _makehunksfilter(self, ctx):
206 def _makehunksfilter(self, ctx):
207 return None
207 return None
208
208
209 def showdiff(self, ui, ctx, diffopts, graphwidth=0, stat=False):
209 def showdiff(self, ui, ctx, diffopts, graphwidth=0, stat=False):
210 diffordiffstat(
210 diffordiffstat(
211 ui,
211 ui,
212 ctx.repo(),
212 ctx.repo(),
213 diffopts,
213 diffopts,
214 ctx.p1(),
214 ctx.p1(),
215 ctx,
215 ctx,
216 match=self._makefilematcher(ctx),
216 match=self._makefilematcher(ctx),
217 stat=stat,
217 stat=stat,
218 graphwidth=graphwidth,
218 graphwidth=graphwidth,
219 hunksfilterfn=self._makehunksfilter(ctx),
219 hunksfilterfn=self._makehunksfilter(ctx),
220 )
220 )
221
221
222
222
223 def changesetlabels(ctx):
223 def changesetlabels(ctx):
224 labels = [b'log.changeset', b'changeset.%s' % ctx.phasestr()]
224 labels = [b'log.changeset', b'changeset.%s' % ctx.phasestr()]
225 if ctx.obsolete():
225 if ctx.obsolete():
226 labels.append(b'changeset.obsolete')
226 labels.append(b'changeset.obsolete')
227 if ctx.isunstable():
227 if ctx.isunstable():
228 labels.append(b'changeset.unstable')
228 labels.append(b'changeset.unstable')
229 for instability in ctx.instabilities():
229 for instability in ctx.instabilities():
230 labels.append(b'instability.%s' % instability)
230 labels.append(b'instability.%s' % instability)
231 return b' '.join(labels)
231 return b' '.join(labels)
232
232
233
233
234 class changesetprinter(object):
234 class changesetprinter(object):
235 '''show changeset information when templating not requested.'''
235 '''show changeset information when templating not requested.'''
236
236
237 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
237 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
238 self.ui = ui
238 self.ui = ui
239 self.repo = repo
239 self.repo = repo
240 self.buffered = buffered
240 self.buffered = buffered
241 self._differ = differ or changesetdiffer()
241 self._differ = differ or changesetdiffer()
242 self._diffopts = patch.diffallopts(ui, diffopts)
242 self._diffopts = patch.diffallopts(ui, diffopts)
243 self._includestat = diffopts and diffopts.get(b'stat')
243 self._includestat = diffopts and diffopts.get(b'stat')
244 self._includediff = diffopts and diffopts.get(b'patch')
244 self._includediff = diffopts and diffopts.get(b'patch')
245 self.header = {}
245 self.header = {}
246 self.hunk = {}
246 self.hunk = {}
247 self.lastheader = None
247 self.lastheader = None
248 self.footer = None
248 self.footer = None
249 self._columns = templatekw.getlogcolumns()
249 self._columns = templatekw.getlogcolumns()
250
250
251 def flush(self, ctx):
251 def flush(self, ctx):
252 rev = ctx.rev()
252 rev = ctx.rev()
253 if rev in self.header:
253 if rev in self.header:
254 h = self.header[rev]
254 h = self.header[rev]
255 if h != self.lastheader:
255 if h != self.lastheader:
256 self.lastheader = h
256 self.lastheader = h
257 self.ui.write(h)
257 self.ui.write(h)
258 del self.header[rev]
258 del self.header[rev]
259 if rev in self.hunk:
259 if rev in self.hunk:
260 self.ui.write(self.hunk[rev])
260 self.ui.write(self.hunk[rev])
261 del self.hunk[rev]
261 del self.hunk[rev]
262
262
263 def close(self):
263 def close(self):
264 if self.footer:
264 if self.footer:
265 self.ui.write(self.footer)
265 self.ui.write(self.footer)
266
266
267 def show(self, ctx, copies=None, **props):
267 def show(self, ctx, copies=None, **props):
268 props = pycompat.byteskwargs(props)
268 props = pycompat.byteskwargs(props)
269 if self.buffered:
269 if self.buffered:
270 self.ui.pushbuffer(labeled=True)
270 self.ui.pushbuffer(labeled=True)
271 self._show(ctx, copies, props)
271 self._show(ctx, copies, props)
272 self.hunk[ctx.rev()] = self.ui.popbuffer()
272 self.hunk[ctx.rev()] = self.ui.popbuffer()
273 else:
273 else:
274 self._show(ctx, copies, props)
274 self._show(ctx, copies, props)
275
275
276 def _show(self, ctx, copies, props):
276 def _show(self, ctx, copies, props):
277 '''show a single changeset or file revision'''
277 '''show a single changeset or file revision'''
278 changenode = ctx.node()
278 changenode = ctx.node()
279 graphwidth = props.get(b'graphwidth', 0)
279 graphwidth = props.get(b'graphwidth', 0)
280
280
281 if self.ui.quiet:
281 if self.ui.quiet:
282 self.ui.write(
282 self.ui.write(
283 b"%s\n" % scmutil.formatchangeid(ctx), label=b'log.node'
283 b"%s\n" % scmutil.formatchangeid(ctx), label=b'log.node'
284 )
284 )
285 return
285 return
286
286
287 columns = self._columns
287 columns = self._columns
288 self.ui.write(
288 self.ui.write(
289 columns[b'changeset'] % scmutil.formatchangeid(ctx),
289 columns[b'changeset'] % scmutil.formatchangeid(ctx),
290 label=changesetlabels(ctx),
290 label=changesetlabels(ctx),
291 )
291 )
292
292
293 # branches are shown first before any other names due to backwards
293 # branches are shown first before any other names due to backwards
294 # compatibility
294 # compatibility
295 branch = ctx.branch()
295 branch = ctx.branch()
296 # don't show the default branch name
296 # don't show the default branch name
297 if branch != b'default':
297 if branch != b'default':
298 self.ui.write(columns[b'branch'] % branch, label=b'log.branch')
298 self.ui.write(columns[b'branch'] % branch, label=b'log.branch')
299
299
300 for nsname, ns in pycompat.iteritems(self.repo.names):
300 for nsname, ns in pycompat.iteritems(self.repo.names):
301 # branches has special logic already handled above, so here we just
301 # branches has special logic already handled above, so here we just
302 # skip it
302 # skip it
303 if nsname == b'branches':
303 if nsname == b'branches':
304 continue
304 continue
305 # we will use the templatename as the color name since those two
305 # we will use the templatename as the color name since those two
306 # should be the same
306 # should be the same
307 for name in ns.names(self.repo, changenode):
307 for name in ns.names(self.repo, changenode):
308 self.ui.write(ns.logfmt % name, label=b'log.%s' % ns.colorname)
308 self.ui.write(ns.logfmt % name, label=b'log.%s' % ns.colorname)
309 if self.ui.debugflag:
309 if self.ui.debugflag:
310 self.ui.write(
310 self.ui.write(
311 columns[b'phase'] % ctx.phasestr(), label=b'log.phase'
311 columns[b'phase'] % ctx.phasestr(), label=b'log.phase'
312 )
312 )
313 for pctx in scmutil.meaningfulparents(self.repo, ctx):
313 for pctx in scmutil.meaningfulparents(self.repo, ctx):
314 label = b'log.parent changeset.%s' % pctx.phasestr()
314 label = b'log.parent changeset.%s' % pctx.phasestr()
315 self.ui.write(
315 self.ui.write(
316 columns[b'parent'] % scmutil.formatchangeid(pctx), label=label
316 columns[b'parent'] % scmutil.formatchangeid(pctx), label=label
317 )
317 )
318
318
319 if self.ui.debugflag:
319 if self.ui.debugflag:
320 mnode = ctx.manifestnode()
320 mnode = ctx.manifestnode()
321 if mnode is None:
321 if mnode is None:
322 mnode = wdirid
322 mnode = wdirid
323 mrev = wdirrev
323 mrev = wdirrev
324 else:
324 else:
325 mrev = self.repo.manifestlog.rev(mnode)
325 mrev = self.repo.manifestlog.rev(mnode)
326 self.ui.write(
326 self.ui.write(
327 columns[b'manifest']
327 columns[b'manifest']
328 % scmutil.formatrevnode(self.ui, mrev, mnode),
328 % scmutil.formatrevnode(self.ui, mrev, mnode),
329 label=b'ui.debug log.manifest',
329 label=b'ui.debug log.manifest',
330 )
330 )
331 self.ui.write(columns[b'user'] % ctx.user(), label=b'log.user')
331 self.ui.write(columns[b'user'] % ctx.user(), label=b'log.user')
332 self.ui.write(
332 self.ui.write(
333 columns[b'date'] % dateutil.datestr(ctx.date()), label=b'log.date'
333 columns[b'date'] % dateutil.datestr(ctx.date()), label=b'log.date'
334 )
334 )
335
335
336 if ctx.isunstable():
336 if ctx.isunstable():
337 instabilities = ctx.instabilities()
337 instabilities = ctx.instabilities()
338 self.ui.write(
338 self.ui.write(
339 columns[b'instability'] % b', '.join(instabilities),
339 columns[b'instability'] % b', '.join(instabilities),
340 label=b'log.instability',
340 label=b'log.instability',
341 )
341 )
342
342
343 elif ctx.obsolete():
343 elif ctx.obsolete():
344 self._showobsfate(ctx)
344 self._showobsfate(ctx)
345
345
346 self._exthook(ctx)
346 self._exthook(ctx)
347
347
348 if self.ui.debugflag:
348 if self.ui.debugflag:
349 files = ctx.p1().status(ctx)
349 files = ctx.p1().status(ctx)
350 for key, value in zip(
350 for key, value in zip(
351 [b'files', b'files+', b'files-'],
351 [b'files', b'files+', b'files-'],
352 [files.modified, files.added, files.removed],
352 [files.modified, files.added, files.removed],
353 ):
353 ):
354 if value:
354 if value:
355 self.ui.write(
355 self.ui.write(
356 columns[key] % b" ".join(value),
356 columns[key] % b" ".join(value),
357 label=b'ui.debug log.files',
357 label=b'ui.debug log.files',
358 )
358 )
359 elif ctx.files() and self.ui.verbose:
359 elif ctx.files() and self.ui.verbose:
360 self.ui.write(
360 self.ui.write(
361 columns[b'files'] % b" ".join(ctx.files()),
361 columns[b'files'] % b" ".join(ctx.files()),
362 label=b'ui.note log.files',
362 label=b'ui.note log.files',
363 )
363 )
364 if copies and self.ui.verbose:
364 if copies and self.ui.verbose:
365 copies = [b'%s (%s)' % c for c in copies]
365 copies = [b'%s (%s)' % c for c in copies]
366 self.ui.write(
366 self.ui.write(
367 columns[b'copies'] % b' '.join(copies),
367 columns[b'copies'] % b' '.join(copies),
368 label=b'ui.note log.copies',
368 label=b'ui.note log.copies',
369 )
369 )
370
370
371 extra = ctx.extra()
371 extra = ctx.extra()
372 if extra and self.ui.debugflag:
372 if extra and self.ui.debugflag:
373 for key, value in sorted(extra.items()):
373 for key, value in sorted(extra.items()):
374 self.ui.write(
374 self.ui.write(
375 columns[b'extra'] % (key, stringutil.escapestr(value)),
375 columns[b'extra'] % (key, stringutil.escapestr(value)),
376 label=b'ui.debug log.extra',
376 label=b'ui.debug log.extra',
377 )
377 )
378
378
379 description = ctx.description().strip()
379 description = ctx.description().strip()
380 if description:
380 if description:
381 if self.ui.verbose:
381 if self.ui.verbose:
382 self.ui.write(
382 self.ui.write(
383 _(b"description:\n"), label=b'ui.note log.description'
383 _(b"description:\n"), label=b'ui.note log.description'
384 )
384 )
385 self.ui.write(description, label=b'ui.note log.description')
385 self.ui.write(description, label=b'ui.note log.description')
386 self.ui.write(b"\n\n")
386 self.ui.write(b"\n\n")
387 else:
387 else:
388 self.ui.write(
388 self.ui.write(
389 columns[b'summary'] % description.splitlines()[0],
389 columns[b'summary'] % description.splitlines()[0],
390 label=b'log.summary',
390 label=b'log.summary',
391 )
391 )
392 self.ui.write(b"\n")
392 self.ui.write(b"\n")
393
393
394 self._showpatch(ctx, graphwidth)
394 self._showpatch(ctx, graphwidth)
395
395
396 def _showobsfate(self, ctx):
396 def _showobsfate(self, ctx):
397 # TODO: do not depend on templater
397 # TODO: do not depend on templater
398 tres = formatter.templateresources(self.repo.ui, self.repo)
398 tres = formatter.templateresources(self.repo.ui, self.repo)
399 t = formatter.maketemplater(
399 t = formatter.maketemplater(
400 self.repo.ui,
400 self.repo.ui,
401 b'{join(obsfate, "\n")}',
401 b'{join(obsfate, "\n")}',
402 defaults=templatekw.keywords,
402 defaults=templatekw.keywords,
403 resources=tres,
403 resources=tres,
404 )
404 )
405 obsfate = t.renderdefault({b'ctx': ctx}).splitlines()
405 obsfate = t.renderdefault({b'ctx': ctx}).splitlines()
406
406
407 if obsfate:
407 if obsfate:
408 for obsfateline in obsfate:
408 for obsfateline in obsfate:
409 self.ui.write(
409 self.ui.write(
410 self._columns[b'obsolete'] % obsfateline,
410 self._columns[b'obsolete'] % obsfateline,
411 label=b'log.obsfate',
411 label=b'log.obsfate',
412 )
412 )
413
413
414 def _exthook(self, ctx):
414 def _exthook(self, ctx):
415 '''empty method used by extension as a hook point
415 '''empty method used by extension as a hook point
416 '''
416 '''
417
417
418 def _showpatch(self, ctx, graphwidth=0):
418 def _showpatch(self, ctx, graphwidth=0):
419 if self._includestat:
419 if self._includestat:
420 self._differ.showdiff(
420 self._differ.showdiff(
421 self.ui, ctx, self._diffopts, graphwidth, stat=True
421 self.ui, ctx, self._diffopts, graphwidth, stat=True
422 )
422 )
423 if self._includestat and self._includediff:
423 if self._includestat and self._includediff:
424 self.ui.write(b"\n")
424 self.ui.write(b"\n")
425 if self._includediff:
425 if self._includediff:
426 self._differ.showdiff(
426 self._differ.showdiff(
427 self.ui, ctx, self._diffopts, graphwidth, stat=False
427 self.ui, ctx, self._diffopts, graphwidth, stat=False
428 )
428 )
429 if self._includestat or self._includediff:
429 if self._includestat or self._includediff:
430 self.ui.write(b"\n")
430 self.ui.write(b"\n")
431
431
432
432
433 class changesetformatter(changesetprinter):
433 class changesetformatter(changesetprinter):
434 """Format changeset information by generic formatter"""
434 """Format changeset information by generic formatter"""
435
435
436 def __init__(
436 def __init__(
437 self, ui, repo, fm, differ=None, diffopts=None, buffered=False
437 self, ui, repo, fm, differ=None, diffopts=None, buffered=False
438 ):
438 ):
439 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
439 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
440 self._diffopts = patch.difffeatureopts(ui, diffopts, git=True)
440 self._diffopts = patch.difffeatureopts(ui, diffopts, git=True)
441 self._fm = fm
441 self._fm = fm
442
442
443 def close(self):
443 def close(self):
444 self._fm.end()
444 self._fm.end()
445
445
446 def _show(self, ctx, copies, props):
446 def _show(self, ctx, copies, props):
447 '''show a single changeset or file revision'''
447 '''show a single changeset or file revision'''
448 fm = self._fm
448 fm = self._fm
449 fm.startitem()
449 fm.startitem()
450 fm.context(ctx=ctx)
450 fm.context(ctx=ctx)
451 fm.data(rev=scmutil.intrev(ctx), node=fm.hexfunc(scmutil.binnode(ctx)))
451 fm.data(rev=scmutil.intrev(ctx), node=fm.hexfunc(scmutil.binnode(ctx)))
452
452
453 datahint = fm.datahint()
453 datahint = fm.datahint()
454 if self.ui.quiet and not datahint:
454 if self.ui.quiet and not datahint:
455 return
455 return
456
456
457 fm.data(
457 fm.data(
458 branch=ctx.branch(),
458 branch=ctx.branch(),
459 phase=ctx.phasestr(),
459 phase=ctx.phasestr(),
460 user=ctx.user(),
460 user=ctx.user(),
461 date=fm.formatdate(ctx.date()),
461 date=fm.formatdate(ctx.date()),
462 desc=ctx.description(),
462 desc=ctx.description(),
463 bookmarks=fm.formatlist(ctx.bookmarks(), name=b'bookmark'),
463 bookmarks=fm.formatlist(ctx.bookmarks(), name=b'bookmark'),
464 tags=fm.formatlist(ctx.tags(), name=b'tag'),
464 tags=fm.formatlist(ctx.tags(), name=b'tag'),
465 parents=fm.formatlist(
465 parents=fm.formatlist(
466 [fm.hexfunc(c.node()) for c in ctx.parents()], name=b'node'
466 [fm.hexfunc(c.node()) for c in ctx.parents()], name=b'node'
467 ),
467 ),
468 )
468 )
469
469
470 if self.ui.debugflag or b'manifest' in datahint:
470 if self.ui.debugflag or b'manifest' in datahint:
471 fm.data(manifest=fm.hexfunc(ctx.manifestnode() or wdirid))
471 fm.data(manifest=fm.hexfunc(ctx.manifestnode() or wdirid))
472 if self.ui.debugflag or b'extra' in datahint:
472 if self.ui.debugflag or b'extra' in datahint:
473 fm.data(extra=fm.formatdict(ctx.extra()))
473 fm.data(extra=fm.formatdict(ctx.extra()))
474
474
475 if (
475 if (
476 self.ui.debugflag
476 self.ui.debugflag
477 or b'modified' in datahint
477 or b'modified' in datahint
478 or b'added' in datahint
478 or b'added' in datahint
479 or b'removed' in datahint
479 or b'removed' in datahint
480 ):
480 ):
481 files = ctx.p1().status(ctx)
481 files = ctx.p1().status(ctx)
482 fm.data(
482 fm.data(
483 modified=fm.formatlist(files.modified, name=b'file'),
483 modified=fm.formatlist(files.modified, name=b'file'),
484 added=fm.formatlist(files.added, name=b'file'),
484 added=fm.formatlist(files.added, name=b'file'),
485 removed=fm.formatlist(files.removed, name=b'file'),
485 removed=fm.formatlist(files.removed, name=b'file'),
486 )
486 )
487
487
488 verbose = not self.ui.debugflag and self.ui.verbose
488 verbose = not self.ui.debugflag and self.ui.verbose
489 if verbose or b'files' in datahint:
489 if verbose or b'files' in datahint:
490 fm.data(files=fm.formatlist(ctx.files(), name=b'file'))
490 fm.data(files=fm.formatlist(ctx.files(), name=b'file'))
491 if verbose and copies or b'copies' in datahint:
491 if verbose and copies or b'copies' in datahint:
492 fm.data(
492 fm.data(
493 copies=fm.formatdict(copies or {}, key=b'name', value=b'source')
493 copies=fm.formatdict(copies or {}, key=b'name', value=b'source')
494 )
494 )
495
495
496 if self._includestat or b'diffstat' in datahint:
496 if self._includestat or b'diffstat' in datahint:
497 self.ui.pushbuffer()
497 self.ui.pushbuffer()
498 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=True)
498 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=True)
499 fm.data(diffstat=self.ui.popbuffer())
499 fm.data(diffstat=self.ui.popbuffer())
500 if self._includediff or b'diff' in datahint:
500 if self._includediff or b'diff' in datahint:
501 self.ui.pushbuffer()
501 self.ui.pushbuffer()
502 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False)
502 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False)
503 fm.data(diff=self.ui.popbuffer())
503 fm.data(diff=self.ui.popbuffer())
504
504
505
505
506 class changesettemplater(changesetprinter):
506 class changesettemplater(changesetprinter):
507 '''format changeset information.
507 '''format changeset information.
508
508
509 Note: there are a variety of convenience functions to build a
509 Note: there are a variety of convenience functions to build a
510 changesettemplater for common cases. See functions such as:
510 changesettemplater for common cases. See functions such as:
511 maketemplater, changesetdisplayer, buildcommittemplate, or other
511 maketemplater, changesetdisplayer, buildcommittemplate, or other
512 functions that use changesest_templater.
512 functions that use changesest_templater.
513 '''
513 '''
514
514
515 # Arguments before "buffered" used to be positional. Consider not
515 # Arguments before "buffered" used to be positional. Consider not
516 # adding/removing arguments before "buffered" to not break callers.
516 # adding/removing arguments before "buffered" to not break callers.
517 def __init__(
517 def __init__(
518 self, ui, repo, tmplspec, differ=None, diffopts=None, buffered=False
518 self, ui, repo, tmplspec, differ=None, diffopts=None, buffered=False
519 ):
519 ):
520 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
520 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
521 # tres is shared with _graphnodeformatter()
521 # tres is shared with _graphnodeformatter()
522 self._tresources = tres = formatter.templateresources(ui, repo)
522 self._tresources = tres = formatter.templateresources(ui, repo)
523 self.t = formatter.loadtemplater(
523 self.t = formatter.loadtemplater(
524 ui,
524 ui,
525 tmplspec,
525 tmplspec,
526 defaults=templatekw.keywords,
526 defaults=templatekw.keywords,
527 resources=tres,
527 resources=tres,
528 cache=templatekw.defaulttempl,
528 cache=templatekw.defaulttempl,
529 )
529 )
530 self._counter = itertools.count()
530 self._counter = itertools.count()
531
531
532 self._tref = tmplspec.ref
532 self._tref = tmplspec.ref
533 self._parts = {
533 self._parts = {
534 b'header': b'',
534 b'header': b'',
535 b'footer': b'',
535 b'footer': b'',
536 tmplspec.ref: tmplspec.ref,
536 tmplspec.ref: tmplspec.ref,
537 b'docheader': b'',
537 b'docheader': b'',
538 b'docfooter': b'',
538 b'docfooter': b'',
539 b'separator': b'',
539 b'separator': b'',
540 }
540 }
541 if tmplspec.mapfile:
541 if tmplspec.mapfile:
542 # find correct templates for current mode, for backward
542 # find correct templates for current mode, for backward
543 # compatibility with 'log -v/-q/--debug' using a mapfile
543 # compatibility with 'log -v/-q/--debug' using a mapfile
544 tmplmodes = [
544 tmplmodes = [
545 (True, b''),
545 (True, b''),
546 (self.ui.verbose, b'_verbose'),
546 (self.ui.verbose, b'_verbose'),
547 (self.ui.quiet, b'_quiet'),
547 (self.ui.quiet, b'_quiet'),
548 (self.ui.debugflag, b'_debug'),
548 (self.ui.debugflag, b'_debug'),
549 ]
549 ]
550 for mode, postfix in tmplmodes:
550 for mode, postfix in tmplmodes:
551 for t in self._parts:
551 for t in self._parts:
552 cur = t + postfix
552 cur = t + postfix
553 if mode and cur in self.t:
553 if mode and cur in self.t:
554 self._parts[t] = cur
554 self._parts[t] = cur
555 else:
555 else:
556 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
556 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
557 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
557 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
558 self._parts.update(m)
558 self._parts.update(m)
559
559
560 if self._parts[b'docheader']:
560 if self._parts[b'docheader']:
561 self.ui.write(self.t.render(self._parts[b'docheader'], {}))
561 self.ui.write(self.t.render(self._parts[b'docheader'], {}))
562
562
563 def close(self):
563 def close(self):
564 if self._parts[b'docfooter']:
564 if self._parts[b'docfooter']:
565 if not self.footer:
565 if not self.footer:
566 self.footer = b""
566 self.footer = b""
567 self.footer += self.t.render(self._parts[b'docfooter'], {})
567 self.footer += self.t.render(self._parts[b'docfooter'], {})
568 return super(changesettemplater, self).close()
568 return super(changesettemplater, self).close()
569
569
570 def _show(self, ctx, copies, props):
570 def _show(self, ctx, copies, props):
571 '''show a single changeset or file revision'''
571 '''show a single changeset or file revision'''
572 props = props.copy()
572 props = props.copy()
573 props[b'ctx'] = ctx
573 props[b'ctx'] = ctx
574 props[b'index'] = index = next(self._counter)
574 props[b'index'] = index = next(self._counter)
575 props[b'revcache'] = {b'copies': copies}
575 props[b'revcache'] = {b'copies': copies}
576 graphwidth = props.get(b'graphwidth', 0)
576 graphwidth = props.get(b'graphwidth', 0)
577
577
578 # write separator, which wouldn't work well with the header part below
578 # write separator, which wouldn't work well with the header part below
579 # since there's inherently a conflict between header (across items) and
579 # since there's inherently a conflict between header (across items) and
580 # separator (per item)
580 # separator (per item)
581 if self._parts[b'separator'] and index > 0:
581 if self._parts[b'separator'] and index > 0:
582 self.ui.write(self.t.render(self._parts[b'separator'], {}))
582 self.ui.write(self.t.render(self._parts[b'separator'], {}))
583
583
584 # write header
584 # write header
585 if self._parts[b'header']:
585 if self._parts[b'header']:
586 h = self.t.render(self._parts[b'header'], props)
586 h = self.t.render(self._parts[b'header'], props)
587 if self.buffered:
587 if self.buffered:
588 self.header[ctx.rev()] = h
588 self.header[ctx.rev()] = h
589 else:
589 else:
590 if self.lastheader != h:
590 if self.lastheader != h:
591 self.lastheader = h
591 self.lastheader = h
592 self.ui.write(h)
592 self.ui.write(h)
593
593
594 # write changeset metadata, then patch if requested
594 # write changeset metadata, then patch if requested
595 key = self._parts[self._tref]
595 key = self._parts[self._tref]
596 self.ui.write(self.t.render(key, props))
596 self.ui.write(self.t.render(key, props))
597 self._exthook(ctx)
597 self._exthook(ctx)
598 self._showpatch(ctx, graphwidth)
598 self._showpatch(ctx, graphwidth)
599
599
600 if self._parts[b'footer']:
600 if self._parts[b'footer']:
601 if not self.footer:
601 if not self.footer:
602 self.footer = self.t.render(self._parts[b'footer'], props)
602 self.footer = self.t.render(self._parts[b'footer'], props)
603
603
604
604
605 def templatespec(tmpl, mapfile):
605 def templatespec(tmpl, mapfile):
606 assert not (tmpl and mapfile)
606 assert not (tmpl and mapfile)
607 if mapfile:
607 if mapfile:
608 return formatter.mapfile_templatespec(b'changeset', mapfile)
608 return formatter.mapfile_templatespec(b'changeset', mapfile)
609 else:
609 else:
610 if pycompat.ispy3:
611 assert not isinstance(tmpl, str), b'tmpl must not be a str'
612 return formatter.literal_templatespec(tmpl)
610 return formatter.literal_templatespec(tmpl)
613
611
614
612
615 def _lookuptemplate(ui, tmpl, style):
613 def _lookuptemplate(ui, tmpl, style):
616 """Find the template matching the given template spec or style
614 """Find the template matching the given template spec or style
617
615
618 See formatter.lookuptemplate() for details.
616 See formatter.lookuptemplate() for details.
619 """
617 """
620
618
621 # ui settings
619 # ui settings
622 if not tmpl and not style: # template are stronger than style
620 if not tmpl and not style: # template are stronger than style
623 tmpl = ui.config(b'ui', b'logtemplate')
621 tmpl = ui.config(b'ui', b'logtemplate')
624 if tmpl:
622 if tmpl:
625 return templatespec(templater.unquotestring(tmpl), None)
623 return templatespec(templater.unquotestring(tmpl), None)
626 else:
624 else:
627 style = util.expandpath(ui.config(b'ui', b'style'))
625 style = util.expandpath(ui.config(b'ui', b'style'))
628
626
629 if not tmpl and style:
627 if not tmpl and style:
630 mapfile = style
628 mapfile = style
631 if not os.path.split(mapfile)[0]:
629 if not os.path.split(mapfile)[0]:
632 mapname = templater.templatepath(
630 mapname = templater.templatepath(
633 b'map-cmdline.' + mapfile
631 b'map-cmdline.' + mapfile
634 ) or templater.templatepath(mapfile)
632 ) or templater.templatepath(mapfile)
635 if mapname:
633 if mapname:
636 mapfile = mapname
634 mapfile = mapname
637 return templatespec(None, mapfile)
635 return templatespec(None, mapfile)
638
636
639 return formatter.lookuptemplate(ui, b'changeset', tmpl)
637 return formatter.lookuptemplate(ui, b'changeset', tmpl)
640
638
641
639
642 def maketemplater(ui, repo, tmpl, buffered=False):
640 def maketemplater(ui, repo, tmpl, buffered=False):
643 """Create a changesettemplater from a literal template 'tmpl'
641 """Create a changesettemplater from a literal template 'tmpl'
644 byte-string."""
642 byte-string."""
645 spec = templatespec(tmpl, None)
643 spec = templatespec(tmpl, None)
646 return changesettemplater(ui, repo, spec, buffered=buffered)
644 return changesettemplater(ui, repo, spec, buffered=buffered)
647
645
648
646
649 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
647 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
650 """show one changeset using template or regular display.
648 """show one changeset using template or regular display.
651
649
652 Display format will be the first non-empty hit of:
650 Display format will be the first non-empty hit of:
653 1. option 'template'
651 1. option 'template'
654 2. option 'style'
652 2. option 'style'
655 3. [ui] setting 'logtemplate'
653 3. [ui] setting 'logtemplate'
656 4. [ui] setting 'style'
654 4. [ui] setting 'style'
657 If all of these values are either the unset or the empty string,
655 If all of these values are either the unset or the empty string,
658 regular display via changesetprinter() is done.
656 regular display via changesetprinter() is done.
659 """
657 """
660 postargs = (differ, opts, buffered)
658 postargs = (differ, opts, buffered)
661 spec = _lookuptemplate(ui, opts.get(b'template'), opts.get(b'style'))
659 spec = _lookuptemplate(ui, opts.get(b'template'), opts.get(b'style'))
662
660
663 # machine-readable formats have slightly different keyword set than
661 # machine-readable formats have slightly different keyword set than
664 # plain templates, which are handled by changesetformatter.
662 # plain templates, which are handled by changesetformatter.
665 # note that {b'pickle', b'debug'} can also be added to the list if needed.
663 # note that {b'pickle', b'debug'} can also be added to the list if needed.
666 if spec.ref in {b'cbor', b'json'}:
664 if spec.ref in {b'cbor', b'json'}:
667 fm = ui.formatter(b'log', opts)
665 fm = ui.formatter(b'log', opts)
668 return changesetformatter(ui, repo, fm, *postargs)
666 return changesetformatter(ui, repo, fm, *postargs)
669
667
670 if not spec.ref and not spec.tmpl and not spec.mapfile:
668 if not spec.ref and not spec.tmpl and not spec.mapfile:
671 return changesetprinter(ui, repo, *postargs)
669 return changesetprinter(ui, repo, *postargs)
672
670
673 return changesettemplater(ui, repo, spec, *postargs)
671 return changesettemplater(ui, repo, spec, *postargs)
674
672
675
673
676 def _makematcher(repo, revs, pats, opts):
674 def _makematcher(repo, revs, pats, opts):
677 """Build matcher and expanded patterns from log options
675 """Build matcher and expanded patterns from log options
678
676
679 If --follow, revs are the revisions to follow from.
677 If --follow, revs are the revisions to follow from.
680
678
681 Returns (match, pats, slowpath) where
679 Returns (match, pats, slowpath) where
682 - match: a matcher built from the given pats and -I/-X opts
680 - match: a matcher built from the given pats and -I/-X opts
683 - pats: patterns used (globs are expanded on Windows)
681 - pats: patterns used (globs are expanded on Windows)
684 - slowpath: True if patterns aren't as simple as scanning filelogs
682 - slowpath: True if patterns aren't as simple as scanning filelogs
685 """
683 """
686 # pats/include/exclude are passed to match.match() directly in
684 # pats/include/exclude are passed to match.match() directly in
687 # _matchfiles() revset but walkchangerevs() builds its matcher with
685 # _matchfiles() revset but walkchangerevs() builds its matcher with
688 # scmutil.match(). The difference is input pats are globbed on
686 # scmutil.match(). The difference is input pats are globbed on
689 # platforms without shell expansion (windows).
687 # platforms without shell expansion (windows).
690 wctx = repo[None]
688 wctx = repo[None]
691 match, pats = scmutil.matchandpats(wctx, pats, opts)
689 match, pats = scmutil.matchandpats(wctx, pats, opts)
692 slowpath = match.anypats() or (not match.always() and opts.get(b'removed'))
690 slowpath = match.anypats() or (not match.always() and opts.get(b'removed'))
693 if not slowpath:
691 if not slowpath:
694 follow = opts.get(b'follow') or opts.get(b'follow_first')
692 follow = opts.get(b'follow') or opts.get(b'follow_first')
695 startctxs = []
693 startctxs = []
696 if follow and opts.get(b'rev'):
694 if follow and opts.get(b'rev'):
697 startctxs = [repo[r] for r in revs]
695 startctxs = [repo[r] for r in revs]
698 for f in match.files():
696 for f in match.files():
699 if follow and startctxs:
697 if follow and startctxs:
700 # No idea if the path was a directory at that revision, so
698 # No idea if the path was a directory at that revision, so
701 # take the slow path.
699 # take the slow path.
702 if any(f not in c for c in startctxs):
700 if any(f not in c for c in startctxs):
703 slowpath = True
701 slowpath = True
704 continue
702 continue
705 elif follow and f not in wctx:
703 elif follow and f not in wctx:
706 # If the file exists, it may be a directory, so let it
704 # If the file exists, it may be a directory, so let it
707 # take the slow path.
705 # take the slow path.
708 if os.path.exists(repo.wjoin(f)):
706 if os.path.exists(repo.wjoin(f)):
709 slowpath = True
707 slowpath = True
710 continue
708 continue
711 else:
709 else:
712 raise error.Abort(
710 raise error.Abort(
713 _(
711 _(
714 b'cannot follow file not in parent '
712 b'cannot follow file not in parent '
715 b'revision: "%s"'
713 b'revision: "%s"'
716 )
714 )
717 % f
715 % f
718 )
716 )
719 filelog = repo.file(f)
717 filelog = repo.file(f)
720 if not filelog:
718 if not filelog:
721 # A zero count may be a directory or deleted file, so
719 # A zero count may be a directory or deleted file, so
722 # try to find matching entries on the slow path.
720 # try to find matching entries on the slow path.
723 if follow:
721 if follow:
724 raise error.Abort(
722 raise error.Abort(
725 _(b'cannot follow nonexistent file: "%s"') % f
723 _(b'cannot follow nonexistent file: "%s"') % f
726 )
724 )
727 slowpath = True
725 slowpath = True
728
726
729 # We decided to fall back to the slowpath because at least one
727 # We decided to fall back to the slowpath because at least one
730 # of the paths was not a file. Check to see if at least one of them
728 # of the paths was not a file. Check to see if at least one of them
731 # existed in history - in that case, we'll continue down the
729 # existed in history - in that case, we'll continue down the
732 # slowpath; otherwise, we can turn off the slowpath
730 # slowpath; otherwise, we can turn off the slowpath
733 if slowpath:
731 if slowpath:
734 for path in match.files():
732 for path in match.files():
735 if path == b'.' or path in repo.store:
733 if path == b'.' or path in repo.store:
736 break
734 break
737 else:
735 else:
738 slowpath = False
736 slowpath = False
739
737
740 return match, pats, slowpath
738 return match, pats, slowpath
741
739
742
740
743 def _fileancestors(repo, revs, match, followfirst):
741 def _fileancestors(repo, revs, match, followfirst):
744 fctxs = []
742 fctxs = []
745 for r in revs:
743 for r in revs:
746 ctx = repo[r]
744 ctx = repo[r]
747 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
745 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
748
746
749 # When displaying a revision with --patch --follow FILE, we have
747 # When displaying a revision with --patch --follow FILE, we have
750 # to know which file of the revision must be diffed. With
748 # to know which file of the revision must be diffed. With
751 # --follow, we want the names of the ancestors of FILE in the
749 # --follow, we want the names of the ancestors of FILE in the
752 # revision, stored in "fcache". "fcache" is populated as a side effect
750 # revision, stored in "fcache". "fcache" is populated as a side effect
753 # of the graph traversal.
751 # of the graph traversal.
754 fcache = {}
752 fcache = {}
755
753
756 def filematcher(ctx):
754 def filematcher(ctx):
757 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
755 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
758
756
759 def revgen():
757 def revgen():
760 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
758 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
761 fcache[rev] = [c.path() for c in cs]
759 fcache[rev] = [c.path() for c in cs]
762 yield rev
760 yield rev
763
761
764 return smartset.generatorset(revgen(), iterasc=False), filematcher
762 return smartset.generatorset(revgen(), iterasc=False), filematcher
765
763
766
764
767 def _makenofollowfilematcher(repo, pats, opts):
765 def _makenofollowfilematcher(repo, pats, opts):
768 '''hook for extensions to override the filematcher for non-follow cases'''
766 '''hook for extensions to override the filematcher for non-follow cases'''
769 return None
767 return None
770
768
771
769
772 _opt2logrevset = {
770 _opt2logrevset = {
773 b'no_merges': (b'not merge()', None),
771 b'no_merges': (b'not merge()', None),
774 b'only_merges': (b'merge()', None),
772 b'only_merges': (b'merge()', None),
775 b'_matchfiles': (None, b'_matchfiles(%ps)'),
773 b'_matchfiles': (None, b'_matchfiles(%ps)'),
776 b'date': (b'date(%s)', None),
774 b'date': (b'date(%s)', None),
777 b'branch': (b'branch(%s)', b'%lr'),
775 b'branch': (b'branch(%s)', b'%lr'),
778 b'_patslog': (b'filelog(%s)', b'%lr'),
776 b'_patslog': (b'filelog(%s)', b'%lr'),
779 b'keyword': (b'keyword(%s)', b'%lr'),
777 b'keyword': (b'keyword(%s)', b'%lr'),
780 b'prune': (b'ancestors(%s)', b'not %lr'),
778 b'prune': (b'ancestors(%s)', b'not %lr'),
781 b'user': (b'user(%s)', b'%lr'),
779 b'user': (b'user(%s)', b'%lr'),
782 }
780 }
783
781
784
782
785 def _makerevset(repo, match, pats, slowpath, opts):
783 def _makerevset(repo, match, pats, slowpath, opts):
786 """Return a revset string built from log options and file patterns"""
784 """Return a revset string built from log options and file patterns"""
787 opts = dict(opts)
785 opts = dict(opts)
788 # follow or not follow?
786 # follow or not follow?
789 follow = opts.get(b'follow') or opts.get(b'follow_first')
787 follow = opts.get(b'follow') or opts.get(b'follow_first')
790
788
791 # branch and only_branch are really aliases and must be handled at
789 # branch and only_branch are really aliases and must be handled at
792 # the same time
790 # the same time
793 opts[b'branch'] = opts.get(b'branch', []) + opts.get(b'only_branch', [])
791 opts[b'branch'] = opts.get(b'branch', []) + opts.get(b'only_branch', [])
794 opts[b'branch'] = [repo.lookupbranch(b) for b in opts[b'branch']]
792 opts[b'branch'] = [repo.lookupbranch(b) for b in opts[b'branch']]
795
793
796 if slowpath:
794 if slowpath:
797 # See walkchangerevs() slow path.
795 # See walkchangerevs() slow path.
798 #
796 #
799 # pats/include/exclude cannot be represented as separate
797 # pats/include/exclude cannot be represented as separate
800 # revset expressions as their filtering logic applies at file
798 # revset expressions as their filtering logic applies at file
801 # level. For instance "-I a -X b" matches a revision touching
799 # level. For instance "-I a -X b" matches a revision touching
802 # "a" and "b" while "file(a) and not file(b)" does
800 # "a" and "b" while "file(a) and not file(b)" does
803 # not. Besides, filesets are evaluated against the working
801 # not. Besides, filesets are evaluated against the working
804 # directory.
802 # directory.
805 matchargs = [b'r:', b'd:relpath']
803 matchargs = [b'r:', b'd:relpath']
806 for p in pats:
804 for p in pats:
807 matchargs.append(b'p:' + p)
805 matchargs.append(b'p:' + p)
808 for p in opts.get(b'include', []):
806 for p in opts.get(b'include', []):
809 matchargs.append(b'i:' + p)
807 matchargs.append(b'i:' + p)
810 for p in opts.get(b'exclude', []):
808 for p in opts.get(b'exclude', []):
811 matchargs.append(b'x:' + p)
809 matchargs.append(b'x:' + p)
812 opts[b'_matchfiles'] = matchargs
810 opts[b'_matchfiles'] = matchargs
813 elif not follow:
811 elif not follow:
814 opts[b'_patslog'] = list(pats)
812 opts[b'_patslog'] = list(pats)
815
813
816 expr = []
814 expr = []
817 for op, val in sorted(pycompat.iteritems(opts)):
815 for op, val in sorted(pycompat.iteritems(opts)):
818 if not val:
816 if not val:
819 continue
817 continue
820 if op not in _opt2logrevset:
818 if op not in _opt2logrevset:
821 continue
819 continue
822 revop, listop = _opt2logrevset[op]
820 revop, listop = _opt2logrevset[op]
823 if revop and b'%' not in revop:
821 if revop and b'%' not in revop:
824 expr.append(revop)
822 expr.append(revop)
825 elif not listop:
823 elif not listop:
826 expr.append(revsetlang.formatspec(revop, val))
824 expr.append(revsetlang.formatspec(revop, val))
827 else:
825 else:
828 if revop:
826 if revop:
829 val = [revsetlang.formatspec(revop, v) for v in val]
827 val = [revsetlang.formatspec(revop, v) for v in val]
830 expr.append(revsetlang.formatspec(listop, val))
828 expr.append(revsetlang.formatspec(listop, val))
831
829
832 if expr:
830 if expr:
833 expr = b'(' + b' and '.join(expr) + b')'
831 expr = b'(' + b' and '.join(expr) + b')'
834 else:
832 else:
835 expr = None
833 expr = None
836 return expr
834 return expr
837
835
838
836
839 def _initialrevs(repo, opts):
837 def _initialrevs(repo, opts):
840 """Return the initial set of revisions to be filtered or followed"""
838 """Return the initial set of revisions to be filtered or followed"""
841 follow = opts.get(b'follow') or opts.get(b'follow_first')
839 follow = opts.get(b'follow') or opts.get(b'follow_first')
842 if opts.get(b'rev'):
840 if opts.get(b'rev'):
843 revs = scmutil.revrange(repo, opts[b'rev'])
841 revs = scmutil.revrange(repo, opts[b'rev'])
844 elif follow and repo.dirstate.p1() == nullid:
842 elif follow and repo.dirstate.p1() == nullid:
845 revs = smartset.baseset()
843 revs = smartset.baseset()
846 elif follow:
844 elif follow:
847 revs = repo.revs(b'.')
845 revs = repo.revs(b'.')
848 else:
846 else:
849 revs = smartset.spanset(repo)
847 revs = smartset.spanset(repo)
850 revs.reverse()
848 revs.reverse()
851 return revs
849 return revs
852
850
853
851
854 def getrevs(repo, pats, opts):
852 def getrevs(repo, pats, opts):
855 # type: (Any, Any, Any) -> Tuple[smartset.abstractsmartset, Optional[changesetdiffer]]
853 # type: (Any, Any, Any) -> Tuple[smartset.abstractsmartset, Optional[changesetdiffer]]
856 """Return (revs, differ) where revs is a smartset
854 """Return (revs, differ) where revs is a smartset
857
855
858 differ is a changesetdiffer with pre-configured file matcher.
856 differ is a changesetdiffer with pre-configured file matcher.
859 """
857 """
860 follow = opts.get(b'follow') or opts.get(b'follow_first')
858 follow = opts.get(b'follow') or opts.get(b'follow_first')
861 followfirst = opts.get(b'follow_first')
859 followfirst = opts.get(b'follow_first')
862 limit = getlimit(opts)
860 limit = getlimit(opts)
863 revs = _initialrevs(repo, opts)
861 revs = _initialrevs(repo, opts)
864 if not revs:
862 if not revs:
865 return smartset.baseset(), None
863 return smartset.baseset(), None
866 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
864 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
867 filematcher = None
865 filematcher = None
868 if follow:
866 if follow:
869 if slowpath or match.always():
867 if slowpath or match.always():
870 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
868 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
871 else:
869 else:
872 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
870 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
873 revs.reverse()
871 revs.reverse()
874 if filematcher is None:
872 if filematcher is None:
875 filematcher = _makenofollowfilematcher(repo, pats, opts)
873 filematcher = _makenofollowfilematcher(repo, pats, opts)
876 if filematcher is None:
874 if filematcher is None:
877
875
878 def filematcher(ctx):
876 def filematcher(ctx):
879 return match
877 return match
880
878
881 expr = _makerevset(repo, match, pats, slowpath, opts)
879 expr = _makerevset(repo, match, pats, slowpath, opts)
882 if opts.get(b'graph'):
880 if opts.get(b'graph'):
883 # User-specified revs might be unsorted, but don't sort before
881 # User-specified revs might be unsorted, but don't sort before
884 # _makerevset because it might depend on the order of revs
882 # _makerevset because it might depend on the order of revs
885 if repo.ui.configbool(b'experimental', b'log.topo'):
883 if repo.ui.configbool(b'experimental', b'log.topo'):
886 if not revs.istopo():
884 if not revs.istopo():
887 revs = dagop.toposort(revs, repo.changelog.parentrevs)
885 revs = dagop.toposort(revs, repo.changelog.parentrevs)
888 # TODO: try to iterate the set lazily
886 # TODO: try to iterate the set lazily
889 revs = revset.baseset(list(revs), istopo=True)
887 revs = revset.baseset(list(revs), istopo=True)
890 elif not (revs.isdescending() or revs.istopo()):
888 elif not (revs.isdescending() or revs.istopo()):
891 revs.sort(reverse=True)
889 revs.sort(reverse=True)
892 if expr:
890 if expr:
893 matcher = revset.match(None, expr)
891 matcher = revset.match(None, expr)
894 revs = matcher(repo, revs)
892 revs = matcher(repo, revs)
895 if limit is not None:
893 if limit is not None:
896 revs = revs.slice(0, limit)
894 revs = revs.slice(0, limit)
897
895
898 differ = changesetdiffer()
896 differ = changesetdiffer()
899 differ._makefilematcher = filematcher
897 differ._makefilematcher = filematcher
900 return revs, differ
898 return revs, differ
901
899
902
900
903 def _parselinerangeopt(repo, opts):
901 def _parselinerangeopt(repo, opts):
904 """Parse --line-range log option and return a list of tuples (filename,
902 """Parse --line-range log option and return a list of tuples (filename,
905 (fromline, toline)).
903 (fromline, toline)).
906 """
904 """
907 linerangebyfname = []
905 linerangebyfname = []
908 for pat in opts.get(b'line_range', []):
906 for pat in opts.get(b'line_range', []):
909 try:
907 try:
910 pat, linerange = pat.rsplit(b',', 1)
908 pat, linerange = pat.rsplit(b',', 1)
911 except ValueError:
909 except ValueError:
912 raise error.Abort(_(b'malformatted line-range pattern %s') % pat)
910 raise error.Abort(_(b'malformatted line-range pattern %s') % pat)
913 try:
911 try:
914 fromline, toline = map(int, linerange.split(b':'))
912 fromline, toline = map(int, linerange.split(b':'))
915 except ValueError:
913 except ValueError:
916 raise error.Abort(_(b"invalid line range for %s") % pat)
914 raise error.Abort(_(b"invalid line range for %s") % pat)
917 msg = _(b"line range pattern '%s' must match exactly one file") % pat
915 msg = _(b"line range pattern '%s' must match exactly one file") % pat
918 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
916 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
919 linerangebyfname.append(
917 linerangebyfname.append(
920 (fname, util.processlinerange(fromline, toline))
918 (fname, util.processlinerange(fromline, toline))
921 )
919 )
922 return linerangebyfname
920 return linerangebyfname
923
921
924
922
925 def getlinerangerevs(repo, userrevs, opts):
923 def getlinerangerevs(repo, userrevs, opts):
926 """Return (revs, differ).
924 """Return (revs, differ).
927
925
928 "revs" are revisions obtained by processing "line-range" log options and
926 "revs" are revisions obtained by processing "line-range" log options and
929 walking block ancestors of each specified file/line-range.
927 walking block ancestors of each specified file/line-range.
930
928
931 "differ" is a changesetdiffer with pre-configured file matcher and hunks
929 "differ" is a changesetdiffer with pre-configured file matcher and hunks
932 filter.
930 filter.
933 """
931 """
934 wctx = repo[None]
932 wctx = repo[None]
935
933
936 # Two-levels map of "rev -> file ctx -> [line range]".
934 # Two-levels map of "rev -> file ctx -> [line range]".
937 linerangesbyrev = {}
935 linerangesbyrev = {}
938 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
936 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
939 if fname not in wctx:
937 if fname not in wctx:
940 raise error.Abort(
938 raise error.Abort(
941 _(b'cannot follow file not in parent revision: "%s"') % fname
939 _(b'cannot follow file not in parent revision: "%s"') % fname
942 )
940 )
943 fctx = wctx.filectx(fname)
941 fctx = wctx.filectx(fname)
944 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
942 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
945 rev = fctx.introrev()
943 rev = fctx.introrev()
946 if rev is None:
944 if rev is None:
947 rev = wdirrev
945 rev = wdirrev
948 if rev not in userrevs:
946 if rev not in userrevs:
949 continue
947 continue
950 linerangesbyrev.setdefault(rev, {}).setdefault(
948 linerangesbyrev.setdefault(rev, {}).setdefault(
951 fctx.path(), []
949 fctx.path(), []
952 ).append(linerange)
950 ).append(linerange)
953
951
954 def nofilterhunksfn(fctx, hunks):
952 def nofilterhunksfn(fctx, hunks):
955 return hunks
953 return hunks
956
954
957 def hunksfilter(ctx):
955 def hunksfilter(ctx):
958 fctxlineranges = linerangesbyrev.get(scmutil.intrev(ctx))
956 fctxlineranges = linerangesbyrev.get(scmutil.intrev(ctx))
959 if fctxlineranges is None:
957 if fctxlineranges is None:
960 return nofilterhunksfn
958 return nofilterhunksfn
961
959
962 def filterfn(fctx, hunks):
960 def filterfn(fctx, hunks):
963 lineranges = fctxlineranges.get(fctx.path())
961 lineranges = fctxlineranges.get(fctx.path())
964 if lineranges is not None:
962 if lineranges is not None:
965 for hr, lines in hunks:
963 for hr, lines in hunks:
966 if hr is None: # binary
964 if hr is None: # binary
967 yield hr, lines
965 yield hr, lines
968 continue
966 continue
969 if any(mdiff.hunkinrange(hr[2:], lr) for lr in lineranges):
967 if any(mdiff.hunkinrange(hr[2:], lr) for lr in lineranges):
970 yield hr, lines
968 yield hr, lines
971 else:
969 else:
972 for hunk in hunks:
970 for hunk in hunks:
973 yield hunk
971 yield hunk
974
972
975 return filterfn
973 return filterfn
976
974
977 def filematcher(ctx):
975 def filematcher(ctx):
978 files = list(linerangesbyrev.get(scmutil.intrev(ctx), []))
976 files = list(linerangesbyrev.get(scmutil.intrev(ctx), []))
979 return scmutil.matchfiles(repo, files)
977 return scmutil.matchfiles(repo, files)
980
978
981 revs = sorted(linerangesbyrev, reverse=True)
979 revs = sorted(linerangesbyrev, reverse=True)
982
980
983 differ = changesetdiffer()
981 differ = changesetdiffer()
984 differ._makefilematcher = filematcher
982 differ._makefilematcher = filematcher
985 differ._makehunksfilter = hunksfilter
983 differ._makehunksfilter = hunksfilter
986 return smartset.baseset(revs), differ
984 return smartset.baseset(revs), differ
987
985
988
986
989 def _graphnodeformatter(ui, displayer):
987 def _graphnodeformatter(ui, displayer):
990 spec = ui.config(b'ui', b'graphnodetemplate')
988 spec = ui.config(b'ui', b'graphnodetemplate')
991 if not spec:
989 if not spec:
992 return templatekw.getgraphnode # fast path for "{graphnode}"
990 return templatekw.getgraphnode # fast path for "{graphnode}"
993
991
994 spec = templater.unquotestring(spec)
992 spec = templater.unquotestring(spec)
995 if isinstance(displayer, changesettemplater):
993 if isinstance(displayer, changesettemplater):
996 # reuse cache of slow templates
994 # reuse cache of slow templates
997 tres = displayer._tresources
995 tres = displayer._tresources
998 else:
996 else:
999 tres = formatter.templateresources(ui)
997 tres = formatter.templateresources(ui)
1000 templ = formatter.maketemplater(
998 templ = formatter.maketemplater(
1001 ui, spec, defaults=templatekw.keywords, resources=tres
999 ui, spec, defaults=templatekw.keywords, resources=tres
1002 )
1000 )
1003
1001
1004 def formatnode(repo, ctx, cache):
1002 def formatnode(repo, ctx, cache):
1005 props = {b'ctx': ctx, b'repo': repo}
1003 props = {b'ctx': ctx, b'repo': repo}
1006 return templ.renderdefault(props)
1004 return templ.renderdefault(props)
1007
1005
1008 return formatnode
1006 return formatnode
1009
1007
1010
1008
1011 def displaygraph(ui, repo, dag, displayer, edgefn, getcopies=None, props=None):
1009 def displaygraph(ui, repo, dag, displayer, edgefn, getcopies=None, props=None):
1012 props = props or {}
1010 props = props or {}
1013 formatnode = _graphnodeformatter(ui, displayer)
1011 formatnode = _graphnodeformatter(ui, displayer)
1014 state = graphmod.asciistate()
1012 state = graphmod.asciistate()
1015 styles = state.styles
1013 styles = state.styles
1016
1014
1017 # only set graph styling if HGPLAIN is not set.
1015 # only set graph styling if HGPLAIN is not set.
1018 if ui.plain(b'graph'):
1016 if ui.plain(b'graph'):
1019 # set all edge styles to |, the default pre-3.8 behaviour
1017 # set all edge styles to |, the default pre-3.8 behaviour
1020 styles.update(dict.fromkeys(styles, b'|'))
1018 styles.update(dict.fromkeys(styles, b'|'))
1021 else:
1019 else:
1022 edgetypes = {
1020 edgetypes = {
1023 b'parent': graphmod.PARENT,
1021 b'parent': graphmod.PARENT,
1024 b'grandparent': graphmod.GRANDPARENT,
1022 b'grandparent': graphmod.GRANDPARENT,
1025 b'missing': graphmod.MISSINGPARENT,
1023 b'missing': graphmod.MISSINGPARENT,
1026 }
1024 }
1027 for name, key in edgetypes.items():
1025 for name, key in edgetypes.items():
1028 # experimental config: experimental.graphstyle.*
1026 # experimental config: experimental.graphstyle.*
1029 styles[key] = ui.config(
1027 styles[key] = ui.config(
1030 b'experimental', b'graphstyle.%s' % name, styles[key]
1028 b'experimental', b'graphstyle.%s' % name, styles[key]
1031 )
1029 )
1032 if not styles[key]:
1030 if not styles[key]:
1033 styles[key] = None
1031 styles[key] = None
1034
1032
1035 # experimental config: experimental.graphshorten
1033 # experimental config: experimental.graphshorten
1036 state.graphshorten = ui.configbool(b'experimental', b'graphshorten')
1034 state.graphshorten = ui.configbool(b'experimental', b'graphshorten')
1037
1035
1038 formatnode_cache = {}
1036 formatnode_cache = {}
1039 for rev, type, ctx, parents in dag:
1037 for rev, type, ctx, parents in dag:
1040 char = formatnode(repo, ctx, formatnode_cache)
1038 char = formatnode(repo, ctx, formatnode_cache)
1041 copies = getcopies(ctx) if getcopies else None
1039 copies = getcopies(ctx) if getcopies else None
1042 edges = edgefn(type, char, state, rev, parents)
1040 edges = edgefn(type, char, state, rev, parents)
1043 firstedge = next(edges)
1041 firstedge = next(edges)
1044 width = firstedge[2]
1042 width = firstedge[2]
1045 displayer.show(
1043 displayer.show(
1046 ctx, copies=copies, graphwidth=width, **pycompat.strkwargs(props)
1044 ctx, copies=copies, graphwidth=width, **pycompat.strkwargs(props)
1047 )
1045 )
1048 lines = displayer.hunk.pop(rev).split(b'\n')
1046 lines = displayer.hunk.pop(rev).split(b'\n')
1049 if not lines[-1]:
1047 if not lines[-1]:
1050 del lines[-1]
1048 del lines[-1]
1051 displayer.flush(ctx)
1049 displayer.flush(ctx)
1052 for type, char, width, coldata in itertools.chain([firstedge], edges):
1050 for type, char, width, coldata in itertools.chain([firstedge], edges):
1053 graphmod.ascii(ui, state, type, char, lines, coldata)
1051 graphmod.ascii(ui, state, type, char, lines, coldata)
1054 lines = []
1052 lines = []
1055 displayer.close()
1053 displayer.close()
1056
1054
1057
1055
1058 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
1056 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
1059 revdag = graphmod.dagwalker(repo, revs)
1057 revdag = graphmod.dagwalker(repo, revs)
1060 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
1058 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
1061
1059
1062
1060
1063 def displayrevs(ui, repo, revs, displayer, getcopies):
1061 def displayrevs(ui, repo, revs, displayer, getcopies):
1064 for rev in revs:
1062 for rev in revs:
1065 ctx = repo[rev]
1063 ctx = repo[rev]
1066 copies = getcopies(ctx) if getcopies else None
1064 copies = getcopies(ctx) if getcopies else None
1067 displayer.show(ctx, copies=copies)
1065 displayer.show(ctx, copies=copies)
1068 displayer.flush(ctx)
1066 displayer.flush(ctx)
1069 displayer.close()
1067 displayer.close()
1070
1068
1071
1069
1072 def checkunsupportedgraphflags(pats, opts):
1070 def checkunsupportedgraphflags(pats, opts):
1073 for op in [b"newest_first"]:
1071 for op in [b"newest_first"]:
1074 if op in opts and opts[op]:
1072 if op in opts and opts[op]:
1075 raise error.Abort(
1073 raise error.Abort(
1076 _(b"-G/--graph option is incompatible with --%s")
1074 _(b"-G/--graph option is incompatible with --%s")
1077 % op.replace(b"_", b"-")
1075 % op.replace(b"_", b"-")
1078 )
1076 )
1079
1077
1080
1078
1081 def graphrevs(repo, nodes, opts):
1079 def graphrevs(repo, nodes, opts):
1082 limit = getlimit(opts)
1080 limit = getlimit(opts)
1083 nodes.reverse()
1081 nodes.reverse()
1084 if limit is not None:
1082 if limit is not None:
1085 nodes = nodes[:limit]
1083 nodes = nodes[:limit]
1086 return graphmod.nodes(repo, nodes)
1084 return graphmod.nodes(repo, nodes)
General Comments 0
You need to be logged in to leave comments. Login now