##// END OF EJS Templates
formatter: remove template resources from nested items before generating JSON
Yuya Nishihara -
r37520:40c7347f default
parent child Browse files
Show More
@@ -1,682 +1,689 b''
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 abc
11 11 import types
12 12
13 13 from .i18n import _
14 14 from . import (
15 15 error,
16 16 pycompat,
17 17 util,
18 18 )
19 19 from .utils import (
20 20 dateutil,
21 21 stringutil,
22 22 )
23 23
24 24 class ResourceUnavailable(error.Abort):
25 25 pass
26 26
27 27 class TemplateNotFound(error.Abort):
28 28 pass
29 29
30 30 class wrapped(object):
31 31 """Object requiring extra conversion prior to displaying or processing
32 32 as value
33 33
34 34 Use unwrapvalue(), unwrapastype(), or unwraphybrid() to obtain the inner
35 35 object.
36 36 """
37 37
38 38 __metaclass__ = abc.ABCMeta
39 39
40 40 @abc.abstractmethod
41 41 def itermaps(self, context):
42 42 """Yield each template mapping"""
43 43
44 44 @abc.abstractmethod
45 45 def join(self, context, mapping, sep):
46 46 """Join items with the separator; Returns a bytes or (possibly nested)
47 47 generator of bytes
48 48
49 49 A pre-configured template may be rendered per item if this container
50 50 holds unprintable items.
51 51 """
52 52
53 53 @abc.abstractmethod
54 54 def show(self, context, mapping):
55 55 """Return a bytes or (possibly nested) generator of bytes representing
56 56 the underlying object
57 57
58 58 A pre-configured template may be rendered if the underlying object is
59 59 not printable.
60 60 """
61 61
62 62 @abc.abstractmethod
63 63 def tovalue(self, context, mapping):
64 64 """Move the inner value object out or create a value representation
65 65
66 66 A returned value must be serializable by templaterfilters.json().
67 67 """
68 68
69 69 # stub for representing a date type; may be a real date type that can
70 70 # provide a readable string value
71 71 class date(object):
72 72 pass
73 73
74 74 class hybrid(wrapped):
75 75 """Wrapper for list or dict to support legacy template
76 76
77 77 This class allows us to handle both:
78 78 - "{files}" (legacy command-line-specific list hack) and
79 79 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
80 80 and to access raw values:
81 81 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
82 82 - "{get(extras, key)}"
83 83 - "{files|json}"
84 84 """
85 85
86 86 def __init__(self, gen, values, makemap, joinfmt, keytype=None):
87 87 self._gen = gen # generator or function returning generator
88 88 self._values = values
89 89 self._makemap = makemap
90 90 self._joinfmt = joinfmt
91 91 self.keytype = keytype # hint for 'x in y' where type(x) is unresolved
92 92
93 93 def itermaps(self, context):
94 94 makemap = self._makemap
95 95 for x in self._values:
96 96 yield makemap(x)
97 97
98 98 def join(self, context, mapping, sep):
99 99 # TODO: switch gen to (context, mapping) API?
100 100 return joinitems((self._joinfmt(x) for x in self._values), sep)
101 101
102 102 def show(self, context, mapping):
103 103 # TODO: switch gen to (context, mapping) API?
104 104 gen = self._gen
105 105 if gen is None:
106 106 return self.join(context, mapping, ' ')
107 107 if callable(gen):
108 108 return gen()
109 109 return gen
110 110
111 111 def tovalue(self, context, mapping):
112 112 # TODO: return self._values and get rid of proxy methods
113 113 return self
114 114
115 115 def __contains__(self, x):
116 116 return x in self._values
117 117 def __getitem__(self, key):
118 118 return self._values[key]
119 119 def __len__(self):
120 120 return len(self._values)
121 121 def __iter__(self):
122 122 return iter(self._values)
123 123 def __getattr__(self, name):
124 124 if name not in (r'get', r'items', r'iteritems', r'iterkeys',
125 125 r'itervalues', r'keys', r'values'):
126 126 raise AttributeError(name)
127 127 return getattr(self._values, name)
128 128
129 129 class mappable(wrapped):
130 130 """Wrapper for non-list/dict object to support map operation
131 131
132 132 This class allows us to handle both:
133 133 - "{manifest}"
134 134 - "{manifest % '{rev}:{node}'}"
135 135 - "{manifest.rev}"
136 136
137 137 Unlike a hybrid, this does not simulate the behavior of the underling
138 138 value.
139 139 """
140 140
141 141 def __init__(self, gen, key, value, makemap):
142 142 self._gen = gen # generator or function returning generator
143 143 self._key = key
144 144 self._value = value # may be generator of strings
145 145 self._makemap = makemap
146 146
147 147 def tomap(self):
148 148 return self._makemap(self._key)
149 149
150 150 def itermaps(self, context):
151 151 yield self.tomap()
152 152
153 153 def join(self, context, mapping, sep):
154 154 # TODO: just copies the old behavior where a value was a generator
155 155 # yielding one item, but reconsider about it. join() over a string
156 156 # has no consistent result because a string may be a bytes, or a
157 157 # generator yielding an item, or a generator yielding multiple items.
158 158 # Preserving all of the current behaviors wouldn't make any sense.
159 159 return self.show(context, mapping)
160 160
161 161 def show(self, context, mapping):
162 162 # TODO: switch gen to (context, mapping) API?
163 163 gen = self._gen
164 164 if gen is None:
165 165 return pycompat.bytestr(self._value)
166 166 if callable(gen):
167 167 return gen()
168 168 return gen
169 169
170 170 def tovalue(self, context, mapping):
171 171 return _unthunk(context, mapping, self._value)
172 172
173 173 class _mappingsequence(wrapped):
174 174 """Wrapper for sequence of template mappings
175 175
176 176 This represents an inner template structure (i.e. a list of dicts),
177 177 which can also be rendered by the specified named/literal template.
178 178
179 179 Template mappings may be nested.
180 180 """
181 181
182 182 def __init__(self, name=None, tmpl=None, sep=''):
183 183 if name is not None and tmpl is not None:
184 184 raise error.ProgrammingError('name and tmpl are mutually exclusive')
185 185 self._name = name
186 186 self._tmpl = tmpl
187 187 self._defaultsep = sep
188 188
189 189 def join(self, context, mapping, sep):
190 190 mapsiter = _iteroverlaymaps(context, mapping, self.itermaps(context))
191 191 if self._name:
192 192 itemiter = (context.process(self._name, m) for m in mapsiter)
193 193 elif self._tmpl:
194 194 itemiter = (context.expand(self._tmpl, m) for m in mapsiter)
195 195 else:
196 196 raise error.ParseError(_('not displayable without template'))
197 197 return joinitems(itemiter, sep)
198 198
199 199 def show(self, context, mapping):
200 200 return self.join(context, mapping, self._defaultsep)
201 201
202 202 def tovalue(self, context, mapping):
203 return list(self.itermaps(context))
203 knownres = context.knownresourcekeys()
204 items = []
205 for nm in self.itermaps(context):
206 # drop internal resources (recursively) which shouldn't be displayed
207 lm = context.overlaymap(mapping, nm)
208 items.append({k: unwrapvalue(context, lm, v)
209 for k, v in nm.iteritems() if k not in knownres})
210 return items
204 211
205 212 class mappinggenerator(_mappingsequence):
206 213 """Wrapper for generator of template mappings
207 214
208 215 The function ``make(context, *args)`` should return a generator of
209 216 mapping dicts.
210 217 """
211 218
212 219 def __init__(self, make, args=(), name=None, tmpl=None, sep=''):
213 220 super(mappinggenerator, self).__init__(name, tmpl, sep)
214 221 self._make = make
215 222 self._args = args
216 223
217 224 def itermaps(self, context):
218 225 return self._make(context, *self._args)
219 226
220 227 class mappinglist(_mappingsequence):
221 228 """Wrapper for list of template mappings"""
222 229
223 230 def __init__(self, mappings, name=None, tmpl=None, sep=''):
224 231 super(mappinglist, self).__init__(name, tmpl, sep)
225 232 self._mappings = mappings
226 233
227 234 def itermaps(self, context):
228 235 return iter(self._mappings)
229 236
230 237 class mappedgenerator(wrapped):
231 238 """Wrapper for generator of strings which acts as a list
232 239
233 240 The function ``make(context, *args)`` should return a generator of
234 241 byte strings, or a generator of (possibly nested) generators of byte
235 242 strings (i.e. a generator for a list of byte strings.)
236 243 """
237 244
238 245 def __init__(self, make, args=()):
239 246 self._make = make
240 247 self._args = args
241 248
242 249 def _gen(self, context):
243 250 return self._make(context, *self._args)
244 251
245 252 def itermaps(self, context):
246 253 raise error.ParseError(_('list of strings is not mappable'))
247 254
248 255 def join(self, context, mapping, sep):
249 256 return joinitems(self._gen(context), sep)
250 257
251 258 def show(self, context, mapping):
252 259 return self.join(context, mapping, '')
253 260
254 261 def tovalue(self, context, mapping):
255 262 return [stringify(context, mapping, x) for x in self._gen(context)]
256 263
257 264 def hybriddict(data, key='key', value='value', fmt=None, gen=None):
258 265 """Wrap data to support both dict-like and string-like operations"""
259 266 prefmt = pycompat.identity
260 267 if fmt is None:
261 268 fmt = '%s=%s'
262 269 prefmt = pycompat.bytestr
263 270 return hybrid(gen, data, lambda k: {key: k, value: data[k]},
264 271 lambda k: fmt % (prefmt(k), prefmt(data[k])))
265 272
266 273 def hybridlist(data, name, fmt=None, gen=None):
267 274 """Wrap data to support both list-like and string-like operations"""
268 275 prefmt = pycompat.identity
269 276 if fmt is None:
270 277 fmt = '%s'
271 278 prefmt = pycompat.bytestr
272 279 return hybrid(gen, data, lambda x: {name: x}, lambda x: fmt % prefmt(x))
273 280
274 281 def unwraphybrid(context, mapping, thing):
275 282 """Return an object which can be stringified possibly by using a legacy
276 283 template"""
277 284 if not isinstance(thing, wrapped):
278 285 return thing
279 286 return thing.show(context, mapping)
280 287
281 288 def unwrapvalue(context, mapping, thing):
282 289 """Move the inner value object out of the wrapper"""
283 290 if not isinstance(thing, wrapped):
284 291 return thing
285 292 return thing.tovalue(context, mapping)
286 293
287 294 def wraphybridvalue(container, key, value):
288 295 """Wrap an element of hybrid container to be mappable
289 296
290 297 The key is passed to the makemap function of the given container, which
291 298 should be an item generated by iter(container).
292 299 """
293 300 makemap = getattr(container, '_makemap', None)
294 301 if makemap is None:
295 302 return value
296 303 if util.safehasattr(value, '_makemap'):
297 304 # a nested hybrid list/dict, which has its own way of map operation
298 305 return value
299 306 return mappable(None, key, value, makemap)
300 307
301 308 def compatdict(context, mapping, name, data, key='key', value='value',
302 309 fmt=None, plural=None, separator=' '):
303 310 """Wrap data like hybriddict(), but also supports old-style list template
304 311
305 312 This exists for backward compatibility with the old-style template. Use
306 313 hybriddict() for new template keywords.
307 314 """
308 315 c = [{key: k, value: v} for k, v in data.iteritems()]
309 316 f = _showcompatlist(context, mapping, name, c, plural, separator)
310 317 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
311 318
312 319 def compatlist(context, mapping, name, data, element=None, fmt=None,
313 320 plural=None, separator=' '):
314 321 """Wrap data like hybridlist(), but also supports old-style list template
315 322
316 323 This exists for backward compatibility with the old-style template. Use
317 324 hybridlist() for new template keywords.
318 325 """
319 326 f = _showcompatlist(context, mapping, name, data, plural, separator)
320 327 return hybridlist(data, name=element or name, fmt=fmt, gen=f)
321 328
322 329 def _showcompatlist(context, mapping, name, values, plural=None, separator=' '):
323 330 """Return a generator that renders old-style list template
324 331
325 332 name is name of key in template map.
326 333 values is list of strings or dicts.
327 334 plural is plural of name, if not simply name + 's'.
328 335 separator is used to join values as a string
329 336
330 337 expansion works like this, given name 'foo'.
331 338
332 339 if values is empty, expand 'no_foos'.
333 340
334 341 if 'foo' not in template map, return values as a string,
335 342 joined by 'separator'.
336 343
337 344 expand 'start_foos'.
338 345
339 346 for each value, expand 'foo'. if 'last_foo' in template
340 347 map, expand it instead of 'foo' for last key.
341 348
342 349 expand 'end_foos'.
343 350 """
344 351 if not plural:
345 352 plural = name + 's'
346 353 if not values:
347 354 noname = 'no_' + plural
348 355 if context.preload(noname):
349 356 yield context.process(noname, mapping)
350 357 return
351 358 if not context.preload(name):
352 359 if isinstance(values[0], bytes):
353 360 yield separator.join(values)
354 361 else:
355 362 for v in values:
356 363 r = dict(v)
357 364 r.update(mapping)
358 365 yield r
359 366 return
360 367 startname = 'start_' + plural
361 368 if context.preload(startname):
362 369 yield context.process(startname, mapping)
363 370 def one(v, tag=name):
364 371 vmapping = {}
365 372 try:
366 373 vmapping.update(v)
367 374 # Python 2 raises ValueError if the type of v is wrong. Python
368 375 # 3 raises TypeError.
369 376 except (AttributeError, TypeError, ValueError):
370 377 try:
371 378 # Python 2 raises ValueError trying to destructure an e.g.
372 379 # bytes. Python 3 raises TypeError.
373 380 for a, b in v:
374 381 vmapping[a] = b
375 382 except (TypeError, ValueError):
376 383 vmapping[name] = v
377 384 vmapping = context.overlaymap(mapping, vmapping)
378 385 return context.process(tag, vmapping)
379 386 lastname = 'last_' + name
380 387 if context.preload(lastname):
381 388 last = values.pop()
382 389 else:
383 390 last = None
384 391 for v in values:
385 392 yield one(v)
386 393 if last is not None:
387 394 yield one(last, tag=lastname)
388 395 endname = 'end_' + plural
389 396 if context.preload(endname):
390 397 yield context.process(endname, mapping)
391 398
392 399 def flatten(context, mapping, thing):
393 400 """Yield a single stream from a possibly nested set of iterators"""
394 401 thing = unwraphybrid(context, mapping, thing)
395 402 if isinstance(thing, bytes):
396 403 yield thing
397 404 elif isinstance(thing, str):
398 405 # We can only hit this on Python 3, and it's here to guard
399 406 # against infinite recursion.
400 407 raise error.ProgrammingError('Mercurial IO including templates is done'
401 408 ' with bytes, not strings, got %r' % thing)
402 409 elif thing is None:
403 410 pass
404 411 elif not util.safehasattr(thing, '__iter__'):
405 412 yield pycompat.bytestr(thing)
406 413 else:
407 414 for i in thing:
408 415 i = unwraphybrid(context, mapping, i)
409 416 if isinstance(i, bytes):
410 417 yield i
411 418 elif i is None:
412 419 pass
413 420 elif not util.safehasattr(i, '__iter__'):
414 421 yield pycompat.bytestr(i)
415 422 else:
416 423 for j in flatten(context, mapping, i):
417 424 yield j
418 425
419 426 def stringify(context, mapping, thing):
420 427 """Turn values into bytes by converting into text and concatenating them"""
421 428 if isinstance(thing, bytes):
422 429 return thing # retain localstr to be round-tripped
423 430 return b''.join(flatten(context, mapping, thing))
424 431
425 432 def findsymbolicname(arg):
426 433 """Find symbolic name for the given compiled expression; returns None
427 434 if nothing found reliably"""
428 435 while True:
429 436 func, data = arg
430 437 if func is runsymbol:
431 438 return data
432 439 elif func is runfilter:
433 440 arg = data[0]
434 441 else:
435 442 return None
436 443
437 444 def _unthunk(context, mapping, thing):
438 445 """Evaluate a lazy byte string into value"""
439 446 if not isinstance(thing, types.GeneratorType):
440 447 return thing
441 448 return stringify(context, mapping, thing)
442 449
443 450 def evalrawexp(context, mapping, arg):
444 451 """Evaluate given argument as a bare template object which may require
445 452 further processing (such as folding generator of strings)"""
446 453 func, data = arg
447 454 return func(context, mapping, data)
448 455
449 456 def evalfuncarg(context, mapping, arg):
450 457 """Evaluate given argument as value type"""
451 458 return _unwrapvalue(context, mapping, evalrawexp(context, mapping, arg))
452 459
453 460 # TODO: unify this with unwrapvalue() once the bug of templatefunc.join()
454 461 # is fixed. we can't do that right now because join() has to take a generator
455 462 # of byte strings as it is, not a lazy byte string.
456 463 def _unwrapvalue(context, mapping, thing):
457 464 thing = unwrapvalue(context, mapping, thing)
458 465 # evalrawexp() may return string, generator of strings or arbitrary object
459 466 # such as date tuple, but filter does not want generator.
460 467 return _unthunk(context, mapping, thing)
461 468
462 469 def evalboolean(context, mapping, arg):
463 470 """Evaluate given argument as boolean, but also takes boolean literals"""
464 471 func, data = arg
465 472 if func is runsymbol:
466 473 thing = func(context, mapping, data, default=None)
467 474 if thing is None:
468 475 # not a template keyword, takes as a boolean literal
469 476 thing = stringutil.parsebool(data)
470 477 else:
471 478 thing = func(context, mapping, data)
472 479 thing = unwrapvalue(context, mapping, thing)
473 480 if isinstance(thing, bool):
474 481 return thing
475 482 # other objects are evaluated as strings, which means 0 is True, but
476 483 # empty dict/list should be False as they are expected to be ''
477 484 return bool(stringify(context, mapping, thing))
478 485
479 486 def evaldate(context, mapping, arg, err=None):
480 487 """Evaluate given argument as a date tuple or a date string; returns
481 488 a (unixtime, offset) tuple"""
482 489 thing = evalrawexp(context, mapping, arg)
483 490 return unwrapdate(context, mapping, thing, err)
484 491
485 492 def unwrapdate(context, mapping, thing, err=None):
486 493 thing = _unwrapvalue(context, mapping, thing)
487 494 try:
488 495 return dateutil.parsedate(thing)
489 496 except AttributeError:
490 497 raise error.ParseError(err or _('not a date tuple nor a string'))
491 498 except error.ParseError:
492 499 if not err:
493 500 raise
494 501 raise error.ParseError(err)
495 502
496 503 def evalinteger(context, mapping, arg, err=None):
497 504 thing = evalrawexp(context, mapping, arg)
498 505 return unwrapinteger(context, mapping, thing, err)
499 506
500 507 def unwrapinteger(context, mapping, thing, err=None):
501 508 thing = _unwrapvalue(context, mapping, thing)
502 509 try:
503 510 return int(thing)
504 511 except (TypeError, ValueError):
505 512 raise error.ParseError(err or _('not an integer'))
506 513
507 514 def evalstring(context, mapping, arg):
508 515 return stringify(context, mapping, evalrawexp(context, mapping, arg))
509 516
510 517 def evalstringliteral(context, mapping, arg):
511 518 """Evaluate given argument as string template, but returns symbol name
512 519 if it is unknown"""
513 520 func, data = arg
514 521 if func is runsymbol:
515 522 thing = func(context, mapping, data, default=data)
516 523 else:
517 524 thing = func(context, mapping, data)
518 525 return stringify(context, mapping, thing)
519 526
520 527 _unwrapfuncbytype = {
521 528 None: _unwrapvalue,
522 529 bytes: stringify,
523 530 date: unwrapdate,
524 531 int: unwrapinteger,
525 532 }
526 533
527 534 def unwrapastype(context, mapping, thing, typ):
528 535 """Move the inner value object out of the wrapper and coerce its type"""
529 536 try:
530 537 f = _unwrapfuncbytype[typ]
531 538 except KeyError:
532 539 raise error.ProgrammingError('invalid type specified: %r' % typ)
533 540 return f(context, mapping, thing)
534 541
535 542 def runinteger(context, mapping, data):
536 543 return int(data)
537 544
538 545 def runstring(context, mapping, data):
539 546 return data
540 547
541 548 def _recursivesymbolblocker(key):
542 549 def showrecursion(**args):
543 550 raise error.Abort(_("recursive reference '%s' in template") % key)
544 551 return showrecursion
545 552
546 553 def runsymbol(context, mapping, key, default=''):
547 554 v = context.symbol(mapping, key)
548 555 if v is None:
549 556 # put poison to cut recursion. we can't move this to parsing phase
550 557 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
551 558 safemapping = mapping.copy()
552 559 safemapping[key] = _recursivesymbolblocker(key)
553 560 try:
554 561 v = context.process(key, safemapping)
555 562 except TemplateNotFound:
556 563 v = default
557 564 if callable(v) and getattr(v, '_requires', None) is None:
558 565 # old templatekw: expand all keywords and resources
559 566 # (TODO: deprecate this after porting web template keywords to new API)
560 567 props = {k: context._resources.lookup(context, mapping, k)
561 568 for k in context._resources.knownkeys()}
562 569 # pass context to _showcompatlist() through templatekw._showlist()
563 570 props['templ'] = context
564 571 props.update(mapping)
565 572 return v(**pycompat.strkwargs(props))
566 573 if callable(v):
567 574 # new templatekw
568 575 try:
569 576 return v(context, mapping)
570 577 except ResourceUnavailable:
571 578 # unsupported keyword is mapped to empty just like unknown keyword
572 579 return None
573 580 return v
574 581
575 582 def runtemplate(context, mapping, template):
576 583 for arg in template:
577 584 yield evalrawexp(context, mapping, arg)
578 585
579 586 def runfilter(context, mapping, data):
580 587 arg, filt = data
581 588 thing = evalrawexp(context, mapping, arg)
582 589 intype = getattr(filt, '_intype', None)
583 590 try:
584 591 thing = unwrapastype(context, mapping, thing, intype)
585 592 return filt(thing)
586 593 except error.ParseError as e:
587 594 raise error.ParseError(bytes(e), hint=_formatfiltererror(arg, filt))
588 595
589 596 def _formatfiltererror(arg, filt):
590 597 fn = pycompat.sysbytes(filt.__name__)
591 598 sym = findsymbolicname(arg)
592 599 if not sym:
593 600 return _("incompatible use of template filter '%s'") % fn
594 601 return (_("template filter '%s' is not compatible with keyword '%s'")
595 602 % (fn, sym))
596 603
597 604 def _checkeditermaps(darg, d):
598 605 try:
599 606 for v in d:
600 607 if not isinstance(v, dict):
601 608 raise TypeError
602 609 yield v
603 610 except TypeError:
604 611 sym = findsymbolicname(darg)
605 612 if sym:
606 613 raise error.ParseError(_("keyword '%s' is not iterable of mappings")
607 614 % sym)
608 615 else:
609 616 raise error.ParseError(_("%r is not iterable of mappings") % d)
610 617
611 618 def _iteroverlaymaps(context, origmapping, newmappings):
612 619 """Generate combined mappings from the original mapping and an iterable
613 620 of partial mappings to override the original"""
614 621 for i, nm in enumerate(newmappings):
615 622 lm = context.overlaymap(origmapping, nm)
616 623 lm['index'] = i
617 624 yield lm
618 625
619 626 def _applymap(context, mapping, diter, targ):
620 627 for lm in _iteroverlaymaps(context, mapping, diter):
621 628 yield evalrawexp(context, lm, targ)
622 629
623 630 def runmap(context, mapping, data):
624 631 darg, targ = data
625 632 d = evalrawexp(context, mapping, darg)
626 633 # TODO: a generator should be rejected because it is a thunk of lazy
627 634 # string, but we can't because hgweb abuses generator as a keyword
628 635 # that returns a list of dicts.
629 636 # TODO: drop _checkeditermaps() and pass 'd' to mappedgenerator so it
630 637 # can be restarted.
631 638 if isinstance(d, wrapped):
632 639 diter = d.itermaps(context)
633 640 else:
634 641 diter = _checkeditermaps(darg, d)
635 642 return mappedgenerator(_applymap, args=(mapping, diter, targ))
636 643
637 644 def runmember(context, mapping, data):
638 645 darg, memb = data
639 646 d = evalrawexp(context, mapping, darg)
640 647 if util.safehasattr(d, 'tomap'):
641 648 lm = context.overlaymap(mapping, d.tomap())
642 649 return runsymbol(context, lm, memb)
643 650 if util.safehasattr(d, 'get'):
644 651 return getdictitem(d, memb)
645 652
646 653 sym = findsymbolicname(darg)
647 654 if sym:
648 655 raise error.ParseError(_("keyword '%s' has no member") % sym)
649 656 else:
650 657 raise error.ParseError(_("%r has no member") % pycompat.bytestr(d))
651 658
652 659 def runnegate(context, mapping, data):
653 660 data = evalinteger(context, mapping, data,
654 661 _('negation needs an integer argument'))
655 662 return -data
656 663
657 664 def runarithmetic(context, mapping, data):
658 665 func, left, right = data
659 666 left = evalinteger(context, mapping, left,
660 667 _('arithmetic only defined on integers'))
661 668 right = evalinteger(context, mapping, right,
662 669 _('arithmetic only defined on integers'))
663 670 try:
664 671 return func(left, right)
665 672 except ZeroDivisionError:
666 673 raise error.Abort(_('division by zero is not defined'))
667 674
668 675 def getdictitem(dictarg, key):
669 676 val = dictarg.get(key)
670 677 if val is None:
671 678 return
672 679 return wraphybridvalue(dictarg, key, val)
673 680
674 681 def joinitems(itemiter, sep):
675 682 """Join items with the separator; Returns generator of bytes"""
676 683 first = True
677 684 for x in itemiter:
678 685 if first:
679 686 first = False
680 687 elif sep:
681 688 yield sep
682 689 yield x
@@ -1,1068 +1,1070 b''
1 1 $ HGMERGE=true; export HGMERGE
2 2
3 3 init
4 4
5 5 $ hg init repo
6 6 $ cd repo
7 7
8 8 commit
9 9
10 10 $ echo 'a' > a
11 11 $ hg ci -A -m test -u nobody -d '1 0'
12 12 adding a
13 13
14 14 annotate -c
15 15
16 16 $ hg annotate -c a
17 17 8435f90966e4: a
18 18
19 19 annotate -cl
20 20
21 21 $ hg annotate -cl a
22 22 8435f90966e4:1: a
23 23
24 24 annotate -d
25 25
26 26 $ hg annotate -d a
27 27 Thu Jan 01 00:00:01 1970 +0000: a
28 28
29 29 annotate -n
30 30
31 31 $ hg annotate -n a
32 32 0: a
33 33
34 34 annotate -nl
35 35
36 36 $ hg annotate -nl a
37 37 0:1: a
38 38
39 39 annotate -u
40 40
41 41 $ hg annotate -u a
42 42 nobody: a
43 43
44 44 annotate -cdnu
45 45
46 46 $ hg annotate -cdnu a
47 47 nobody 0 8435f90966e4 Thu Jan 01 00:00:01 1970 +0000: a
48 48
49 49 annotate -cdnul
50 50
51 51 $ hg annotate -cdnul a
52 52 nobody 0 8435f90966e4 Thu Jan 01 00:00:01 1970 +0000:1: a
53 53
54 54 annotate (JSON)
55 55
56 56 $ hg annotate -Tjson a
57 57 [
58 58 {
59 59 "abspath": "a",
60 60 "lines": [{"line": "a\n", "rev": 0}],
61 61 "path": "a"
62 62 }
63 63 ]
64 64
65 65 $ hg annotate -Tjson -cdfnul a
66 66 [
67 67 {
68 68 "abspath": "a",
69 69 "lines": [{"date": [1.0, 0], "file": "a", "line": "a\n", "line_number": 1, "node": "8435f90966e442695d2ded29fdade2bac5ad8065", "rev": 0, "user": "nobody"}],
70 70 "path": "a"
71 71 }
72 72 ]
73 73
74 74 log-like templating
75 75
76 76 $ hg annotate -T'{lines % "{rev} {node|shortest}: {line}"}' a
77 77 0 8435: a
78 78
79 79 $ cat <<EOF >>a
80 80 > a
81 81 > a
82 82 > EOF
83 83 $ hg ci -ma1 -d '1 0'
84 84 $ hg cp a b
85 85 $ hg ci -mb -d '1 0'
86 86 $ cat <<EOF >> b
87 87 > b4
88 88 > b5
89 89 > b6
90 90 > EOF
91 91 $ hg ci -mb2 -d '2 0'
92 92
93 93 default output of '{lines}' should be readable
94 94
95 95 $ hg annotate -T'{lines}' a
96 96 0: a
97 97 1: a
98 98 1: a
99 99 $ hg annotate -T'{join(lines, "\n")}' a
100 100 0: a
101 101
102 102 1: a
103 103
104 104 1: a
105 105
106 106 several filters can be applied to '{lines}'
107 107
108 $ hg annotate -T'{lines|json}\n' a
109 [{"line": "a\n", "rev": 0}, {"line": "a\n", "rev": 1}, {"line": "a\n", "rev": 1}]
108 110 $ hg annotate -T'{lines|stringify}' a
109 111 0: a
110 112 1: a
111 113 1: a
112 114 $ hg annotate -T'{lines|count}\n' a
113 115 3
114 116
115 117 annotate multiple files (JSON)
116 118
117 119 $ hg annotate -Tjson a b
118 120 [
119 121 {
120 122 "abspath": "a",
121 123 "lines": [{"line": "a\n", "rev": 0}, {"line": "a\n", "rev": 1}, {"line": "a\n", "rev": 1}],
122 124 "path": "a"
123 125 },
124 126 {
125 127 "abspath": "b",
126 128 "lines": [{"line": "a\n", "rev": 0}, {"line": "a\n", "rev": 1}, {"line": "a\n", "rev": 1}, {"line": "b4\n", "rev": 3}, {"line": "b5\n", "rev": 3}, {"line": "b6\n", "rev": 3}],
127 129 "path": "b"
128 130 }
129 131 ]
130 132
131 133 annotate multiple files (template)
132 134
133 135 $ hg annotate -T'== {abspath} ==\n{lines % "{rev}: {line}"}' a b
134 136 == a ==
135 137 0: a
136 138 1: a
137 139 1: a
138 140 == b ==
139 141 0: a
140 142 1: a
141 143 1: a
142 144 3: b4
143 145 3: b5
144 146 3: b6
145 147
146 148 annotate -n b
147 149
148 150 $ hg annotate -n b
149 151 0: a
150 152 1: a
151 153 1: a
152 154 3: b4
153 155 3: b5
154 156 3: b6
155 157
156 158 annotate --no-follow b
157 159
158 160 $ hg annotate --no-follow b
159 161 2: a
160 162 2: a
161 163 2: a
162 164 3: b4
163 165 3: b5
164 166 3: b6
165 167
166 168 annotate -nl b
167 169
168 170 $ hg annotate -nl b
169 171 0:1: a
170 172 1:2: a
171 173 1:3: a
172 174 3:4: b4
173 175 3:5: b5
174 176 3:6: b6
175 177
176 178 annotate -nf b
177 179
178 180 $ hg annotate -nf b
179 181 0 a: a
180 182 1 a: a
181 183 1 a: a
182 184 3 b: b4
183 185 3 b: b5
184 186 3 b: b6
185 187
186 188 annotate -nlf b
187 189
188 190 $ hg annotate -nlf b
189 191 0 a:1: a
190 192 1 a:2: a
191 193 1 a:3: a
192 194 3 b:4: b4
193 195 3 b:5: b5
194 196 3 b:6: b6
195 197
196 198 $ hg up -C 2
197 199 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
198 200 $ cat <<EOF >> b
199 201 > b4
200 202 > c
201 203 > b5
202 204 > EOF
203 205 $ hg ci -mb2.1 -d '2 0'
204 206 created new head
205 207 $ hg merge
206 208 merging b
207 209 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
208 210 (branch merge, don't forget to commit)
209 211 $ hg ci -mmergeb -d '3 0'
210 212
211 213 annotate after merge
212 214
213 215 $ hg annotate -nf b
214 216 0 a: a
215 217 1 a: a
216 218 1 a: a
217 219 3 b: b4
218 220 4 b: c
219 221 3 b: b5
220 222
221 223 annotate after merge with -l
222 224
223 225 $ hg annotate -nlf b
224 226 0 a:1: a
225 227 1 a:2: a
226 228 1 a:3: a
227 229 3 b:4: b4
228 230 4 b:5: c
229 231 3 b:5: b5
230 232
231 233 $ hg up -C 1
232 234 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
233 235 $ hg cp a b
234 236 $ cat <<EOF > b
235 237 > a
236 238 > z
237 239 > a
238 240 > EOF
239 241 $ hg ci -mc -d '3 0'
240 242 created new head
241 243 $ hg merge
242 244 merging b
243 245 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
244 246 (branch merge, don't forget to commit)
245 247 $ cat <<EOF >> b
246 248 > b4
247 249 > c
248 250 > b5
249 251 > EOF
250 252 $ echo d >> b
251 253 $ hg ci -mmerge2 -d '4 0'
252 254
253 255 annotate after rename merge
254 256
255 257 $ hg annotate -nf b
256 258 0 a: a
257 259 6 b: z
258 260 1 a: a
259 261 3 b: b4
260 262 4 b: c
261 263 3 b: b5
262 264 7 b: d
263 265
264 266 annotate after rename merge with -l
265 267
266 268 $ hg annotate -nlf b
267 269 0 a:1: a
268 270 6 b:2: z
269 271 1 a:3: a
270 272 3 b:4: b4
271 273 4 b:5: c
272 274 3 b:5: b5
273 275 7 b:7: d
274 276
275 277 --skip nothing (should be the same as no --skip at all)
276 278
277 279 $ hg annotate -nlf b --skip '1::0'
278 280 0 a:1: a
279 281 6 b:2: z
280 282 1 a:3: a
281 283 3 b:4: b4
282 284 4 b:5: c
283 285 3 b:5: b5
284 286 7 b:7: d
285 287
286 288 --skip a modified line. Note a slight behavior difference in pure - this is
287 289 because the pure code comes up with slightly different deltas internally.
288 290
289 291 $ hg annotate -nlf b --skip 6
290 292 0 a:1: a
291 293 1 a:2* z (no-pure !)
292 294 0 a:1* z (pure !)
293 295 1 a:3: a
294 296 3 b:4: b4
295 297 4 b:5: c
296 298 3 b:5: b5
297 299 7 b:7: d
298 300
299 301 --skip added lines (and test multiple skip)
300 302
301 303 $ hg annotate -nlf b --skip 3
302 304 0 a:1: a
303 305 6 b:2: z
304 306 1 a:3: a
305 307 1 a:3* b4
306 308 4 b:5: c
307 309 1 a:3* b5
308 310 7 b:7: d
309 311
310 312 $ hg annotate -nlf b --skip 4
311 313 0 a:1: a
312 314 6 b:2: z
313 315 1 a:3: a
314 316 3 b:4: b4
315 317 1 a:3* c
316 318 3 b:5: b5
317 319 7 b:7: d
318 320
319 321 $ hg annotate -nlf b --skip 3 --skip 4
320 322 0 a:1: a
321 323 6 b:2: z
322 324 1 a:3: a
323 325 1 a:3* b4
324 326 1 a:3* c
325 327 1 a:3* b5
326 328 7 b:7: d
327 329
328 330 $ hg annotate -nlf b --skip 'merge()'
329 331 0 a:1: a
330 332 6 b:2: z
331 333 1 a:3: a
332 334 3 b:4: b4
333 335 4 b:5: c
334 336 3 b:5: b5
335 337 3 b:5* d
336 338
337 339 --skip everything -- use the revision the file was introduced in
338 340
339 341 $ hg annotate -nlf b --skip 'all()'
340 342 0 a:1: a
341 343 0 a:1* z
342 344 0 a:1* a
343 345 0 a:1* b4
344 346 0 a:1* c
345 347 0 a:1* b5
346 348 0 a:1* d
347 349
348 350 Issue2807: alignment of line numbers with -l
349 351
350 352 $ echo more >> b
351 353 $ hg ci -mmore -d '5 0'
352 354 $ echo more >> b
353 355 $ hg ci -mmore -d '6 0'
354 356 $ echo more >> b
355 357 $ hg ci -mmore -d '7 0'
356 358 $ hg annotate -nlf b
357 359 0 a: 1: a
358 360 6 b: 2: z
359 361 1 a: 3: a
360 362 3 b: 4: b4
361 363 4 b: 5: c
362 364 3 b: 5: b5
363 365 7 b: 7: d
364 366 8 b: 8: more
365 367 9 b: 9: more
366 368 10 b:10: more
367 369
368 370 linkrev vs rev
369 371
370 372 $ hg annotate -r tip -n a
371 373 0: a
372 374 1: a
373 375 1: a
374 376
375 377 linkrev vs rev with -l
376 378
377 379 $ hg annotate -r tip -nl a
378 380 0:1: a
379 381 1:2: a
380 382 1:3: a
381 383
382 384 Issue589: "undelete" sequence leads to crash
383 385
384 386 annotate was crashing when trying to --follow something
385 387
386 388 like A -> B -> A
387 389
388 390 generate ABA rename configuration
389 391
390 392 $ echo foo > foo
391 393 $ hg add foo
392 394 $ hg ci -m addfoo
393 395 $ hg rename foo bar
394 396 $ hg ci -m renamefoo
395 397 $ hg rename bar foo
396 398 $ hg ci -m renamebar
397 399
398 400 annotate after ABA with follow
399 401
400 402 $ hg annotate --follow foo
401 403 foo: foo
402 404
403 405 missing file
404 406
405 407 $ hg ann nosuchfile
406 408 abort: nosuchfile: no such file in rev e9e6b4fa872f
407 409 [255]
408 410
409 411 annotate file without '\n' on last line
410 412
411 413 $ printf "" > c
412 414 $ hg ci -A -m test -u nobody -d '1 0'
413 415 adding c
414 416 $ hg annotate c
415 417 $ printf "a\nb" > c
416 418 $ hg ci -m test
417 419 $ hg annotate c
418 420 [0-9]+: a (re)
419 421 [0-9]+: b (re)
420 422
421 423 Issue3841: check annotation of the file of which filelog includes
422 424 merging between the revision and its ancestor
423 425
424 426 to reproduce the situation with recent Mercurial, this script uses (1)
425 427 "hg debugsetparents" to merge without ancestor check by "hg merge",
426 428 and (2) the extension to allow filelog merging between the revision
427 429 and its ancestor by overriding "repo._filecommit".
428 430
429 431 $ cat > ../legacyrepo.py <<EOF
430 432 > from __future__ import absolute_import
431 433 > from mercurial import error, node
432 434 > def reposetup(ui, repo):
433 435 > class legacyrepo(repo.__class__):
434 436 > def _filecommit(self, fctx, manifest1, manifest2,
435 437 > linkrev, tr, changelist):
436 438 > fname = fctx.path()
437 439 > text = fctx.data()
438 440 > flog = self.file(fname)
439 441 > fparent1 = manifest1.get(fname, node.nullid)
440 442 > fparent2 = manifest2.get(fname, node.nullid)
441 443 > meta = {}
442 444 > copy = fctx.renamed()
443 445 > if copy and copy[0] != fname:
444 446 > raise error.Abort('copying is not supported')
445 447 > if fparent2 != node.nullid:
446 448 > changelist.append(fname)
447 449 > return flog.add(text, meta, tr, linkrev,
448 450 > fparent1, fparent2)
449 451 > raise error.Abort('only merging is supported')
450 452 > repo.__class__ = legacyrepo
451 453 > EOF
452 454
453 455 $ cat > baz <<EOF
454 456 > 1
455 457 > 2
456 458 > 3
457 459 > 4
458 460 > 5
459 461 > EOF
460 462 $ hg add baz
461 463 $ hg commit -m "baz:0"
462 464
463 465 $ cat > baz <<EOF
464 466 > 1 baz:1
465 467 > 2
466 468 > 3
467 469 > 4
468 470 > 5
469 471 > EOF
470 472 $ hg commit -m "baz:1"
471 473
472 474 $ cat > baz <<EOF
473 475 > 1 baz:1
474 476 > 2 baz:2
475 477 > 3
476 478 > 4
477 479 > 5
478 480 > EOF
479 481 $ hg debugsetparents 17 17
480 482 $ hg --config extensions.legacyrepo=../legacyrepo.py commit -m "baz:2"
481 483 $ hg debugindexdot baz
482 484 digraph G {
483 485 -1 -> 0
484 486 0 -> 1
485 487 1 -> 2
486 488 1 -> 2
487 489 }
488 490 $ hg annotate baz
489 491 17: 1 baz:1
490 492 18: 2 baz:2
491 493 16: 3
492 494 16: 4
493 495 16: 5
494 496
495 497 $ cat > baz <<EOF
496 498 > 1 baz:1
497 499 > 2 baz:2
498 500 > 3 baz:3
499 501 > 4
500 502 > 5
501 503 > EOF
502 504 $ hg commit -m "baz:3"
503 505
504 506 $ cat > baz <<EOF
505 507 > 1 baz:1
506 508 > 2 baz:2
507 509 > 3 baz:3
508 510 > 4 baz:4
509 511 > 5
510 512 > EOF
511 513 $ hg debugsetparents 19 18
512 514 $ hg --config extensions.legacyrepo=../legacyrepo.py commit -m "baz:4"
513 515 $ hg debugindexdot baz
514 516 digraph G {
515 517 -1 -> 0
516 518 0 -> 1
517 519 1 -> 2
518 520 1 -> 2
519 521 2 -> 3
520 522 3 -> 4
521 523 2 -> 4
522 524 }
523 525 $ hg annotate baz
524 526 17: 1 baz:1
525 527 18: 2 baz:2
526 528 19: 3 baz:3
527 529 20: 4 baz:4
528 530 16: 5
529 531
530 532 annotate clean file
531 533
532 534 $ hg annotate -ncr "wdir()" foo
533 535 11 472b18db256d : foo
534 536
535 537 annotate modified file
536 538
537 539 $ echo foofoo >> foo
538 540 $ hg annotate -r "wdir()" foo
539 541 11 : foo
540 542 20+: foofoo
541 543
542 544 $ hg annotate -cr "wdir()" foo
543 545 472b18db256d : foo
544 546 b6bedd5477e7+: foofoo
545 547
546 548 $ hg annotate -ncr "wdir()" foo
547 549 11 472b18db256d : foo
548 550 20 b6bedd5477e7+: foofoo
549 551
550 552 $ hg annotate --debug -ncr "wdir()" foo
551 553 11 472b18db256d1e8282064eab4bfdaf48cbfe83cd : foo
552 554 20 b6bedd5477e797f25e568a6402d4697f3f895a72+: foofoo
553 555
554 556 $ hg annotate -udr "wdir()" foo
555 557 test Thu Jan 01 00:00:00 1970 +0000: foo
556 558 test [A-Za-z0-9:+ ]+: foofoo (re)
557 559
558 560 $ hg annotate -ncr "wdir()" -Tjson foo
559 561 [
560 562 {
561 563 "abspath": "foo",
562 564 "lines": [{"line": "foo\n", "node": "472b18db256d1e8282064eab4bfdaf48cbfe83cd", "rev": 11}, {"line": "foofoo\n", "node": null, "rev": null}],
563 565 "path": "foo"
564 566 }
565 567 ]
566 568
567 569 annotate added file
568 570
569 571 $ echo bar > bar
570 572 $ hg add bar
571 573 $ hg annotate -ncr "wdir()" bar
572 574 20 b6bedd5477e7+: bar
573 575
574 576 annotate renamed file
575 577
576 578 $ hg rename foo renamefoo2
577 579 $ hg annotate -ncr "wdir()" renamefoo2
578 580 11 472b18db256d : foo
579 581 20 b6bedd5477e7+: foofoo
580 582
581 583 annotate missing file
582 584
583 585 $ rm baz
584 586
585 587 $ hg annotate -ncr "wdir()" baz
586 588 abort: $TESTTMP\repo\baz: $ENOENT$ (windows !)
587 589 abort: $ENOENT$: $TESTTMP/repo/baz (no-windows !)
588 590 [255]
589 591
590 592 annotate removed file
591 593
592 594 $ hg rm baz
593 595
594 596 $ hg annotate -ncr "wdir()" baz
595 597 abort: $TESTTMP\repo\baz: $ENOENT$ (windows !)
596 598 abort: $ENOENT$: $TESTTMP/repo/baz (no-windows !)
597 599 [255]
598 600
599 601 $ hg revert --all --no-backup --quiet
600 602 $ hg id -n
601 603 20
602 604
603 605 Test followlines() revset; we usually check both followlines(pat, range) and
604 606 followlines(pat, range, descend=True) to make sure both give the same result
605 607 when they should.
606 608
607 609 $ echo a >> foo
608 610 $ hg ci -m 'foo: add a'
609 611 $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3:5)'
610 612 16: baz:0
611 613 19: baz:3
612 614 20: baz:4
613 615 $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3:5, startrev=20)'
614 616 16: baz:0
615 617 19: baz:3
616 618 20: baz:4
617 619 $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3:5, startrev=19)'
618 620 16: baz:0
619 621 19: baz:3
620 622 $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3:5, startrev=19, descend=True)'
621 623 19: baz:3
622 624 20: baz:4
623 625 $ printf "0\n0\n" | cat - baz > baz1
624 626 $ mv baz1 baz
625 627 $ hg ci -m 'added two lines with 0'
626 628 $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 5:7)'
627 629 16: baz:0
628 630 19: baz:3
629 631 20: baz:4
630 632 $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3:5, descend=true, startrev=19)'
631 633 19: baz:3
632 634 20: baz:4
633 635 $ echo 6 >> baz
634 636 $ hg ci -m 'added line 8'
635 637 $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 5:7)'
636 638 16: baz:0
637 639 19: baz:3
638 640 20: baz:4
639 641 $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3:5, startrev=19, descend=1)'
640 642 19: baz:3
641 643 20: baz:4
642 644 $ sed 's/3/3+/' baz > baz.new
643 645 $ mv baz.new baz
644 646 $ hg ci -m 'baz:3->3+'
645 647 $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 5:7, descend=0)'
646 648 16: baz:0
647 649 19: baz:3
648 650 20: baz:4
649 651 24: baz:3->3+
650 652 $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3:5, startrev=17, descend=True)'
651 653 19: baz:3
652 654 20: baz:4
653 655 24: baz:3->3+
654 656 $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 1:2, descend=false)'
655 657 22: added two lines with 0
656 658
657 659 file patterns are okay
658 660 $ hg log -T '{rev}: {desc}\n' -r 'followlines("path:baz", 1:2)'
659 661 22: added two lines with 0
660 662
661 663 renames are followed
662 664 $ hg mv baz qux
663 665 $ sed 's/4/4+/' qux > qux.new
664 666 $ mv qux.new qux
665 667 $ hg ci -m 'qux:4->4+'
666 668 $ hg log -T '{rev}: {desc}\n' -r 'followlines(qux, 5:7)'
667 669 16: baz:0
668 670 19: baz:3
669 671 20: baz:4
670 672 24: baz:3->3+
671 673 25: qux:4->4+
672 674
673 675 but are missed when following children
674 676 $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 5:7, startrev=22, descend=True)'
675 677 24: baz:3->3+
676 678
677 679 merge
678 680 $ hg up 24 --quiet
679 681 $ echo 7 >> baz
680 682 $ hg ci -m 'one more line, out of line range'
681 683 created new head
682 684 $ sed 's/3+/3-/' baz > baz.new
683 685 $ mv baz.new baz
684 686 $ hg ci -m 'baz:3+->3-'
685 687 $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 5:7)'
686 688 16: baz:0
687 689 19: baz:3
688 690 20: baz:4
689 691 24: baz:3->3+
690 692 27: baz:3+->3-
691 693 $ hg merge 25
692 694 merging baz and qux to qux
693 695 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
694 696 (branch merge, don't forget to commit)
695 697 $ hg ci -m merge
696 698 $ hg log -T '{rev}: {desc}\n' -r 'followlines(qux, 5:7)'
697 699 16: baz:0
698 700 19: baz:3
699 701 20: baz:4
700 702 24: baz:3->3+
701 703 25: qux:4->4+
702 704 27: baz:3+->3-
703 705 28: merge
704 706 $ hg up 25 --quiet
705 707 $ hg merge 27
706 708 merging qux and baz to qux
707 709 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
708 710 (branch merge, don't forget to commit)
709 711 $ hg ci -m 'merge from other side'
710 712 created new head
711 713 $ hg log -T '{rev}: {desc}\n' -r 'followlines(qux, 5:7)'
712 714 16: baz:0
713 715 19: baz:3
714 716 20: baz:4
715 717 24: baz:3->3+
716 718 25: qux:4->4+
717 719 27: baz:3+->3-
718 720 29: merge from other side
719 721 $ hg up 24 --quiet
720 722
721 723 we are missing the branch with rename when following children
722 724 $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 5:7, startrev=26, descend=True)'
723 725 27: baz:3+->3-
724 726
725 727 we follow all branches in descending direction
726 728 $ hg up 23 --quiet
727 729 $ sed 's/3/+3/' baz > baz.new
728 730 $ mv baz.new baz
729 731 $ hg ci -m 'baz:3->+3'
730 732 created new head
731 733 $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 2:5, startrev=16, descend=True)' --graph
732 734 @ 30: baz:3->+3
733 735 :
734 736 : o 27: baz:3+->3-
735 737 : :
736 738 : o 24: baz:3->3+
737 739 :/
738 740 o 20: baz:4
739 741 |\
740 742 | o 19: baz:3
741 743 |/
742 744 o 18: baz:2
743 745 :
744 746 o 16: baz:0
745 747 |
746 748 ~
747 749
748 750 Issue5595: on a merge changeset with different line ranges depending on
749 751 parent, be conservative and use the surrounding interval to avoid loosing
750 752 track of possible further descendants in specified range.
751 753
752 754 $ hg up 23 --quiet
753 755 $ hg cat baz -r 24
754 756 0
755 757 0
756 758 1 baz:1
757 759 2 baz:2
758 760 3+ baz:3
759 761 4 baz:4
760 762 5
761 763 6
762 764 $ cat > baz << EOF
763 765 > 0
764 766 > 0
765 767 > a
766 768 > b
767 769 > 3+ baz:3
768 770 > 4 baz:4
769 771 > y
770 772 > z
771 773 > EOF
772 774 $ hg ci -m 'baz: mostly rewrite with some content from 24'
773 775 created new head
774 776 $ hg merge --tool :merge-other 24
775 777 merging baz
776 778 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
777 779 (branch merge, don't forget to commit)
778 780 $ hg ci -m 'merge forgetting about baz rewrite'
779 781 $ cat > baz << EOF
780 782 > 0
781 783 > 0
782 784 > 1 baz:1
783 785 > 2+ baz:2
784 786 > 3+ baz:3
785 787 > 4 baz:4
786 788 > 5
787 789 > 6
788 790 > EOF
789 791 $ hg ci -m 'baz: narrow change (2->2+)'
790 792 $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3:4, startrev=20, descend=True)' --graph
791 793 @ 33: baz: narrow change (2->2+)
792 794 |
793 795 o 32: merge forgetting about baz rewrite
794 796 |\
795 797 | o 31: baz: mostly rewrite with some content from 24
796 798 | :
797 799 | : o 30: baz:3->+3
798 800 | :/
799 801 +---o 27: baz:3+->3-
800 802 | :
801 803 o : 24: baz:3->3+
802 804 :/
803 805 o 20: baz:4
804 806 |\
805 807 ~ ~
806 808
807 809 check error cases
808 810 $ hg up 24 --quiet
809 811 $ hg log -r 'followlines()'
810 812 hg: parse error: followlines takes at least 1 positional arguments
811 813 [255]
812 814 $ hg log -r 'followlines(baz)'
813 815 hg: parse error: followlines requires a line range
814 816 [255]
815 817 $ hg log -r 'followlines(baz, 1)'
816 818 hg: parse error: followlines expects a line range
817 819 [255]
818 820 $ hg log -r 'followlines(baz, 1:2, startrev=desc("b"))'
819 821 hg: parse error: followlines expects exactly one revision
820 822 [255]
821 823 $ hg log -r 'followlines("glob:*", 1:2)'
822 824 hg: parse error: followlines expects exactly one file
823 825 [255]
824 826 $ hg log -r 'followlines(baz, 1:)'
825 827 hg: parse error: line range bounds must be integers
826 828 [255]
827 829 $ hg log -r 'followlines(baz, :1)'
828 830 hg: parse error: line range bounds must be integers
829 831 [255]
830 832 $ hg log -r 'followlines(baz, x:4)'
831 833 hg: parse error: line range bounds must be integers
832 834 [255]
833 835 $ hg log -r 'followlines(baz, 5:4)'
834 836 hg: parse error: line range must be positive
835 837 [255]
836 838 $ hg log -r 'followlines(baz, 0:4)'
837 839 hg: parse error: fromline must be strictly positive
838 840 [255]
839 841 $ hg log -r 'followlines(baz, 2:40)'
840 842 abort: line range exceeds file size
841 843 [255]
842 844 $ hg log -r 'followlines(baz, 2:4, startrev=20, descend=[1])'
843 845 hg: parse error at 43: not a prefix: [
844 846 (followlines(baz, 2:4, startrev=20, descend=[1])
845 847 ^ here)
846 848 [255]
847 849 $ hg log -r 'followlines(baz, 2:4, startrev=20, descend=a)'
848 850 hg: parse error: descend argument must be a boolean
849 851 [255]
850 852
851 853 Test empty annotate output
852 854
853 855 $ printf '\0' > binary
854 856 $ touch empty
855 857 $ hg ci -qAm 'add binary and empty files'
856 858
857 859 $ hg annotate binary empty
858 860 binary: binary file
859 861
860 862 $ hg annotate -Tjson binary empty
861 863 [
862 864 {
863 865 "abspath": "binary",
864 866 "path": "binary"
865 867 },
866 868 {
867 869 "abspath": "empty",
868 870 "lines": [],
869 871 "path": "empty"
870 872 }
871 873 ]
872 874
873 875 Test annotate with whitespace options
874 876
875 877 $ cd ..
876 878 $ hg init repo-ws
877 879 $ cd repo-ws
878 880 $ cat > a <<EOF
879 881 > aa
880 882 >
881 883 > b b
882 884 > EOF
883 885 $ hg ci -Am "adda"
884 886 adding a
885 887 $ sed 's/EOL$//g' > a <<EOF
886 888 > a a
887 889 >
888 890 > EOL
889 891 > b b
890 892 > EOF
891 893 $ hg ci -m "changea"
892 894
893 895 Annotate with no option
894 896
895 897 $ hg annotate a
896 898 1: a a
897 899 0:
898 900 1:
899 901 1: b b
900 902
901 903 Annotate with --ignore-space-change
902 904
903 905 $ hg annotate --ignore-space-change a
904 906 1: a a
905 907 1:
906 908 0:
907 909 0: b b
908 910
909 911 Annotate with --ignore-all-space
910 912
911 913 $ hg annotate --ignore-all-space a
912 914 0: a a
913 915 0:
914 916 1:
915 917 0: b b
916 918
917 919 Annotate with --ignore-blank-lines (similar to no options case)
918 920
919 921 $ hg annotate --ignore-blank-lines a
920 922 1: a a
921 923 0:
922 924 1:
923 925 1: b b
924 926
925 927 $ cd ..
926 928
927 929 Annotate with orphaned CR (issue5798)
928 930 -------------------------------------
929 931
930 932 $ hg init repo-cr
931 933 $ cd repo-cr
932 934
933 935 $ cat <<'EOF' >> "$TESTTMP/substcr.py"
934 936 > import sys
935 937 > from mercurial.utils import procutil
936 938 > procutil.setbinary(sys.stdin)
937 939 > procutil.setbinary(sys.stdout)
938 940 > stdin = getattr(sys.stdin, 'buffer', sys.stdin)
939 941 > stdout = getattr(sys.stdout, 'buffer', sys.stdout)
940 942 > stdout.write(stdin.read().replace(b'\r', b'[CR]'))
941 943 > EOF
942 944
943 945 >>> with open('a', 'wb') as f:
944 946 ... f.write(b'0a\r0b\r\n0c\r0d\r\n0e\n0f\n0g') and None
945 947 $ hg ci -qAm0
946 948 >>> with open('a', 'wb') as f:
947 949 ... f.write(b'0a\r0b\r\n1c\r1d\r\n0e\n1f\n0g') and None
948 950 $ hg ci -m1
949 951
950 952 $ hg annotate -r0 a | $PYTHON "$TESTTMP/substcr.py"
951 953 0: 0a[CR]0b[CR]
952 954 0: 0c[CR]0d[CR]
953 955 0: 0e
954 956 0: 0f
955 957 0: 0g
956 958 $ hg annotate -r1 a | $PYTHON "$TESTTMP/substcr.py"
957 959 0: 0a[CR]0b[CR]
958 960 1: 1c[CR]1d[CR]
959 961 0: 0e
960 962 1: 1f
961 963 0: 0g
962 964
963 965 $ cd ..
964 966
965 967 Annotate with linkrev pointing to another branch
966 968 ------------------------------------------------
967 969
968 970 create history with a filerev whose linkrev points to another branch
969 971
970 972 $ hg init branchedlinkrev
971 973 $ cd branchedlinkrev
972 974 $ echo A > a
973 975 $ hg commit -Am 'contentA'
974 976 adding a
975 977 $ echo B >> a
976 978 $ hg commit -m 'contentB'
977 979 $ hg up --rev 'desc(contentA)'
978 980 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
979 981 $ echo unrelated > unrelated
980 982 $ hg commit -Am 'unrelated'
981 983 adding unrelated
982 984 created new head
983 985 $ hg graft -r 'desc(contentB)'
984 986 grafting 1:fd27c222e3e6 "contentB"
985 987 $ echo C >> a
986 988 $ hg commit -m 'contentC'
987 989 $ echo W >> a
988 990 $ hg log -G
989 991 @ changeset: 4:072f1e8df249
990 992 | tag: tip
991 993 | user: test
992 994 | date: Thu Jan 01 00:00:00 1970 +0000
993 995 | summary: contentC
994 996 |
995 997 o changeset: 3:ff38df03cc4b
996 998 | user: test
997 999 | date: Thu Jan 01 00:00:00 1970 +0000
998 1000 | summary: contentB
999 1001 |
1000 1002 o changeset: 2:62aaf3f6fc06
1001 1003 | parent: 0:f0932f74827e
1002 1004 | user: test
1003 1005 | date: Thu Jan 01 00:00:00 1970 +0000
1004 1006 | summary: unrelated
1005 1007 |
1006 1008 | o changeset: 1:fd27c222e3e6
1007 1009 |/ user: test
1008 1010 | date: Thu Jan 01 00:00:00 1970 +0000
1009 1011 | summary: contentB
1010 1012 |
1011 1013 o changeset: 0:f0932f74827e
1012 1014 user: test
1013 1015 date: Thu Jan 01 00:00:00 1970 +0000
1014 1016 summary: contentA
1015 1017
1016 1018
1017 1019 Annotate should list ancestor of starting revision only
1018 1020
1019 1021 $ hg annotate a
1020 1022 0: A
1021 1023 3: B
1022 1024 4: C
1023 1025
1024 1026 $ hg annotate a -r 'wdir()'
1025 1027 0 : A
1026 1028 3 : B
1027 1029 4 : C
1028 1030 4+: W
1029 1031
1030 1032 Even when the starting revision is the linkrev-shadowed one:
1031 1033
1032 1034 $ hg annotate a -r 3
1033 1035 0: A
1034 1036 3: B
1035 1037
1036 1038 $ cd ..
1037 1039
1038 1040 Issue5360: Deleted chunk in p1 of a merge changeset
1039 1041
1040 1042 $ hg init repo-5360
1041 1043 $ cd repo-5360
1042 1044 $ echo 1 > a
1043 1045 $ hg commit -A a -m 1
1044 1046 $ echo 2 >> a
1045 1047 $ hg commit -m 2
1046 1048 $ echo a > a
1047 1049 $ hg commit -m a
1048 1050 $ hg update '.^' -q
1049 1051 $ echo 3 >> a
1050 1052 $ hg commit -m 3 -q
1051 1053 $ hg merge 2 -q
1052 1054 $ cat > a << EOF
1053 1055 > b
1054 1056 > 1
1055 1057 > 2
1056 1058 > 3
1057 1059 > a
1058 1060 > EOF
1059 1061 $ hg resolve --mark -q
1060 1062 $ hg commit -m m
1061 1063 $ hg annotate a
1062 1064 4: b
1063 1065 0: 1
1064 1066 1: 2
1065 1067 3: 3
1066 1068 2: a
1067 1069
1068 1070 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now