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