##// END OF EJS Templates
templater: fix if() to not evaluate False as bool('False')...
Yuya Nishihara -
r29816:034412ca default
parent child Browse files
Show More
@@ -1,1175 +1,1184 b''
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
9 9
10 10 import os
11 11 import re
12 12 import types
13 13
14 14 from .i18n import _
15 15 from . import (
16 16 config,
17 17 error,
18 18 minirst,
19 19 parser,
20 20 registrar,
21 21 revset as revsetmod,
22 22 templatefilters,
23 23 templatekw,
24 24 util,
25 25 )
26 26
27 27 # template parsing
28 28
29 29 elements = {
30 30 # token-type: binding-strength, primary, prefix, infix, suffix
31 31 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
32 32 ",": (2, None, None, ("list", 2), None),
33 33 "|": (5, None, None, ("|", 5), None),
34 34 "%": (6, None, None, ("%", 6), None),
35 35 ")": (0, None, None, None, None),
36 36 "integer": (0, "integer", None, None, None),
37 37 "symbol": (0, "symbol", None, None, None),
38 38 "string": (0, "string", None, None, None),
39 39 "template": (0, "template", None, None, None),
40 40 "end": (0, None, None, None, None),
41 41 }
42 42
43 43 def tokenize(program, start, end, term=None):
44 44 """Parse a template expression into a stream of tokens, which must end
45 45 with term if specified"""
46 46 pos = start
47 47 while pos < end:
48 48 c = program[pos]
49 49 if c.isspace(): # skip inter-token whitespace
50 50 pass
51 51 elif c in "(,)%|": # handle simple operators
52 52 yield (c, None, pos)
53 53 elif c in '"\'': # handle quoted templates
54 54 s = pos + 1
55 55 data, pos = _parsetemplate(program, s, end, c)
56 56 yield ('template', data, s)
57 57 pos -= 1
58 58 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
59 59 # handle quoted strings
60 60 c = program[pos + 1]
61 61 s = pos = pos + 2
62 62 while pos < end: # find closing quote
63 63 d = program[pos]
64 64 if d == '\\': # skip over escaped characters
65 65 pos += 2
66 66 continue
67 67 if d == c:
68 68 yield ('string', program[s:pos], s)
69 69 break
70 70 pos += 1
71 71 else:
72 72 raise error.ParseError(_("unterminated string"), s)
73 73 elif c.isdigit() or c == '-':
74 74 s = pos
75 75 if c == '-': # simply take negate operator as part of integer
76 76 pos += 1
77 77 if pos >= end or not program[pos].isdigit():
78 78 raise error.ParseError(_("integer literal without digits"), s)
79 79 pos += 1
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 (r"\'", r'\"')
88 88 or c == 'r' and program[pos:pos + 3] in (r"r\'", r'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 + 1)
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('foo{bar}"baz', 0, 12)
145 145 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
146 146 >>> _parsetemplate('foo{bar}"baz', 0, 12, quote='"')
147 147 ([('string', 'foo'), ('symbol', 'bar')], 9)
148 148 >>> _parsetemplate('foo"{bar}', 0, 9, quote='"')
149 149 ([('string', 'foo')], 4)
150 150 >>> _parsetemplate(r'foo\"bar"baz', 0, 12, quote='"')
151 151 ([('string', 'foo"'), ('string', 'bar')], 9)
152 152 >>> _parsetemplate(r'foo\\"bar', 0, 10, quote='"')
153 153 ([('string', 'foo\\')], 6)
154 154 """
155 155 parsed = []
156 156 sepchars = '{' + quote
157 157 pos = start
158 158 p = parser.parser(elements)
159 159 while pos < stop:
160 160 n = min((tmpl.find(c, pos, stop) for c in sepchars),
161 161 key=lambda n: (n < 0, n))
162 162 if n < 0:
163 163 parsed.append(('string', parser.unescapestr(tmpl[pos:stop])))
164 164 pos = stop
165 165 break
166 166 c = tmpl[n]
167 167 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
168 168 if bs % 2 == 1:
169 169 # escaped (e.g. '\{', '\\\{', but not '\\{')
170 170 parsed.append(('string', parser.unescapestr(tmpl[pos:n - 1]) + c))
171 171 pos = n + 1
172 172 continue
173 173 if n > pos:
174 174 parsed.append(('string', parser.unescapestr(tmpl[pos:n])))
175 175 if c == quote:
176 176 return parsed, n + 1
177 177
178 178 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
179 179 parsed.append(parseres)
180 180
181 181 if quote:
182 182 raise error.ParseError(_("unterminated string"), start)
183 183 return parsed, pos
184 184
185 185 def _unnesttemplatelist(tree):
186 186 """Expand list of templates to node tuple
187 187
188 188 >>> def f(tree):
189 189 ... print prettyformat(_unnesttemplatelist(tree))
190 190 >>> f(('template', []))
191 191 ('string', '')
192 192 >>> f(('template', [('string', 'foo')]))
193 193 ('string', 'foo')
194 194 >>> f(('template', [('string', 'foo'), ('symbol', 'rev')]))
195 195 (template
196 196 ('string', 'foo')
197 197 ('symbol', 'rev'))
198 198 >>> f(('template', [('symbol', 'rev')])) # template(rev) -> str
199 199 (template
200 200 ('symbol', 'rev'))
201 201 >>> f(('template', [('template', [('string', 'foo')])]))
202 202 ('string', 'foo')
203 203 """
204 204 if not isinstance(tree, tuple):
205 205 return tree
206 206 op = tree[0]
207 207 if op != 'template':
208 208 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
209 209
210 210 assert len(tree) == 2
211 211 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
212 212 if not xs:
213 213 return ('string', '') # empty template ""
214 214 elif len(xs) == 1 and xs[0][0] == 'string':
215 215 return xs[0] # fast path for string with no template fragment "x"
216 216 else:
217 217 return (op,) + xs
218 218
219 219 def parse(tmpl):
220 220 """Parse template string into tree"""
221 221 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
222 222 assert pos == len(tmpl), 'unquoted template should be consumed'
223 223 return _unnesttemplatelist(('template', parsed))
224 224
225 225 def _parseexpr(expr):
226 226 """Parse a template expression into tree
227 227
228 228 >>> _parseexpr('"foo"')
229 229 ('string', 'foo')
230 230 >>> _parseexpr('foo(bar)')
231 231 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
232 232 >>> _parseexpr('foo(')
233 233 Traceback (most recent call last):
234 234 ...
235 235 ParseError: ('not a prefix: end', 4)
236 236 >>> _parseexpr('"foo" "bar"')
237 237 Traceback (most recent call last):
238 238 ...
239 239 ParseError: ('invalid token', 7)
240 240 """
241 241 p = parser.parser(elements)
242 242 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
243 243 if pos != len(expr):
244 244 raise error.ParseError(_('invalid token'), pos)
245 245 return _unnesttemplatelist(tree)
246 246
247 247 def prettyformat(tree):
248 248 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
249 249
250 250 def compileexp(exp, context, curmethods):
251 251 """Compile parsed template tree to (func, data) pair"""
252 252 t = exp[0]
253 253 if t in curmethods:
254 254 return curmethods[t](exp, context)
255 255 raise error.ParseError(_("unknown method '%s'") % t)
256 256
257 257 # template evaluation
258 258
259 259 def getsymbol(exp):
260 260 if exp[0] == 'symbol':
261 261 return exp[1]
262 262 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
263 263
264 264 def getlist(x):
265 265 if not x:
266 266 return []
267 267 if x[0] == 'list':
268 268 return getlist(x[1]) + [x[2]]
269 269 return [x]
270 270
271 271 def gettemplate(exp, context):
272 272 """Compile given template tree or load named template from map file;
273 273 returns (func, data) pair"""
274 274 if exp[0] in ('template', 'string'):
275 275 return compileexp(exp, context, methods)
276 276 if exp[0] == 'symbol':
277 277 # unlike runsymbol(), here 'symbol' is always taken as template name
278 278 # even if it exists in mapping. this allows us to override mapping
279 279 # by web templates, e.g. 'changelogtag' is redefined in map file.
280 280 return context._load(exp[1])
281 281 raise error.ParseError(_("expected template specifier"))
282 282
283 283 def evalfuncarg(context, mapping, arg):
284 284 func, data = arg
285 285 # func() may return string, generator of strings or arbitrary object such
286 286 # as date tuple, but filter does not want generator.
287 287 thing = func(context, mapping, data)
288 288 if isinstance(thing, types.GeneratorType):
289 289 thing = stringify(thing)
290 290 return thing
291 291
292 def evalboolean(context, mapping, arg):
293 func, data = arg
294 thing = func(context, mapping, data)
295 if isinstance(thing, bool):
296 return thing
297 # other objects are evaluated as strings, which means 0 is True, but
298 # empty dict/list should be False as they are expected to be ''
299 return bool(stringify(thing))
300
292 301 def evalinteger(context, mapping, arg, err):
293 302 v = evalfuncarg(context, mapping, arg)
294 303 try:
295 304 return int(v)
296 305 except (TypeError, ValueError):
297 306 raise error.ParseError(err)
298 307
299 308 def evalstring(context, mapping, arg):
300 309 func, data = arg
301 310 return stringify(func(context, mapping, data))
302 311
303 312 def evalstringliteral(context, mapping, arg):
304 313 """Evaluate given argument as string template, but returns symbol name
305 314 if it is unknown"""
306 315 func, data = arg
307 316 if func is runsymbol:
308 317 thing = func(context, mapping, data, default=data)
309 318 else:
310 319 thing = func(context, mapping, data)
311 320 return stringify(thing)
312 321
313 322 def runinteger(context, mapping, data):
314 323 return int(data)
315 324
316 325 def runstring(context, mapping, data):
317 326 return data
318 327
319 328 def _recursivesymbolblocker(key):
320 329 def showrecursion(**args):
321 330 raise error.Abort(_("recursive reference '%s' in template") % key)
322 331 return showrecursion
323 332
324 333 def _runrecursivesymbol(context, mapping, key):
325 334 raise error.Abort(_("recursive reference '%s' in template") % key)
326 335
327 336 def runsymbol(context, mapping, key, default=''):
328 337 v = mapping.get(key)
329 338 if v is None:
330 339 v = context._defaults.get(key)
331 340 if v is None:
332 341 # put poison to cut recursion. we can't move this to parsing phase
333 342 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
334 343 safemapping = mapping.copy()
335 344 safemapping[key] = _recursivesymbolblocker(key)
336 345 try:
337 346 v = context.process(key, safemapping)
338 347 except TemplateNotFound:
339 348 v = default
340 349 if callable(v):
341 350 return v(**mapping)
342 351 return v
343 352
344 353 def buildtemplate(exp, context):
345 354 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
346 355 return (runtemplate, ctmpl)
347 356
348 357 def runtemplate(context, mapping, template):
349 358 for func, data in template:
350 359 yield func(context, mapping, data)
351 360
352 361 def buildfilter(exp, context):
353 362 arg = compileexp(exp[1], context, methods)
354 363 n = getsymbol(exp[2])
355 364 if n in context._filters:
356 365 filt = context._filters[n]
357 366 return (runfilter, (arg, filt))
358 367 if n in funcs:
359 368 f = funcs[n]
360 369 return (f, [arg])
361 370 raise error.ParseError(_("unknown function '%s'") % n)
362 371
363 372 def runfilter(context, mapping, data):
364 373 arg, filt = data
365 374 thing = evalfuncarg(context, mapping, arg)
366 375 try:
367 376 return filt(thing)
368 377 except (ValueError, AttributeError, TypeError):
369 378 if isinstance(arg[1], tuple):
370 379 dt = arg[1][1]
371 380 else:
372 381 dt = arg[1]
373 382 raise error.Abort(_("template filter '%s' is not compatible with "
374 383 "keyword '%s'") % (filt.func_name, dt))
375 384
376 385 def buildmap(exp, context):
377 386 func, data = compileexp(exp[1], context, methods)
378 387 tfunc, tdata = gettemplate(exp[2], context)
379 388 return (runmap, (func, data, tfunc, tdata))
380 389
381 390 def runmap(context, mapping, data):
382 391 func, data, tfunc, tdata = data
383 392 d = func(context, mapping, data)
384 393 if util.safehasattr(d, 'itermaps'):
385 394 diter = d.itermaps()
386 395 else:
387 396 try:
388 397 diter = iter(d)
389 398 except TypeError:
390 399 if func is runsymbol:
391 400 raise error.ParseError(_("keyword '%s' is not iterable") % data)
392 401 else:
393 402 raise error.ParseError(_("%r is not iterable") % d)
394 403
395 404 for i in diter:
396 405 lm = mapping.copy()
397 406 if isinstance(i, dict):
398 407 lm.update(i)
399 408 lm['originalnode'] = mapping.get('node')
400 409 yield tfunc(context, lm, tdata)
401 410 else:
402 411 # v is not an iterable of dicts, this happen when 'key'
403 412 # has been fully expanded already and format is useless.
404 413 # If so, return the expanded value.
405 414 yield i
406 415
407 416 def buildfunc(exp, context):
408 417 n = getsymbol(exp[1])
409 418 args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
410 419 if n in funcs:
411 420 f = funcs[n]
412 421 return (f, args)
413 422 if n in context._filters:
414 423 if len(args) != 1:
415 424 raise error.ParseError(_("filter %s expects one argument") % n)
416 425 f = context._filters[n]
417 426 return (runfilter, (args[0], f))
418 427 raise error.ParseError(_("unknown function '%s'") % n)
419 428
420 429 # dict of template built-in functions
421 430 funcs = {}
422 431
423 432 templatefunc = registrar.templatefunc(funcs)
424 433
425 434 @templatefunc('date(date[, fmt])')
426 435 def date(context, mapping, args):
427 436 """Format a date. See :hg:`help dates` for formatting
428 437 strings. The default is a Unix date format, including the timezone:
429 438 "Mon Sep 04 15:13:13 2006 0700"."""
430 439 if not (1 <= len(args) <= 2):
431 440 # i18n: "date" is a keyword
432 441 raise error.ParseError(_("date expects one or two arguments"))
433 442
434 443 date = evalfuncarg(context, mapping, args[0])
435 444 fmt = None
436 445 if len(args) == 2:
437 446 fmt = evalstring(context, mapping, args[1])
438 447 try:
439 448 if fmt is None:
440 449 return util.datestr(date)
441 450 else:
442 451 return util.datestr(date, fmt)
443 452 except (TypeError, ValueError):
444 453 # i18n: "date" is a keyword
445 454 raise error.ParseError(_("date expects a date information"))
446 455
447 456 @templatefunc('diff([includepattern [, excludepattern]])')
448 457 def diff(context, mapping, args):
449 458 """Show a diff, optionally
450 459 specifying files to include or exclude."""
451 460 if len(args) > 2:
452 461 # i18n: "diff" is a keyword
453 462 raise error.ParseError(_("diff expects zero, one, or two arguments"))
454 463
455 464 def getpatterns(i):
456 465 if i < len(args):
457 466 s = evalstring(context, mapping, args[i]).strip()
458 467 if s:
459 468 return [s]
460 469 return []
461 470
462 471 ctx = mapping['ctx']
463 472 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
464 473
465 474 return ''.join(chunks)
466 475
467 476 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
468 477 def fill(context, mapping, args):
469 478 """Fill many
470 479 paragraphs with optional indentation. See the "fill" filter."""
471 480 if not (1 <= len(args) <= 4):
472 481 # i18n: "fill" is a keyword
473 482 raise error.ParseError(_("fill expects one to four arguments"))
474 483
475 484 text = evalstring(context, mapping, args[0])
476 485 width = 76
477 486 initindent = ''
478 487 hangindent = ''
479 488 if 2 <= len(args) <= 4:
480 489 width = evalinteger(context, mapping, args[1],
481 490 # i18n: "fill" is a keyword
482 491 _("fill expects an integer width"))
483 492 try:
484 493 initindent = evalstring(context, mapping, args[2])
485 494 hangindent = evalstring(context, mapping, args[3])
486 495 except IndexError:
487 496 pass
488 497
489 498 return templatefilters.fill(text, width, initindent, hangindent)
490 499
491 500 @templatefunc('pad(text, width[, fillchar=\' \'[, right=False]])')
492 501 def pad(context, mapping, args):
493 502 """Pad text with a
494 503 fill character."""
495 504 if not (2 <= len(args) <= 4):
496 505 # i18n: "pad" is a keyword
497 506 raise error.ParseError(_("pad() expects two to four arguments"))
498 507
499 508 width = evalinteger(context, mapping, args[1],
500 509 # i18n: "pad" is a keyword
501 510 _("pad() expects an integer width"))
502 511
503 512 text = evalstring(context, mapping, args[0])
504 513
505 514 right = False
506 515 fillchar = ' '
507 516 if len(args) > 2:
508 517 fillchar = evalstring(context, mapping, args[2])
509 518 if len(args) > 3:
510 519 right = util.parsebool(args[3][1])
511 520
512 521 if right:
513 522 return text.rjust(width, fillchar)
514 523 else:
515 524 return text.ljust(width, fillchar)
516 525
517 526 @templatefunc('indent(text, indentchars[, firstline])')
518 527 def indent(context, mapping, args):
519 528 """Indents all non-empty lines
520 529 with the characters given in the indentchars string. An optional
521 530 third parameter will override the indent for the first line only
522 531 if present."""
523 532 if not (2 <= len(args) <= 3):
524 533 # i18n: "indent" is a keyword
525 534 raise error.ParseError(_("indent() expects two or three arguments"))
526 535
527 536 text = evalstring(context, mapping, args[0])
528 537 indent = evalstring(context, mapping, args[1])
529 538
530 539 if len(args) == 3:
531 540 firstline = evalstring(context, mapping, args[2])
532 541 else:
533 542 firstline = indent
534 543
535 544 # the indent function doesn't indent the first line, so we do it here
536 545 return templatefilters.indent(firstline + text, indent)
537 546
538 547 @templatefunc('get(dict, key)')
539 548 def get(context, mapping, args):
540 549 """Get an attribute/key from an object. Some keywords
541 550 are complex types. This function allows you to obtain the value of an
542 551 attribute on these types."""
543 552 if len(args) != 2:
544 553 # i18n: "get" is a keyword
545 554 raise error.ParseError(_("get() expects two arguments"))
546 555
547 556 dictarg = evalfuncarg(context, mapping, args[0])
548 557 if not util.safehasattr(dictarg, 'get'):
549 558 # i18n: "get" is a keyword
550 559 raise error.ParseError(_("get() expects a dict as first argument"))
551 560
552 561 key = evalfuncarg(context, mapping, args[1])
553 562 return dictarg.get(key)
554 563
555 564 @templatefunc('if(expr, then[, else])')
556 565 def if_(context, mapping, args):
557 566 """Conditionally execute based on the result of
558 567 an expression."""
559 568 if not (2 <= len(args) <= 3):
560 569 # i18n: "if" is a keyword
561 570 raise error.ParseError(_("if expects two or three arguments"))
562 571
563 test = evalstring(context, mapping, args[0])
572 test = evalboolean(context, mapping, args[0])
564 573 if test:
565 574 yield args[1][0](context, mapping, args[1][1])
566 575 elif len(args) == 3:
567 576 yield args[2][0](context, mapping, args[2][1])
568 577
569 578 @templatefunc('ifcontains(search, thing, then[, else])')
570 579 def ifcontains(context, mapping, args):
571 580 """Conditionally execute based
572 581 on whether the item "search" is in "thing"."""
573 582 if not (3 <= len(args) <= 4):
574 583 # i18n: "ifcontains" is a keyword
575 584 raise error.ParseError(_("ifcontains expects three or four arguments"))
576 585
577 586 item = evalstring(context, mapping, args[0])
578 587 items = evalfuncarg(context, mapping, args[1])
579 588
580 589 if item in items:
581 590 yield args[2][0](context, mapping, args[2][1])
582 591 elif len(args) == 4:
583 592 yield args[3][0](context, mapping, args[3][1])
584 593
585 594 @templatefunc('ifeq(expr1, expr2, then[, else])')
586 595 def ifeq(context, mapping, args):
587 596 """Conditionally execute based on
588 597 whether 2 items are equivalent."""
589 598 if not (3 <= len(args) <= 4):
590 599 # i18n: "ifeq" is a keyword
591 600 raise error.ParseError(_("ifeq expects three or four arguments"))
592 601
593 602 test = evalstring(context, mapping, args[0])
594 603 match = evalstring(context, mapping, args[1])
595 604 if test == match:
596 605 yield args[2][0](context, mapping, args[2][1])
597 606 elif len(args) == 4:
598 607 yield args[3][0](context, mapping, args[3][1])
599 608
600 609 @templatefunc('join(list, sep)')
601 610 def join(context, mapping, args):
602 611 """Join items in a list with a delimiter."""
603 612 if not (1 <= len(args) <= 2):
604 613 # i18n: "join" is a keyword
605 614 raise error.ParseError(_("join expects one or two arguments"))
606 615
607 616 joinset = args[0][0](context, mapping, args[0][1])
608 617 if util.safehasattr(joinset, 'itermaps'):
609 618 jf = joinset.joinfmt
610 619 joinset = [jf(x) for x in joinset.itermaps()]
611 620
612 621 joiner = " "
613 622 if len(args) > 1:
614 623 joiner = evalstring(context, mapping, args[1])
615 624
616 625 first = True
617 626 for x in joinset:
618 627 if first:
619 628 first = False
620 629 else:
621 630 yield joiner
622 631 yield x
623 632
624 633 @templatefunc('label(label, expr)')
625 634 def label(context, mapping, args):
626 635 """Apply a label to generated content. Content with
627 636 a label applied can result in additional post-processing, such as
628 637 automatic colorization."""
629 638 if len(args) != 2:
630 639 # i18n: "label" is a keyword
631 640 raise error.ParseError(_("label expects two arguments"))
632 641
633 642 ui = mapping['ui']
634 643 thing = evalstring(context, mapping, args[1])
635 644 # preserve unknown symbol as literal so effects like 'red', 'bold',
636 645 # etc. don't need to be quoted
637 646 label = evalstringliteral(context, mapping, args[0])
638 647
639 648 return ui.label(thing, label)
640 649
641 650 @templatefunc('latesttag([pattern])')
642 651 def latesttag(context, mapping, args):
643 652 """The global tags matching the given pattern on the
644 653 most recent globally tagged ancestor of this changeset."""
645 654 if len(args) > 1:
646 655 # i18n: "latesttag" is a keyword
647 656 raise error.ParseError(_("latesttag expects at most one argument"))
648 657
649 658 pattern = None
650 659 if len(args) == 1:
651 660 pattern = evalstring(context, mapping, args[0])
652 661
653 662 return templatekw.showlatesttags(pattern, **mapping)
654 663
655 664 @templatefunc('localdate(date[, tz])')
656 665 def localdate(context, mapping, args):
657 666 """Converts a date to the specified timezone.
658 667 The default is local date."""
659 668 if not (1 <= len(args) <= 2):
660 669 # i18n: "localdate" is a keyword
661 670 raise error.ParseError(_("localdate expects one or two arguments"))
662 671
663 672 date = evalfuncarg(context, mapping, args[0])
664 673 try:
665 674 date = util.parsedate(date)
666 675 except AttributeError: # not str nor date tuple
667 676 # i18n: "localdate" is a keyword
668 677 raise error.ParseError(_("localdate expects a date information"))
669 678 if len(args) >= 2:
670 679 tzoffset = None
671 680 tz = evalfuncarg(context, mapping, args[1])
672 681 if isinstance(tz, str):
673 682 tzoffset, remainder = util.parsetimezone(tz)
674 683 if remainder:
675 684 tzoffset = None
676 685 if tzoffset is None:
677 686 try:
678 687 tzoffset = int(tz)
679 688 except (TypeError, ValueError):
680 689 # i18n: "localdate" is a keyword
681 690 raise error.ParseError(_("localdate expects a timezone"))
682 691 else:
683 692 tzoffset = util.makedate()[1]
684 693 return (date[0], tzoffset)
685 694
686 695 @templatefunc('revset(query[, formatargs...])')
687 696 def revset(context, mapping, args):
688 697 """Execute a revision set query. See
689 698 :hg:`help revset`."""
690 699 if not len(args) > 0:
691 700 # i18n: "revset" is a keyword
692 701 raise error.ParseError(_("revset expects one or more arguments"))
693 702
694 703 raw = evalstring(context, mapping, args[0])
695 704 ctx = mapping['ctx']
696 705 repo = ctx.repo()
697 706
698 707 def query(expr):
699 708 m = revsetmod.match(repo.ui, expr)
700 709 return m(repo)
701 710
702 711 if len(args) > 1:
703 712 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
704 713 revs = query(revsetmod.formatspec(raw, *formatargs))
705 714 revs = list(revs)
706 715 else:
707 716 revsetcache = mapping['cache'].setdefault("revsetcache", {})
708 717 if raw in revsetcache:
709 718 revs = revsetcache[raw]
710 719 else:
711 720 revs = query(raw)
712 721 revs = list(revs)
713 722 revsetcache[raw] = revs
714 723
715 724 return templatekw.showrevslist("revision", revs, **mapping)
716 725
717 726 @templatefunc('rstdoc(text, style)')
718 727 def rstdoc(context, mapping, args):
719 728 """Format ReStructuredText."""
720 729 if len(args) != 2:
721 730 # i18n: "rstdoc" is a keyword
722 731 raise error.ParseError(_("rstdoc expects two arguments"))
723 732
724 733 text = evalstring(context, mapping, args[0])
725 734 style = evalstring(context, mapping, args[1])
726 735
727 736 return minirst.format(text, style=style, keep=['verbose'])
728 737
729 738 @templatefunc('separate(sep, args)')
730 739 def separate(context, mapping, args):
731 740 """Add a separator between non-empty arguments."""
732 741 if not args:
733 742 # i18n: "separate" is a keyword
734 743 raise error.ParseError(_("separate expects at least one argument"))
735 744
736 745 sep = evalstring(context, mapping, args[0])
737 746 first = True
738 747 for arg in args[1:]:
739 748 argstr = evalstring(context, mapping, arg)
740 749 if not argstr:
741 750 continue
742 751 if first:
743 752 first = False
744 753 else:
745 754 yield sep
746 755 yield argstr
747 756
748 757 @templatefunc('shortest(node, minlength=4)')
749 758 def shortest(context, mapping, args):
750 759 """Obtain the shortest representation of
751 760 a node."""
752 761 if not (1 <= len(args) <= 2):
753 762 # i18n: "shortest" is a keyword
754 763 raise error.ParseError(_("shortest() expects one or two arguments"))
755 764
756 765 node = evalstring(context, mapping, args[0])
757 766
758 767 minlength = 4
759 768 if len(args) > 1:
760 769 minlength = evalinteger(context, mapping, args[1],
761 770 # i18n: "shortest" is a keyword
762 771 _("shortest() expects an integer minlength"))
763 772
764 773 cl = mapping['ctx']._repo.changelog
765 774 def isvalid(test):
766 775 try:
767 776 try:
768 777 cl.index.partialmatch(test)
769 778 except AttributeError:
770 779 # Pure mercurial doesn't support partialmatch on the index.
771 780 # Fallback to the slow way.
772 781 if cl._partialmatch(test) is None:
773 782 return False
774 783
775 784 try:
776 785 i = int(test)
777 786 # if we are a pure int, then starting with zero will not be
778 787 # confused as a rev; or, obviously, if the int is larger than
779 788 # the value of the tip rev
780 789 if test[0] == '0' or i > len(cl):
781 790 return True
782 791 return False
783 792 except ValueError:
784 793 return True
785 794 except error.RevlogError:
786 795 return False
787 796
788 797 shortest = node
789 798 startlength = max(6, minlength)
790 799 length = startlength
791 800 while True:
792 801 test = node[:length]
793 802 if isvalid(test):
794 803 shortest = test
795 804 if length == minlength or length > startlength:
796 805 return shortest
797 806 length -= 1
798 807 else:
799 808 length += 1
800 809 if len(shortest) <= length:
801 810 return shortest
802 811
803 812 @templatefunc('strip(text[, chars])')
804 813 def strip(context, mapping, args):
805 814 """Strip characters from a string. By default,
806 815 strips all leading and trailing whitespace."""
807 816 if not (1 <= len(args) <= 2):
808 817 # i18n: "strip" is a keyword
809 818 raise error.ParseError(_("strip expects one or two arguments"))
810 819
811 820 text = evalstring(context, mapping, args[0])
812 821 if len(args) == 2:
813 822 chars = evalstring(context, mapping, args[1])
814 823 return text.strip(chars)
815 824 return text.strip()
816 825
817 826 @templatefunc('sub(pattern, replacement, expression)')
818 827 def sub(context, mapping, args):
819 828 """Perform text substitution
820 829 using regular expressions."""
821 830 if len(args) != 3:
822 831 # i18n: "sub" is a keyword
823 832 raise error.ParseError(_("sub expects three arguments"))
824 833
825 834 pat = evalstring(context, mapping, args[0])
826 835 rpl = evalstring(context, mapping, args[1])
827 836 src = evalstring(context, mapping, args[2])
828 837 try:
829 838 patre = re.compile(pat)
830 839 except re.error:
831 840 # i18n: "sub" is a keyword
832 841 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
833 842 try:
834 843 yield patre.sub(rpl, src)
835 844 except re.error:
836 845 # i18n: "sub" is a keyword
837 846 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
838 847
839 848 @templatefunc('startswith(pattern, text)')
840 849 def startswith(context, mapping, args):
841 850 """Returns the value from the "text" argument
842 851 if it begins with the content from the "pattern" argument."""
843 852 if len(args) != 2:
844 853 # i18n: "startswith" is a keyword
845 854 raise error.ParseError(_("startswith expects two arguments"))
846 855
847 856 patn = evalstring(context, mapping, args[0])
848 857 text = evalstring(context, mapping, args[1])
849 858 if text.startswith(patn):
850 859 return text
851 860 return ''
852 861
853 862 @templatefunc('word(number, text[, separator])')
854 863 def word(context, mapping, args):
855 864 """Return the nth word from a string."""
856 865 if not (2 <= len(args) <= 3):
857 866 # i18n: "word" is a keyword
858 867 raise error.ParseError(_("word expects two or three arguments, got %d")
859 868 % len(args))
860 869
861 870 num = evalinteger(context, mapping, args[0],
862 871 # i18n: "word" is a keyword
863 872 _("word expects an integer index"))
864 873 text = evalstring(context, mapping, args[1])
865 874 if len(args) == 3:
866 875 splitter = evalstring(context, mapping, args[2])
867 876 else:
868 877 splitter = None
869 878
870 879 tokens = text.split(splitter)
871 880 if num >= len(tokens) or num < -len(tokens):
872 881 return ''
873 882 else:
874 883 return tokens[num]
875 884
876 885 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
877 886 exprmethods = {
878 887 "integer": lambda e, c: (runinteger, e[1]),
879 888 "string": lambda e, c: (runstring, e[1]),
880 889 "symbol": lambda e, c: (runsymbol, e[1]),
881 890 "template": buildtemplate,
882 891 "group": lambda e, c: compileexp(e[1], c, exprmethods),
883 892 # ".": buildmember,
884 893 "|": buildfilter,
885 894 "%": buildmap,
886 895 "func": buildfunc,
887 896 }
888 897
889 898 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
890 899 methods = exprmethods.copy()
891 900 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
892 901
893 902 class _aliasrules(parser.basealiasrules):
894 903 """Parsing and expansion rule set of template aliases"""
895 904 _section = _('template alias')
896 905 _parse = staticmethod(_parseexpr)
897 906
898 907 @staticmethod
899 908 def _trygetfunc(tree):
900 909 """Return (name, args) if tree is func(...) or ...|filter; otherwise
901 910 None"""
902 911 if tree[0] == 'func' and tree[1][0] == 'symbol':
903 912 return tree[1][1], getlist(tree[2])
904 913 if tree[0] == '|' and tree[2][0] == 'symbol':
905 914 return tree[2][1], [tree[1]]
906 915
907 916 def expandaliases(tree, aliases):
908 917 """Return new tree of aliases are expanded"""
909 918 aliasmap = _aliasrules.buildmap(aliases)
910 919 return _aliasrules.expand(aliasmap, tree)
911 920
912 921 # template engine
913 922
914 923 stringify = templatefilters.stringify
915 924
916 925 def _flatten(thing):
917 926 '''yield a single stream from a possibly nested set of iterators'''
918 927 if isinstance(thing, str):
919 928 yield thing
920 929 elif thing is None:
921 930 pass
922 931 elif not util.safehasattr(thing, '__iter__'):
923 932 yield str(thing)
924 933 else:
925 934 for i in thing:
926 935 if isinstance(i, str):
927 936 yield i
928 937 elif i is None:
929 938 pass
930 939 elif not util.safehasattr(i, '__iter__'):
931 940 yield str(i)
932 941 else:
933 942 for j in _flatten(i):
934 943 yield j
935 944
936 945 def unquotestring(s):
937 946 '''unwrap quotes if any; otherwise returns unmodified string'''
938 947 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
939 948 return s
940 949 return s[1:-1]
941 950
942 951 class engine(object):
943 952 '''template expansion engine.
944 953
945 954 template expansion works like this. a map file contains key=value
946 955 pairs. if value is quoted, it is treated as string. otherwise, it
947 956 is treated as name of template file.
948 957
949 958 templater is asked to expand a key in map. it looks up key, and
950 959 looks for strings like this: {foo}. it expands {foo} by looking up
951 960 foo in map, and substituting it. expansion is recursive: it stops
952 961 when there is no more {foo} to replace.
953 962
954 963 expansion also allows formatting and filtering.
955 964
956 965 format uses key to expand each item in list. syntax is
957 966 {key%format}.
958 967
959 968 filter uses function to transform value. syntax is
960 969 {key|filter1|filter2|...}.'''
961 970
962 971 def __init__(self, loader, filters=None, defaults=None, aliases=()):
963 972 self._loader = loader
964 973 if filters is None:
965 974 filters = {}
966 975 self._filters = filters
967 976 if defaults is None:
968 977 defaults = {}
969 978 self._defaults = defaults
970 979 self._aliasmap = _aliasrules.buildmap(aliases)
971 980 self._cache = {} # key: (func, data)
972 981
973 982 def _load(self, t):
974 983 '''load, parse, and cache a template'''
975 984 if t not in self._cache:
976 985 # put poison to cut recursion while compiling 't'
977 986 self._cache[t] = (_runrecursivesymbol, t)
978 987 try:
979 988 x = parse(self._loader(t))
980 989 if self._aliasmap:
981 990 x = _aliasrules.expand(self._aliasmap, x)
982 991 self._cache[t] = compileexp(x, self, methods)
983 992 except: # re-raises
984 993 del self._cache[t]
985 994 raise
986 995 return self._cache[t]
987 996
988 997 def process(self, t, mapping):
989 998 '''Perform expansion. t is name of map element to expand.
990 999 mapping contains added elements for use during expansion. Is a
991 1000 generator.'''
992 1001 func, data = self._load(t)
993 1002 return _flatten(func(self, mapping, data))
994 1003
995 1004 engines = {'default': engine}
996 1005
997 1006 def stylelist():
998 1007 paths = templatepaths()
999 1008 if not paths:
1000 1009 return _('no templates found, try `hg debuginstall` for more info')
1001 1010 dirlist = os.listdir(paths[0])
1002 1011 stylelist = []
1003 1012 for file in dirlist:
1004 1013 split = file.split(".")
1005 1014 if split[-1] in ('orig', 'rej'):
1006 1015 continue
1007 1016 if split[0] == "map-cmdline":
1008 1017 stylelist.append(split[1])
1009 1018 return ", ".join(sorted(stylelist))
1010 1019
1011 1020 def _readmapfile(mapfile):
1012 1021 """Load template elements from the given map file"""
1013 1022 if not os.path.exists(mapfile):
1014 1023 raise error.Abort(_("style '%s' not found") % mapfile,
1015 1024 hint=_("available styles: %s") % stylelist())
1016 1025
1017 1026 base = os.path.dirname(mapfile)
1018 1027 conf = config.config(includepaths=templatepaths())
1019 1028 conf.read(mapfile)
1020 1029
1021 1030 cache = {}
1022 1031 tmap = {}
1023 1032 for key, val in conf[''].items():
1024 1033 if not val:
1025 1034 raise error.ParseError(_('missing value'), conf.source('', key))
1026 1035 if val[0] in "'\"":
1027 1036 if val[0] != val[-1]:
1028 1037 raise error.ParseError(_('unmatched quotes'),
1029 1038 conf.source('', key))
1030 1039 cache[key] = unquotestring(val)
1031 1040 elif key == "__base__":
1032 1041 # treat as a pointer to a base class for this style
1033 1042 path = util.normpath(os.path.join(base, val))
1034 1043 bcache, btmap = _readmapfile(path)
1035 1044 for k in bcache:
1036 1045 if k not in cache:
1037 1046 cache[k] = bcache[k]
1038 1047 for k in btmap:
1039 1048 if k not in tmap:
1040 1049 tmap[k] = btmap[k]
1041 1050 else:
1042 1051 val = 'default', val
1043 1052 if ':' in val[1]:
1044 1053 val = val[1].split(':', 1)
1045 1054 tmap[key] = val[0], os.path.join(base, val[1])
1046 1055 return cache, tmap
1047 1056
1048 1057 class TemplateNotFound(error.Abort):
1049 1058 pass
1050 1059
1051 1060 class templater(object):
1052 1061
1053 1062 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1054 1063 minchunk=1024, maxchunk=65536):
1055 1064 '''set up template engine.
1056 1065 filters is dict of functions. each transforms a value into another.
1057 1066 defaults is dict of default map definitions.
1058 1067 aliases is list of alias (name, replacement) pairs.
1059 1068 '''
1060 1069 if filters is None:
1061 1070 filters = {}
1062 1071 if defaults is None:
1063 1072 defaults = {}
1064 1073 if cache is None:
1065 1074 cache = {}
1066 1075 self.cache = cache.copy()
1067 1076 self.map = {}
1068 1077 self.filters = templatefilters.filters.copy()
1069 1078 self.filters.update(filters)
1070 1079 self.defaults = defaults
1071 1080 self._aliases = aliases
1072 1081 self.minchunk, self.maxchunk = minchunk, maxchunk
1073 1082 self.ecache = {}
1074 1083
1075 1084 @classmethod
1076 1085 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1077 1086 minchunk=1024, maxchunk=65536):
1078 1087 """Create templater from the specified map file"""
1079 1088 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1080 1089 cache, tmap = _readmapfile(mapfile)
1081 1090 t.cache.update(cache)
1082 1091 t.map = tmap
1083 1092 return t
1084 1093
1085 1094 def __contains__(self, key):
1086 1095 return key in self.cache or key in self.map
1087 1096
1088 1097 def load(self, t):
1089 1098 '''Get the template for the given template name. Use a local cache.'''
1090 1099 if t not in self.cache:
1091 1100 try:
1092 1101 self.cache[t] = util.readfile(self.map[t][1])
1093 1102 except KeyError as inst:
1094 1103 raise TemplateNotFound(_('"%s" not in template map') %
1095 1104 inst.args[0])
1096 1105 except IOError as inst:
1097 1106 raise IOError(inst.args[0], _('template file %s: %s') %
1098 1107 (self.map[t][1], inst.args[1]))
1099 1108 return self.cache[t]
1100 1109
1101 1110 def __call__(self, t, **mapping):
1102 1111 ttype = t in self.map and self.map[t][0] or 'default'
1103 1112 if ttype not in self.ecache:
1104 1113 try:
1105 1114 ecls = engines[ttype]
1106 1115 except KeyError:
1107 1116 raise error.Abort(_('invalid template engine: %s') % ttype)
1108 1117 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1109 1118 self._aliases)
1110 1119 proc = self.ecache[ttype]
1111 1120
1112 1121 stream = proc.process(t, mapping)
1113 1122 if self.minchunk:
1114 1123 stream = util.increasingchunks(stream, min=self.minchunk,
1115 1124 max=self.maxchunk)
1116 1125 return stream
1117 1126
1118 1127 def templatepaths():
1119 1128 '''return locations used for template files.'''
1120 1129 pathsrel = ['templates']
1121 1130 paths = [os.path.normpath(os.path.join(util.datapath, f))
1122 1131 for f in pathsrel]
1123 1132 return [p for p in paths if os.path.isdir(p)]
1124 1133
1125 1134 def templatepath(name):
1126 1135 '''return location of template file. returns None if not found.'''
1127 1136 for p in templatepaths():
1128 1137 f = os.path.join(p, name)
1129 1138 if os.path.exists(f):
1130 1139 return f
1131 1140 return None
1132 1141
1133 1142 def stylemap(styles, paths=None):
1134 1143 """Return path to mapfile for a given style.
1135 1144
1136 1145 Searches mapfile in the following locations:
1137 1146 1. templatepath/style/map
1138 1147 2. templatepath/map-style
1139 1148 3. templatepath/map
1140 1149 """
1141 1150
1142 1151 if paths is None:
1143 1152 paths = templatepaths()
1144 1153 elif isinstance(paths, str):
1145 1154 paths = [paths]
1146 1155
1147 1156 if isinstance(styles, str):
1148 1157 styles = [styles]
1149 1158
1150 1159 for style in styles:
1151 1160 # only plain name is allowed to honor template paths
1152 1161 if (not style
1153 1162 or style in (os.curdir, os.pardir)
1154 1163 or os.sep in style
1155 1164 or os.altsep and os.altsep in style):
1156 1165 continue
1157 1166 locations = [os.path.join(style, 'map'), 'map-' + style]
1158 1167 locations.append('map')
1159 1168
1160 1169 for path in paths:
1161 1170 for location in locations:
1162 1171 mapfile = os.path.join(path, location)
1163 1172 if os.path.isfile(mapfile):
1164 1173 return style, mapfile
1165 1174
1166 1175 raise RuntimeError("No hgweb templates found in %r" % paths)
1167 1176
1168 1177 def loadfunction(ui, extname, registrarobj):
1169 1178 """Load template function from specified registrarobj
1170 1179 """
1171 1180 for name, func in registrarobj._table.iteritems():
1172 1181 funcs[name] = func
1173 1182
1174 1183 # tell hggettext to extract docstrings from these functions:
1175 1184 i18nfunctions = funcs.values()
@@ -1,714 +1,716 b''
1 1 $ hg init a
2 2 $ cd a
3 3 $ echo 'root' >root
4 4 $ hg add root
5 5 $ hg commit -d '0 0' -m "Adding root node"
6 6
7 7 $ echo 'a' >a
8 8 $ hg add a
9 9 $ hg branch a
10 10 marked working directory as branch a
11 11 (branches are permanent and global, did you want a bookmark?)
12 12 $ hg commit -d '1 0' -m "Adding a branch"
13 13
14 14 $ hg branch q
15 15 marked working directory as branch q
16 16 $ echo 'aa' >a
17 17 $ hg branch -C
18 18 reset working directory to branch a
19 19 $ hg commit -d '2 0' -m "Adding to a branch"
20 20
21 21 $ hg update -C 0
22 22 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
23 23 $ echo 'b' >b
24 24 $ hg add b
25 25 $ hg branch b
26 26 marked working directory as branch b
27 27 $ hg commit -d '2 0' -m "Adding b branch"
28 28
29 29 $ echo 'bh1' >bh1
30 30 $ hg add bh1
31 31 $ hg commit -d '3 0' -m "Adding b branch head 1"
32 32
33 33 $ hg update -C 2
34 34 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
35 35 $ echo 'bh2' >bh2
36 36 $ hg add bh2
37 37 $ hg commit -d '4 0' -m "Adding b branch head 2"
38 38
39 39 $ echo 'c' >c
40 40 $ hg add c
41 41 $ hg branch c
42 42 marked working directory as branch c
43 43 $ hg commit -d '5 0' -m "Adding c branch"
44 44
45 45 reserved names
46 46
47 47 $ hg branch tip
48 48 abort: the name 'tip' is reserved
49 49 [255]
50 50 $ hg branch null
51 51 abort: the name 'null' is reserved
52 52 [255]
53 53 $ hg branch .
54 54 abort: the name '.' is reserved
55 55 [255]
56 56
57 57 invalid characters
58 58
59 59 $ hg branch 'foo:bar'
60 60 abort: ':' cannot be used in a name
61 61 [255]
62 62
63 63 $ hg branch 'foo
64 64 > bar'
65 65 abort: '\n' cannot be used in a name
66 66 [255]
67 67
68 68 trailing or leading spaces should be stripped before testing duplicates
69 69
70 70 $ hg branch 'b '
71 71 abort: a branch of the same name already exists
72 72 (use 'hg update' to switch to it)
73 73 [255]
74 74
75 75 $ hg branch ' b'
76 76 abort: a branch of the same name already exists
77 77 (use 'hg update' to switch to it)
78 78 [255]
79 79
80 80 verify update will accept invalid legacy branch names
81 81
82 82 $ hg init test-invalid-branch-name
83 83 $ cd test-invalid-branch-name
84 84 $ hg pull -u "$TESTDIR"/bundles/test-invalid-branch-name.hg
85 85 pulling from *test-invalid-branch-name.hg (glob)
86 86 requesting all changes
87 87 adding changesets
88 88 adding manifests
89 89 adding file changes
90 90 added 3 changesets with 3 changes to 2 files
91 91 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
92 92
93 93 $ hg update '"colon:test"'
94 94 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
95 95 $ cd ..
96 96
97 97 $ echo 'd' >d
98 98 $ hg add d
99 99 $ hg branch 'a branch name much longer than the default justification used by branches'
100 100 marked working directory as branch a branch name much longer than the default justification used by branches
101 101 $ hg commit -d '6 0' -m "Adding d branch"
102 102
103 103 $ hg branches
104 104 a branch name much longer than the default justification used by branches 7:10ff5895aa57
105 105 b 4:aee39cd168d0
106 106 c 6:589736a22561 (inactive)
107 107 a 5:d8cbc61dbaa6 (inactive)
108 108 default 0:19709c5a4e75 (inactive)
109 109
110 110 -------
111 111
112 112 $ hg branches -a
113 113 a branch name much longer than the default justification used by branches 7:10ff5895aa57
114 114 b 4:aee39cd168d0
115 115
116 116 --- Branch a
117 117
118 118 $ hg log -b a
119 119 changeset: 5:d8cbc61dbaa6
120 120 branch: a
121 121 parent: 2:881fe2b92ad0
122 122 user: test
123 123 date: Thu Jan 01 00:00:04 1970 +0000
124 124 summary: Adding b branch head 2
125 125
126 126 changeset: 2:881fe2b92ad0
127 127 branch: a
128 128 user: test
129 129 date: Thu Jan 01 00:00:02 1970 +0000
130 130 summary: Adding to a branch
131 131
132 132 changeset: 1:dd6b440dd85a
133 133 branch: a
134 134 user: test
135 135 date: Thu Jan 01 00:00:01 1970 +0000
136 136 summary: Adding a branch
137 137
138 138
139 139 ---- Branch b
140 140
141 141 $ hg log -b b
142 142 changeset: 4:aee39cd168d0
143 143 branch: b
144 144 user: test
145 145 date: Thu Jan 01 00:00:03 1970 +0000
146 146 summary: Adding b branch head 1
147 147
148 148 changeset: 3:ac22033332d1
149 149 branch: b
150 150 parent: 0:19709c5a4e75
151 151 user: test
152 152 date: Thu Jan 01 00:00:02 1970 +0000
153 153 summary: Adding b branch
154 154
155 155
156 156 ---- going to test branch closing
157 157
158 158 $ hg branches
159 159 a branch name much longer than the default justification used by branches 7:10ff5895aa57
160 160 b 4:aee39cd168d0
161 161 c 6:589736a22561 (inactive)
162 162 a 5:d8cbc61dbaa6 (inactive)
163 163 default 0:19709c5a4e75 (inactive)
164 164 $ hg up -C b
165 165 2 files updated, 0 files merged, 4 files removed, 0 files unresolved
166 166 $ echo 'xxx1' >> b
167 167 $ hg commit -d '7 0' -m 'adding cset to branch b'
168 168 $ hg up -C aee39cd168d0
169 169 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
170 170 $ echo 'xxx2' >> b
171 171 $ hg commit -d '8 0' -m 'adding head to branch b'
172 172 created new head
173 173 $ echo 'xxx3' >> b
174 174 $ hg commit -d '9 0' -m 'adding another cset to branch b'
175 175 $ hg branches
176 176 b 10:bfbe841b666e
177 177 a branch name much longer than the default justification used by branches 7:10ff5895aa57
178 178 c 6:589736a22561 (inactive)
179 179 a 5:d8cbc61dbaa6 (inactive)
180 180 default 0:19709c5a4e75 (inactive)
181 181 $ hg heads --closed
182 182 changeset: 10:bfbe841b666e
183 183 branch: b
184 184 tag: tip
185 185 user: test
186 186 date: Thu Jan 01 00:00:09 1970 +0000
187 187 summary: adding another cset to branch b
188 188
189 189 changeset: 8:eebb944467c9
190 190 branch: b
191 191 parent: 4:aee39cd168d0
192 192 user: test
193 193 date: Thu Jan 01 00:00:07 1970 +0000
194 194 summary: adding cset to branch b
195 195
196 196 changeset: 7:10ff5895aa57
197 197 branch: a branch name much longer than the default justification used by branches
198 198 user: test
199 199 date: Thu Jan 01 00:00:06 1970 +0000
200 200 summary: Adding d branch
201 201
202 202 changeset: 6:589736a22561
203 203 branch: c
204 204 user: test
205 205 date: Thu Jan 01 00:00:05 1970 +0000
206 206 summary: Adding c branch
207 207
208 208 changeset: 5:d8cbc61dbaa6
209 209 branch: a
210 210 parent: 2:881fe2b92ad0
211 211 user: test
212 212 date: Thu Jan 01 00:00:04 1970 +0000
213 213 summary: Adding b branch head 2
214 214
215 215 changeset: 0:19709c5a4e75
216 216 user: test
217 217 date: Thu Jan 01 00:00:00 1970 +0000
218 218 summary: Adding root node
219 219
220 220 $ hg heads
221 221 changeset: 10:bfbe841b666e
222 222 branch: b
223 223 tag: tip
224 224 user: test
225 225 date: Thu Jan 01 00:00:09 1970 +0000
226 226 summary: adding another cset to branch b
227 227
228 228 changeset: 8:eebb944467c9
229 229 branch: b
230 230 parent: 4:aee39cd168d0
231 231 user: test
232 232 date: Thu Jan 01 00:00:07 1970 +0000
233 233 summary: adding cset to branch b
234 234
235 235 changeset: 7:10ff5895aa57
236 236 branch: a branch name much longer than the default justification used by branches
237 237 user: test
238 238 date: Thu Jan 01 00:00:06 1970 +0000
239 239 summary: Adding d branch
240 240
241 241 changeset: 6:589736a22561
242 242 branch: c
243 243 user: test
244 244 date: Thu Jan 01 00:00:05 1970 +0000
245 245 summary: Adding c branch
246 246
247 247 changeset: 5:d8cbc61dbaa6
248 248 branch: a
249 249 parent: 2:881fe2b92ad0
250 250 user: test
251 251 date: Thu Jan 01 00:00:04 1970 +0000
252 252 summary: Adding b branch head 2
253 253
254 254 changeset: 0:19709c5a4e75
255 255 user: test
256 256 date: Thu Jan 01 00:00:00 1970 +0000
257 257 summary: Adding root node
258 258
259 259 $ hg commit -d '9 0' --close-branch -m 'prune bad branch'
260 260 $ hg branches -a
261 261 b 8:eebb944467c9
262 262 a branch name much longer than the default justification used by branches 7:10ff5895aa57
263 263 $ hg up -C b
264 264 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
265 265 $ hg commit -d '9 0' --close-branch -m 'close this part branch too'
266 266 $ hg commit -d '9 0' --close-branch -m 're-closing this branch'
267 267 abort: can only close branch heads
268 268 [255]
269 269
270 270 $ hg log -r tip --debug
271 271 changeset: 12:e3d49c0575d8fc2cb1cd6859c747c14f5f6d499f
272 272 branch: b
273 273 tag: tip
274 274 phase: draft
275 275 parent: 8:eebb944467c9fb9651ed232aeaf31b3c0a7fc6c1
276 276 parent: -1:0000000000000000000000000000000000000000
277 277 manifest: 8:6f9ed32d2b310e391a4f107d5f0f071df785bfee
278 278 user: test
279 279 date: Thu Jan 01 00:00:09 1970 +0000
280 280 extra: branch=b
281 281 extra: close=1
282 282 description:
283 283 close this part branch too
284 284
285 285
286 286 --- b branch should be inactive
287 287
288 288 $ hg branches
289 289 a branch name much longer than the default justification used by branches 7:10ff5895aa57
290 290 c 6:589736a22561 (inactive)
291 291 a 5:d8cbc61dbaa6 (inactive)
292 292 default 0:19709c5a4e75 (inactive)
293 293 $ hg branches -c
294 294 a branch name much longer than the default justification used by branches 7:10ff5895aa57
295 295 b 12:e3d49c0575d8 (closed)
296 296 c 6:589736a22561 (inactive)
297 297 a 5:d8cbc61dbaa6 (inactive)
298 298 default 0:19709c5a4e75 (inactive)
299 299 $ hg branches -a
300 300 a branch name much longer than the default justification used by branches 7:10ff5895aa57
301 301 $ hg branches -q
302 302 a branch name much longer than the default justification used by branches
303 303 c
304 304 a
305 305 default
306 306 $ hg heads b
307 307 no open branch heads found on branches b
308 308 [1]
309 309 $ hg heads --closed b
310 310 changeset: 12:e3d49c0575d8
311 311 branch: b
312 312 tag: tip
313 313 parent: 8:eebb944467c9
314 314 user: test
315 315 date: Thu Jan 01 00:00:09 1970 +0000
316 316 summary: close this part branch too
317 317
318 318 changeset: 11:d3f163457ebf
319 319 branch: b
320 320 user: test
321 321 date: Thu Jan 01 00:00:09 1970 +0000
322 322 summary: prune bad branch
323 323
324 324 $ echo 'xxx4' >> b
325 325 $ hg commit -d '9 0' -m 'reopen branch with a change'
326 326 reopening closed branch head 12
327 327
328 328 --- branch b is back in action
329 329
330 330 $ hg branches -a
331 331 b 13:e23b5505d1ad
332 332 a branch name much longer than the default justification used by branches 7:10ff5895aa57
333 333
334 334 ---- test heads listings
335 335
336 336 $ hg heads
337 337 changeset: 13:e23b5505d1ad
338 338 branch: b
339 339 tag: tip
340 340 user: test
341 341 date: Thu Jan 01 00:00:09 1970 +0000
342 342 summary: reopen branch with a change
343 343
344 344 changeset: 7:10ff5895aa57
345 345 branch: a branch name much longer than the default justification used by branches
346 346 user: test
347 347 date: Thu Jan 01 00:00:06 1970 +0000
348 348 summary: Adding d branch
349 349
350 350 changeset: 6:589736a22561
351 351 branch: c
352 352 user: test
353 353 date: Thu Jan 01 00:00:05 1970 +0000
354 354 summary: Adding c branch
355 355
356 356 changeset: 5:d8cbc61dbaa6
357 357 branch: a
358 358 parent: 2:881fe2b92ad0
359 359 user: test
360 360 date: Thu Jan 01 00:00:04 1970 +0000
361 361 summary: Adding b branch head 2
362 362
363 363 changeset: 0:19709c5a4e75
364 364 user: test
365 365 date: Thu Jan 01 00:00:00 1970 +0000
366 366 summary: Adding root node
367 367
368 368
369 369 branch default
370 370
371 371 $ hg heads default
372 372 changeset: 0:19709c5a4e75
373 373 user: test
374 374 date: Thu Jan 01 00:00:00 1970 +0000
375 375 summary: Adding root node
376 376
377 377
378 378 branch a
379 379
380 380 $ hg heads a
381 381 changeset: 5:d8cbc61dbaa6
382 382 branch: a
383 383 parent: 2:881fe2b92ad0
384 384 user: test
385 385 date: Thu Jan 01 00:00:04 1970 +0000
386 386 summary: Adding b branch head 2
387 387
388 388 $ hg heads --active a
389 389 no open branch heads found on branches a
390 390 [1]
391 391
392 392 branch b
393 393
394 394 $ hg heads b
395 395 changeset: 13:e23b5505d1ad
396 396 branch: b
397 397 tag: tip
398 398 user: test
399 399 date: Thu Jan 01 00:00:09 1970 +0000
400 400 summary: reopen branch with a change
401 401
402 402 $ hg heads --closed b
403 403 changeset: 13:e23b5505d1ad
404 404 branch: b
405 405 tag: tip
406 406 user: test
407 407 date: Thu Jan 01 00:00:09 1970 +0000
408 408 summary: reopen branch with a change
409 409
410 410 changeset: 11:d3f163457ebf
411 411 branch: b
412 412 user: test
413 413 date: Thu Jan 01 00:00:09 1970 +0000
414 414 summary: prune bad branch
415 415
416 416 default branch colors:
417 417
418 418 $ cat <<EOF >> $HGRCPATH
419 419 > [extensions]
420 420 > color =
421 421 > [color]
422 422 > mode = ansi
423 423 > EOF
424 424
425 425 $ hg up -C c
426 426 3 files updated, 0 files merged, 2 files removed, 0 files unresolved
427 427 $ hg commit -d '9 0' --close-branch -m 'reclosing this branch'
428 428 $ hg up -C b
429 429 2 files updated, 0 files merged, 3 files removed, 0 files unresolved
430 430 $ hg branches --color=always
431 431 \x1b[0;32mb\x1b[0m\x1b[0;33m 13:e23b5505d1ad\x1b[0m (esc)
432 432 \x1b[0;0ma branch name much longer than the default justification used by branches\x1b[0m\x1b[0;33m 7:10ff5895aa57\x1b[0m (esc)
433 433 \x1b[0;0ma\x1b[0m\x1b[0;33m 5:d8cbc61dbaa6\x1b[0m (inactive) (esc)
434 434 \x1b[0;0mdefault\x1b[0m\x1b[0;33m 0:19709c5a4e75\x1b[0m (inactive) (esc)
435 435
436 436 default closed branch color:
437 437
438 438 $ hg branches --color=always --closed
439 439 \x1b[0;32mb\x1b[0m\x1b[0;33m 13:e23b5505d1ad\x1b[0m (esc)
440 440 \x1b[0;0ma branch name much longer than the default justification used by branches\x1b[0m\x1b[0;33m 7:10ff5895aa57\x1b[0m (esc)
441 441 \x1b[0;30;1mc\x1b[0m\x1b[0;33m 14:f894c25619d3\x1b[0m (closed) (esc)
442 442 \x1b[0;0ma\x1b[0m\x1b[0;33m 5:d8cbc61dbaa6\x1b[0m (inactive) (esc)
443 443 \x1b[0;0mdefault\x1b[0m\x1b[0;33m 0:19709c5a4e75\x1b[0m (inactive) (esc)
444 444
445 445 $ cat <<EOF >> $HGRCPATH
446 446 > [extensions]
447 447 > color =
448 448 > [color]
449 449 > branches.active = green
450 450 > branches.closed = blue
451 451 > branches.current = red
452 452 > branches.inactive = magenta
453 453 > log.changeset = cyan
454 454 > EOF
455 455
456 456 custom branch colors:
457 457
458 458 $ hg branches --color=always
459 459 \x1b[0;31mb\x1b[0m\x1b[0;36m 13:e23b5505d1ad\x1b[0m (esc)
460 460 \x1b[0;32ma branch name much longer than the default justification used by branches\x1b[0m\x1b[0;36m 7:10ff5895aa57\x1b[0m (esc)
461 461 \x1b[0;35ma\x1b[0m\x1b[0;36m 5:d8cbc61dbaa6\x1b[0m (inactive) (esc)
462 462 \x1b[0;35mdefault\x1b[0m\x1b[0;36m 0:19709c5a4e75\x1b[0m (inactive) (esc)
463 463
464 464 custom closed branch color:
465 465
466 466 $ hg branches --color=always --closed
467 467 \x1b[0;31mb\x1b[0m\x1b[0;36m 13:e23b5505d1ad\x1b[0m (esc)
468 468 \x1b[0;32ma branch name much longer than the default justification used by branches\x1b[0m\x1b[0;36m 7:10ff5895aa57\x1b[0m (esc)
469 469 \x1b[0;34mc\x1b[0m\x1b[0;36m 14:f894c25619d3\x1b[0m (closed) (esc)
470 470 \x1b[0;35ma\x1b[0m\x1b[0;36m 5:d8cbc61dbaa6\x1b[0m (inactive) (esc)
471 471 \x1b[0;35mdefault\x1b[0m\x1b[0;36m 0:19709c5a4e75\x1b[0m (inactive) (esc)
472 472
473 473 template output:
474 474
475 475 $ hg branches -Tjson --closed
476 476 [
477 477 {
478 478 "active": true,
479 479 "branch": "b",
480 480 "closed": false,
481 481 "current": true,
482 482 "node": "e23b5505d1ad24aab6f84fd8c7cb8cd8e5e93be0",
483 483 "rev": 13
484 484 },
485 485 {
486 486 "active": true,
487 487 "branch": "a branch name much longer than the default justification used by branches",
488 488 "closed": false,
489 489 "current": false,
490 490 "node": "10ff5895aa5793bd378da574af8cec8ea408d831",
491 491 "rev": 7
492 492 },
493 493 {
494 494 "active": false,
495 495 "branch": "c",
496 496 "closed": true,
497 497 "current": false,
498 498 "node": "f894c25619d3f1484639d81be950e0a07bc6f1f6",
499 499 "rev": 14
500 500 },
501 501 {
502 502 "active": false,
503 503 "branch": "a",
504 504 "closed": false,
505 505 "current": false,
506 506 "node": "d8cbc61dbaa6dc817175d1e301eecb863f280832",
507 507 "rev": 5
508 508 },
509 509 {
510 510 "active": false,
511 511 "branch": "default",
512 512 "closed": false,
513 513 "current": false,
514 514 "node": "19709c5a4e75bf938f8e349aff97438539bb729e",
515 515 "rev": 0
516 516 }
517 517 ]
518 518
519 $ hg branches --closed -T '{if(closed, "{branch}\n")}'
520 c
519 521
520 522 Tests of revision branch name caching
521 523
522 524 We rev branch cache is updated automatically. In these tests we use a trick to
523 525 trigger rebuilds. We remove the branch head cache and run 'hg head' to cause a
524 526 rebuild that also will populate the rev branch cache.
525 527
526 528 revision branch cache is created when building the branch head cache
527 529 $ rm -rf .hg/cache; hg head a -T '{rev}\n'
528 530 5
529 531 $ f --hexdump --size .hg/cache/rbc-*
530 532 .hg/cache/rbc-names-v1: size=87
531 533 0000: 64 65 66 61 75 6c 74 00 61 00 62 00 63 00 61 20 |default.a.b.c.a |
532 534 0010: 62 72 61 6e 63 68 20 6e 61 6d 65 20 6d 75 63 68 |branch name much|
533 535 0020: 20 6c 6f 6e 67 65 72 20 74 68 61 6e 20 74 68 65 | longer than the|
534 536 0030: 20 64 65 66 61 75 6c 74 20 6a 75 73 74 69 66 69 | default justifi|
535 537 0040: 63 61 74 69 6f 6e 20 75 73 65 64 20 62 79 20 62 |cation used by b|
536 538 0050: 72 61 6e 63 68 65 73 |ranches|
537 539 .hg/cache/rbc-revs-v1: size=120
538 540 0000: 19 70 9c 5a 00 00 00 00 dd 6b 44 0d 00 00 00 01 |.p.Z.....kD.....|
539 541 0010: 88 1f e2 b9 00 00 00 01 ac 22 03 33 00 00 00 02 |.........".3....|
540 542 0020: ae e3 9c d1 00 00 00 02 d8 cb c6 1d 00 00 00 01 |................|
541 543 0030: 58 97 36 a2 00 00 00 03 10 ff 58 95 00 00 00 04 |X.6.......X.....|
542 544 0040: ee bb 94 44 00 00 00 02 5f 40 61 bb 00 00 00 02 |...D...._@a.....|
543 545 0050: bf be 84 1b 00 00 00 02 d3 f1 63 45 80 00 00 02 |..........cE....|
544 546 0060: e3 d4 9c 05 80 00 00 02 e2 3b 55 05 00 00 00 02 |.........;U.....|
545 547 0070: f8 94 c2 56 80 00 00 03 |...V....|
546 548
547 549 no errors when revbranchcache is not writable
548 550
549 551 $ echo >> .hg/cache/rbc-revs-v1
550 552 $ mv .hg/cache/rbc-revs-v1 .hg/cache/rbc-revs-v1_
551 553 $ mkdir .hg/cache/rbc-revs-v1
552 554 $ rm -f .hg/cache/branch* && hg head a -T '{rev}\n'
553 555 5
554 556 $ rmdir .hg/cache/rbc-revs-v1
555 557 $ mv .hg/cache/rbc-revs-v1_ .hg/cache/rbc-revs-v1
556 558
557 559 no errors when wlock cannot be acquired
558 560
559 561 #if unix-permissions
560 562 $ mv .hg/cache/rbc-revs-v1 .hg/cache/rbc-revs-v1_
561 563 $ rm -f .hg/cache/branch*
562 564 $ chmod 555 .hg
563 565 $ hg head a -T '{rev}\n'
564 566 5
565 567 $ chmod 755 .hg
566 568 $ mv .hg/cache/rbc-revs-v1_ .hg/cache/rbc-revs-v1
567 569 #endif
568 570
569 571 recovery from invalid cache revs file with trailing data
570 572 $ echo >> .hg/cache/rbc-revs-v1
571 573 $ rm -f .hg/cache/branch* && hg head a -T '{rev}\n' --debug
572 574 5
573 575 truncating cache/rbc-revs-v1 to 120
574 576 $ f --size .hg/cache/rbc-revs*
575 577 .hg/cache/rbc-revs-v1: size=120
576 578 recovery from invalid cache file with partial last record
577 579 $ mv .hg/cache/rbc-revs-v1 .
578 580 $ f -qDB 119 rbc-revs-v1 > .hg/cache/rbc-revs-v1
579 581 $ f --size .hg/cache/rbc-revs*
580 582 .hg/cache/rbc-revs-v1: size=119
581 583 $ rm -f .hg/cache/branch* && hg head a -T '{rev}\n' --debug
582 584 5
583 585 truncating cache/rbc-revs-v1 to 112
584 586 $ f --size .hg/cache/rbc-revs*
585 587 .hg/cache/rbc-revs-v1: size=120
586 588 recovery from invalid cache file with missing record - no truncation
587 589 $ mv .hg/cache/rbc-revs-v1 .
588 590 $ f -qDB 112 rbc-revs-v1 > .hg/cache/rbc-revs-v1
589 591 $ rm -f .hg/cache/branch* && hg head a -T '{rev}\n' --debug
590 592 5
591 593 $ f --size .hg/cache/rbc-revs*
592 594 .hg/cache/rbc-revs-v1: size=120
593 595 recovery from invalid cache file with some bad records
594 596 $ mv .hg/cache/rbc-revs-v1 .
595 597 $ f -qDB 8 rbc-revs-v1 > .hg/cache/rbc-revs-v1
596 598 $ f --size .hg/cache/rbc-revs*
597 599 .hg/cache/rbc-revs-v1: size=8
598 600 $ f -qDB 112 rbc-revs-v1 >> .hg/cache/rbc-revs-v1
599 601 $ f --size .hg/cache/rbc-revs*
600 602 .hg/cache/rbc-revs-v1: size=120
601 603 $ hg log -r 'branch(.)' -T '{rev} ' --debug
602 604 history modification detected - truncating revision branch cache to revision 13
603 605 history modification detected - truncating revision branch cache to revision 1
604 606 3 4 8 9 10 11 12 13 truncating cache/rbc-revs-v1 to 8
605 607 $ rm -f .hg/cache/branch* && hg head a -T '{rev}\n' --debug
606 608 5
607 609 truncating cache/rbc-revs-v1 to 104
608 610 $ f --size --hexdump --bytes=16 .hg/cache/rbc-revs*
609 611 .hg/cache/rbc-revs-v1: size=120
610 612 0000: 19 70 9c 5a 00 00 00 00 dd 6b 44 0d 00 00 00 01 |.p.Z.....kD.....|
611 613 cache is updated when committing
612 614 $ hg branch i-will-regret-this
613 615 marked working directory as branch i-will-regret-this
614 616 $ hg ci -m regrets
615 617 $ f --size .hg/cache/rbc-*
616 618 .hg/cache/rbc-names-v1: size=106
617 619 .hg/cache/rbc-revs-v1: size=128
618 620 update after rollback - the cache will be correct but rbc-names will will still
619 621 contain the branch name even though it no longer is used
620 622 $ hg up -qr '.^'
621 623 $ hg rollback -qf
622 624 $ f --size --hexdump .hg/cache/rbc-*
623 625 .hg/cache/rbc-names-v1: size=106
624 626 0000: 64 65 66 61 75 6c 74 00 61 00 62 00 63 00 61 20 |default.a.b.c.a |
625 627 0010: 62 72 61 6e 63 68 20 6e 61 6d 65 20 6d 75 63 68 |branch name much|
626 628 0020: 20 6c 6f 6e 67 65 72 20 74 68 61 6e 20 74 68 65 | longer than the|
627 629 0030: 20 64 65 66 61 75 6c 74 20 6a 75 73 74 69 66 69 | default justifi|
628 630 0040: 63 61 74 69 6f 6e 20 75 73 65 64 20 62 79 20 62 |cation used by b|
629 631 0050: 72 61 6e 63 68 65 73 00 69 2d 77 69 6c 6c 2d 72 |ranches.i-will-r|
630 632 0060: 65 67 72 65 74 2d 74 68 69 73 |egret-this|
631 633 .hg/cache/rbc-revs-v1: size=120
632 634 0000: 19 70 9c 5a 00 00 00 00 dd 6b 44 0d 00 00 00 01 |.p.Z.....kD.....|
633 635 0010: 88 1f e2 b9 00 00 00 01 ac 22 03 33 00 00 00 02 |.........".3....|
634 636 0020: ae e3 9c d1 00 00 00 02 d8 cb c6 1d 00 00 00 01 |................|
635 637 0030: 58 97 36 a2 00 00 00 03 10 ff 58 95 00 00 00 04 |X.6.......X.....|
636 638 0040: ee bb 94 44 00 00 00 02 5f 40 61 bb 00 00 00 02 |...D...._@a.....|
637 639 0050: bf be 84 1b 00 00 00 02 d3 f1 63 45 80 00 00 02 |..........cE....|
638 640 0060: e3 d4 9c 05 80 00 00 02 e2 3b 55 05 00 00 00 02 |.........;U.....|
639 641 0070: f8 94 c2 56 80 00 00 03 |...V....|
640 642 cache is updated/truncated when stripping - it is thus very hard to get in a
641 643 situation where the cache is out of sync and the hash check detects it
642 644 $ hg --config extensions.strip= strip -r tip --nob
643 645 $ f --size .hg/cache/rbc-revs*
644 646 .hg/cache/rbc-revs-v1: size=112
645 647
646 648 cache is rebuilt when corruption is detected
647 649 $ echo > .hg/cache/rbc-names-v1
648 650 $ hg log -r '5:&branch(.)' -T '{rev} ' --debug
649 651 referenced branch names not found - rebuilding revision branch cache from scratch
650 652 8 9 10 11 12 13 truncating cache/rbc-revs-v1 to 40
651 653 $ f --size --hexdump .hg/cache/rbc-*
652 654 .hg/cache/rbc-names-v1: size=79
653 655 0000: 62 00 61 00 63 00 61 20 62 72 61 6e 63 68 20 6e |b.a.c.a branch n|
654 656 0010: 61 6d 65 20 6d 75 63 68 20 6c 6f 6e 67 65 72 20 |ame much longer |
655 657 0020: 74 68 61 6e 20 74 68 65 20 64 65 66 61 75 6c 74 |than the default|
656 658 0030: 20 6a 75 73 74 69 66 69 63 61 74 69 6f 6e 20 75 | justification u|
657 659 0040: 73 65 64 20 62 79 20 62 72 61 6e 63 68 65 73 |sed by branches|
658 660 .hg/cache/rbc-revs-v1: size=112
659 661 0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
660 662 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
661 663 0020: 00 00 00 00 00 00 00 00 d8 cb c6 1d 00 00 00 01 |................|
662 664 0030: 58 97 36 a2 00 00 00 02 10 ff 58 95 00 00 00 03 |X.6.......X.....|
663 665 0040: ee bb 94 44 00 00 00 00 5f 40 61 bb 00 00 00 00 |...D...._@a.....|
664 666 0050: bf be 84 1b 00 00 00 00 d3 f1 63 45 80 00 00 00 |..........cE....|
665 667 0060: e3 d4 9c 05 80 00 00 00 e2 3b 55 05 00 00 00 00 |.........;U.....|
666 668
667 669 Test that cache files are created and grows correctly:
668 670
669 671 $ rm .hg/cache/rbc*
670 672 $ hg log -r "5 & branch(5)" -T "{rev}\n"
671 673 5
672 674 $ f --size --hexdump .hg/cache/rbc-*
673 675 .hg/cache/rbc-names-v1: size=1
674 676 0000: 61 |a|
675 677 .hg/cache/rbc-revs-v1: size=112
676 678 0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
677 679 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
678 680 0020: 00 00 00 00 00 00 00 00 d8 cb c6 1d 00 00 00 00 |................|
679 681 0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
680 682 0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
681 683 0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
682 684 0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
683 685
684 686 $ cd ..
685 687
686 688 Test for multiple incorrect branch cache entries:
687 689
688 690 $ hg init b
689 691 $ cd b
690 692 $ touch f
691 693 $ hg ci -Aqmf
692 694 $ echo >> f
693 695 $ hg ci -Amf
694 696 $ hg branch -q branch
695 697 $ hg ci -Amf
696 698
697 699 $ f --size --hexdump .hg/cache/rbc-*
698 700 .hg/cache/rbc-names-v1: size=14
699 701 0000: 64 65 66 61 75 6c 74 00 62 72 61 6e 63 68 |default.branch|
700 702 .hg/cache/rbc-revs-v1: size=24
701 703 0000: 66 e5 f5 aa 00 00 00 00 fa 4c 04 e5 00 00 00 00 |f........L......|
702 704 0010: 56 46 78 69 00 00 00 01 |VFxi....|
703 705 $ : > .hg/cache/rbc-revs-v1
704 706
705 707 No superfluous rebuilding of cache:
706 708 $ hg log -r "branch(null)&branch(branch)" --debug
707 709 $ f --size --hexdump .hg/cache/rbc-*
708 710 .hg/cache/rbc-names-v1: size=14
709 711 0000: 64 65 66 61 75 6c 74 00 62 72 61 6e 63 68 |default.branch|
710 712 .hg/cache/rbc-revs-v1: size=24
711 713 0000: 66 e5 f5 aa 00 00 00 00 fa 4c 04 e5 00 00 00 00 |f........L......|
712 714 0010: 56 46 78 69 00 00 00 01 |VFxi....|
713 715
714 716 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now