##// END OF EJS Templates
date: refactor timezone parsing...
Matt Mackall -
r29636:84ef4517 stable
parent child Browse files
Show More
@@ -1,1161 +1,1163 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 292 def evalinteger(context, mapping, arg, err):
293 293 v = evalfuncarg(context, mapping, arg)
294 294 try:
295 295 return int(v)
296 296 except (TypeError, ValueError):
297 297 raise error.ParseError(err)
298 298
299 299 def evalstring(context, mapping, arg):
300 300 func, data = arg
301 301 return stringify(func(context, mapping, data))
302 302
303 303 def evalstringliteral(context, mapping, arg):
304 304 """Evaluate given argument as string template, but returns symbol name
305 305 if it is unknown"""
306 306 func, data = arg
307 307 if func is runsymbol:
308 308 thing = func(context, mapping, data, default=data)
309 309 else:
310 310 thing = func(context, mapping, data)
311 311 return stringify(thing)
312 312
313 313 def runinteger(context, mapping, data):
314 314 return int(data)
315 315
316 316 def runstring(context, mapping, data):
317 317 return data
318 318
319 319 def _recursivesymbolblocker(key):
320 320 def showrecursion(**args):
321 321 raise error.Abort(_("recursive reference '%s' in template") % key)
322 322 return showrecursion
323 323
324 324 def _runrecursivesymbol(context, mapping, key):
325 325 raise error.Abort(_("recursive reference '%s' in template") % key)
326 326
327 327 def runsymbol(context, mapping, key, default=''):
328 328 v = mapping.get(key)
329 329 if v is None:
330 330 v = context._defaults.get(key)
331 331 if v is None:
332 332 # put poison to cut recursion. we can't move this to parsing phase
333 333 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
334 334 safemapping = mapping.copy()
335 335 safemapping[key] = _recursivesymbolblocker(key)
336 336 try:
337 337 v = context.process(key, safemapping)
338 338 except TemplateNotFound:
339 339 v = default
340 340 if callable(v):
341 341 return v(**mapping)
342 342 return v
343 343
344 344 def buildtemplate(exp, context):
345 345 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
346 346 return (runtemplate, ctmpl)
347 347
348 348 def runtemplate(context, mapping, template):
349 349 for func, data in template:
350 350 yield func(context, mapping, data)
351 351
352 352 def buildfilter(exp, context):
353 353 arg = compileexp(exp[1], context, methods)
354 354 n = getsymbol(exp[2])
355 355 if n in context._filters:
356 356 filt = context._filters[n]
357 357 return (runfilter, (arg, filt))
358 358 if n in funcs:
359 359 f = funcs[n]
360 360 return (f, [arg])
361 361 raise error.ParseError(_("unknown function '%s'") % n)
362 362
363 363 def runfilter(context, mapping, data):
364 364 arg, filt = data
365 365 thing = evalfuncarg(context, mapping, arg)
366 366 try:
367 367 return filt(thing)
368 368 except (ValueError, AttributeError, TypeError):
369 369 if isinstance(arg[1], tuple):
370 370 dt = arg[1][1]
371 371 else:
372 372 dt = arg[1]
373 373 raise error.Abort(_("template filter '%s' is not compatible with "
374 374 "keyword '%s'") % (filt.func_name, dt))
375 375
376 376 def buildmap(exp, context):
377 377 func, data = compileexp(exp[1], context, methods)
378 378 tfunc, tdata = gettemplate(exp[2], context)
379 379 return (runmap, (func, data, tfunc, tdata))
380 380
381 381 def runmap(context, mapping, data):
382 382 func, data, tfunc, tdata = data
383 383 d = func(context, mapping, data)
384 384 if util.safehasattr(d, 'itermaps'):
385 385 diter = d.itermaps()
386 386 else:
387 387 try:
388 388 diter = iter(d)
389 389 except TypeError:
390 390 if func is runsymbol:
391 391 raise error.ParseError(_("keyword '%s' is not iterable") % data)
392 392 else:
393 393 raise error.ParseError(_("%r is not iterable") % d)
394 394
395 395 for i in diter:
396 396 lm = mapping.copy()
397 397 if isinstance(i, dict):
398 398 lm.update(i)
399 399 lm['originalnode'] = mapping.get('node')
400 400 yield tfunc(context, lm, tdata)
401 401 else:
402 402 # v is not an iterable of dicts, this happen when 'key'
403 403 # has been fully expanded already and format is useless.
404 404 # If so, return the expanded value.
405 405 yield i
406 406
407 407 def buildfunc(exp, context):
408 408 n = getsymbol(exp[1])
409 409 args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
410 410 if n in funcs:
411 411 f = funcs[n]
412 412 return (f, args)
413 413 if n in context._filters:
414 414 if len(args) != 1:
415 415 raise error.ParseError(_("filter %s expects one argument") % n)
416 416 f = context._filters[n]
417 417 return (runfilter, (args[0], f))
418 418 raise error.ParseError(_("unknown function '%s'") % n)
419 419
420 420 # dict of template built-in functions
421 421 funcs = {}
422 422
423 423 templatefunc = registrar.templatefunc(funcs)
424 424
425 425 @templatefunc('date(date[, fmt])')
426 426 def date(context, mapping, args):
427 427 """Format a date. See :hg:`help dates` for formatting
428 428 strings. The default is a Unix date format, including the timezone:
429 429 "Mon Sep 04 15:13:13 2006 0700"."""
430 430 if not (1 <= len(args) <= 2):
431 431 # i18n: "date" is a keyword
432 432 raise error.ParseError(_("date expects one or two arguments"))
433 433
434 434 date = evalfuncarg(context, mapping, args[0])
435 435 fmt = None
436 436 if len(args) == 2:
437 437 fmt = evalstring(context, mapping, args[1])
438 438 try:
439 439 if fmt is None:
440 440 return util.datestr(date)
441 441 else:
442 442 return util.datestr(date, fmt)
443 443 except (TypeError, ValueError):
444 444 # i18n: "date" is a keyword
445 445 raise error.ParseError(_("date expects a date information"))
446 446
447 447 @templatefunc('diff([includepattern [, excludepattern]])')
448 448 def diff(context, mapping, args):
449 449 """Show a diff, optionally
450 450 specifying files to include or exclude."""
451 451 if len(args) > 2:
452 452 # i18n: "diff" is a keyword
453 453 raise error.ParseError(_("diff expects zero, one, or two arguments"))
454 454
455 455 def getpatterns(i):
456 456 if i < len(args):
457 457 s = evalstring(context, mapping, args[i]).strip()
458 458 if s:
459 459 return [s]
460 460 return []
461 461
462 462 ctx = mapping['ctx']
463 463 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
464 464
465 465 return ''.join(chunks)
466 466
467 467 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
468 468 def fill(context, mapping, args):
469 469 """Fill many
470 470 paragraphs with optional indentation. See the "fill" filter."""
471 471 if not (1 <= len(args) <= 4):
472 472 # i18n: "fill" is a keyword
473 473 raise error.ParseError(_("fill expects one to four arguments"))
474 474
475 475 text = evalstring(context, mapping, args[0])
476 476 width = 76
477 477 initindent = ''
478 478 hangindent = ''
479 479 if 2 <= len(args) <= 4:
480 480 width = evalinteger(context, mapping, args[1],
481 481 # i18n: "fill" is a keyword
482 482 _("fill expects an integer width"))
483 483 try:
484 484 initindent = evalstring(context, mapping, args[2])
485 485 hangindent = evalstring(context, mapping, args[3])
486 486 except IndexError:
487 487 pass
488 488
489 489 return templatefilters.fill(text, width, initindent, hangindent)
490 490
491 491 @templatefunc('pad(text, width[, fillchar=\' \'[, right=False]])')
492 492 def pad(context, mapping, args):
493 493 """Pad text with a
494 494 fill character."""
495 495 if not (2 <= len(args) <= 4):
496 496 # i18n: "pad" is a keyword
497 497 raise error.ParseError(_("pad() expects two to four arguments"))
498 498
499 499 width = evalinteger(context, mapping, args[1],
500 500 # i18n: "pad" is a keyword
501 501 _("pad() expects an integer width"))
502 502
503 503 text = evalstring(context, mapping, args[0])
504 504
505 505 right = False
506 506 fillchar = ' '
507 507 if len(args) > 2:
508 508 fillchar = evalstring(context, mapping, args[2])
509 509 if len(args) > 3:
510 510 right = util.parsebool(args[3][1])
511 511
512 512 if right:
513 513 return text.rjust(width, fillchar)
514 514 else:
515 515 return text.ljust(width, fillchar)
516 516
517 517 @templatefunc('indent(text, indentchars[, firstline])')
518 518 def indent(context, mapping, args):
519 519 """Indents all non-empty lines
520 520 with the characters given in the indentchars string. An optional
521 521 third parameter will override the indent for the first line only
522 522 if present."""
523 523 if not (2 <= len(args) <= 3):
524 524 # i18n: "indent" is a keyword
525 525 raise error.ParseError(_("indent() expects two or three arguments"))
526 526
527 527 text = evalstring(context, mapping, args[0])
528 528 indent = evalstring(context, mapping, args[1])
529 529
530 530 if len(args) == 3:
531 531 firstline = evalstring(context, mapping, args[2])
532 532 else:
533 533 firstline = indent
534 534
535 535 # the indent function doesn't indent the first line, so we do it here
536 536 return templatefilters.indent(firstline + text, indent)
537 537
538 538 @templatefunc('get(dict, key)')
539 539 def get(context, mapping, args):
540 540 """Get an attribute/key from an object. Some keywords
541 541 are complex types. This function allows you to obtain the value of an
542 542 attribute on these types."""
543 543 if len(args) != 2:
544 544 # i18n: "get" is a keyword
545 545 raise error.ParseError(_("get() expects two arguments"))
546 546
547 547 dictarg = evalfuncarg(context, mapping, args[0])
548 548 if not util.safehasattr(dictarg, 'get'):
549 549 # i18n: "get" is a keyword
550 550 raise error.ParseError(_("get() expects a dict as first argument"))
551 551
552 552 key = evalfuncarg(context, mapping, args[1])
553 553 return dictarg.get(key)
554 554
555 555 @templatefunc('if(expr, then[, else])')
556 556 def if_(context, mapping, args):
557 557 """Conditionally execute based on the result of
558 558 an expression."""
559 559 if not (2 <= len(args) <= 3):
560 560 # i18n: "if" is a keyword
561 561 raise error.ParseError(_("if expects two or three arguments"))
562 562
563 563 test = evalstring(context, mapping, args[0])
564 564 if test:
565 565 yield args[1][0](context, mapping, args[1][1])
566 566 elif len(args) == 3:
567 567 yield args[2][0](context, mapping, args[2][1])
568 568
569 569 @templatefunc('ifcontains(search, thing, then[, else])')
570 570 def ifcontains(context, mapping, args):
571 571 """Conditionally execute based
572 572 on whether the item "search" is in "thing"."""
573 573 if not (3 <= len(args) <= 4):
574 574 # i18n: "ifcontains" is a keyword
575 575 raise error.ParseError(_("ifcontains expects three or four arguments"))
576 576
577 577 item = evalstring(context, mapping, args[0])
578 578 items = evalfuncarg(context, mapping, args[1])
579 579
580 580 if item in items:
581 581 yield args[2][0](context, mapping, args[2][1])
582 582 elif len(args) == 4:
583 583 yield args[3][0](context, mapping, args[3][1])
584 584
585 585 @templatefunc('ifeq(expr1, expr2, then[, else])')
586 586 def ifeq(context, mapping, args):
587 587 """Conditionally execute based on
588 588 whether 2 items are equivalent."""
589 589 if not (3 <= len(args) <= 4):
590 590 # i18n: "ifeq" is a keyword
591 591 raise error.ParseError(_("ifeq expects three or four arguments"))
592 592
593 593 test = evalstring(context, mapping, args[0])
594 594 match = evalstring(context, mapping, args[1])
595 595 if test == match:
596 596 yield args[2][0](context, mapping, args[2][1])
597 597 elif len(args) == 4:
598 598 yield args[3][0](context, mapping, args[3][1])
599 599
600 600 @templatefunc('join(list, sep)')
601 601 def join(context, mapping, args):
602 602 """Join items in a list with a delimiter."""
603 603 if not (1 <= len(args) <= 2):
604 604 # i18n: "join" is a keyword
605 605 raise error.ParseError(_("join expects one or two arguments"))
606 606
607 607 joinset = args[0][0](context, mapping, args[0][1])
608 608 if util.safehasattr(joinset, 'itermaps'):
609 609 jf = joinset.joinfmt
610 610 joinset = [jf(x) for x in joinset.itermaps()]
611 611
612 612 joiner = " "
613 613 if len(args) > 1:
614 614 joiner = evalstring(context, mapping, args[1])
615 615
616 616 first = True
617 617 for x in joinset:
618 618 if first:
619 619 first = False
620 620 else:
621 621 yield joiner
622 622 yield x
623 623
624 624 @templatefunc('label(label, expr)')
625 625 def label(context, mapping, args):
626 626 """Apply a label to generated content. Content with
627 627 a label applied can result in additional post-processing, such as
628 628 automatic colorization."""
629 629 if len(args) != 2:
630 630 # i18n: "label" is a keyword
631 631 raise error.ParseError(_("label expects two arguments"))
632 632
633 633 ui = mapping['ui']
634 634 thing = evalstring(context, mapping, args[1])
635 635 # preserve unknown symbol as literal so effects like 'red', 'bold',
636 636 # etc. don't need to be quoted
637 637 label = evalstringliteral(context, mapping, args[0])
638 638
639 639 return ui.label(thing, label)
640 640
641 641 @templatefunc('latesttag([pattern])')
642 642 def latesttag(context, mapping, args):
643 643 """The global tags matching the given pattern on the
644 644 most recent globally tagged ancestor of this changeset."""
645 645 if len(args) > 1:
646 646 # i18n: "latesttag" is a keyword
647 647 raise error.ParseError(_("latesttag expects at most one argument"))
648 648
649 649 pattern = None
650 650 if len(args) == 1:
651 651 pattern = evalstring(context, mapping, args[0])
652 652
653 653 return templatekw.showlatesttags(pattern, **mapping)
654 654
655 655 @templatefunc('localdate(date[, tz])')
656 656 def localdate(context, mapping, args):
657 657 """Converts a date to the specified timezone.
658 658 The default is local date."""
659 659 if not (1 <= len(args) <= 2):
660 660 # i18n: "localdate" is a keyword
661 661 raise error.ParseError(_("localdate expects one or two arguments"))
662 662
663 663 date = evalfuncarg(context, mapping, args[0])
664 664 try:
665 665 date = util.parsedate(date)
666 666 except AttributeError: # not str nor date tuple
667 667 # i18n: "localdate" is a keyword
668 668 raise error.ParseError(_("localdate expects a date information"))
669 669 if len(args) >= 2:
670 670 tzoffset = None
671 671 tz = evalfuncarg(context, mapping, args[1])
672 672 if isinstance(tz, str):
673 tzoffset = util.parsetimezone(tz)
673 tzoffset, remainder = util.parsetimezone(tz)
674 if remainder:
675 tzoffset = None
674 676 if tzoffset is None:
675 677 try:
676 678 tzoffset = int(tz)
677 679 except (TypeError, ValueError):
678 680 # i18n: "localdate" is a keyword
679 681 raise error.ParseError(_("localdate expects a timezone"))
680 682 else:
681 683 tzoffset = util.makedate()[1]
682 684 return (date[0], tzoffset)
683 685
684 686 @templatefunc('revset(query[, formatargs...])')
685 687 def revset(context, mapping, args):
686 688 """Execute a revision set query. See
687 689 :hg:`help revset`."""
688 690 if not len(args) > 0:
689 691 # i18n: "revset" is a keyword
690 692 raise error.ParseError(_("revset expects one or more arguments"))
691 693
692 694 raw = evalstring(context, mapping, args[0])
693 695 ctx = mapping['ctx']
694 696 repo = ctx.repo()
695 697
696 698 def query(expr):
697 699 m = revsetmod.match(repo.ui, expr)
698 700 return m(repo)
699 701
700 702 if len(args) > 1:
701 703 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
702 704 revs = query(revsetmod.formatspec(raw, *formatargs))
703 705 revs = list(revs)
704 706 else:
705 707 revsetcache = mapping['cache'].setdefault("revsetcache", {})
706 708 if raw in revsetcache:
707 709 revs = revsetcache[raw]
708 710 else:
709 711 revs = query(raw)
710 712 revs = list(revs)
711 713 revsetcache[raw] = revs
712 714
713 715 return templatekw.showrevslist("revision", revs, **mapping)
714 716
715 717 @templatefunc('rstdoc(text, style)')
716 718 def rstdoc(context, mapping, args):
717 719 """Format ReStructuredText."""
718 720 if len(args) != 2:
719 721 # i18n: "rstdoc" is a keyword
720 722 raise error.ParseError(_("rstdoc expects two arguments"))
721 723
722 724 text = evalstring(context, mapping, args[0])
723 725 style = evalstring(context, mapping, args[1])
724 726
725 727 return minirst.format(text, style=style, keep=['verbose'])
726 728
727 729 @templatefunc('separate(sep, args)')
728 730 def separate(context, mapping, args):
729 731 """Add a separator between non-empty arguments."""
730 732 if not args:
731 733 # i18n: "separate" is a keyword
732 734 raise error.ParseError(_("separate expects at least one argument"))
733 735
734 736 sep = evalstring(context, mapping, args[0])
735 737 first = True
736 738 for arg in args[1:]:
737 739 argstr = evalstring(context, mapping, arg)
738 740 if not argstr:
739 741 continue
740 742 if first:
741 743 first = False
742 744 else:
743 745 yield sep
744 746 yield argstr
745 747
746 748 @templatefunc('shortest(node, minlength=4)')
747 749 def shortest(context, mapping, args):
748 750 """Obtain the shortest representation of
749 751 a node."""
750 752 if not (1 <= len(args) <= 2):
751 753 # i18n: "shortest" is a keyword
752 754 raise error.ParseError(_("shortest() expects one or two arguments"))
753 755
754 756 node = evalstring(context, mapping, args[0])
755 757
756 758 minlength = 4
757 759 if len(args) > 1:
758 760 minlength = evalinteger(context, mapping, args[1],
759 761 # i18n: "shortest" is a keyword
760 762 _("shortest() expects an integer minlength"))
761 763
762 764 cl = mapping['ctx']._repo.changelog
763 765 def isvalid(test):
764 766 try:
765 767 try:
766 768 cl.index.partialmatch(test)
767 769 except AttributeError:
768 770 # Pure mercurial doesn't support partialmatch on the index.
769 771 # Fallback to the slow way.
770 772 if cl._partialmatch(test) is None:
771 773 return False
772 774
773 775 try:
774 776 i = int(test)
775 777 # if we are a pure int, then starting with zero will not be
776 778 # confused as a rev; or, obviously, if the int is larger than
777 779 # the value of the tip rev
778 780 if test[0] == '0' or i > len(cl):
779 781 return True
780 782 return False
781 783 except ValueError:
782 784 return True
783 785 except error.RevlogError:
784 786 return False
785 787
786 788 shortest = node
787 789 startlength = max(6, minlength)
788 790 length = startlength
789 791 while True:
790 792 test = node[:length]
791 793 if isvalid(test):
792 794 shortest = test
793 795 if length == minlength or length > startlength:
794 796 return shortest
795 797 length -= 1
796 798 else:
797 799 length += 1
798 800 if len(shortest) <= length:
799 801 return shortest
800 802
801 803 @templatefunc('strip(text[, chars])')
802 804 def strip(context, mapping, args):
803 805 """Strip characters from a string. By default,
804 806 strips all leading and trailing whitespace."""
805 807 if not (1 <= len(args) <= 2):
806 808 # i18n: "strip" is a keyword
807 809 raise error.ParseError(_("strip expects one or two arguments"))
808 810
809 811 text = evalstring(context, mapping, args[0])
810 812 if len(args) == 2:
811 813 chars = evalstring(context, mapping, args[1])
812 814 return text.strip(chars)
813 815 return text.strip()
814 816
815 817 @templatefunc('sub(pattern, replacement, expression)')
816 818 def sub(context, mapping, args):
817 819 """Perform text substitution
818 820 using regular expressions."""
819 821 if len(args) != 3:
820 822 # i18n: "sub" is a keyword
821 823 raise error.ParseError(_("sub expects three arguments"))
822 824
823 825 pat = evalstring(context, mapping, args[0])
824 826 rpl = evalstring(context, mapping, args[1])
825 827 src = evalstring(context, mapping, args[2])
826 828 try:
827 829 patre = re.compile(pat)
828 830 except re.error:
829 831 # i18n: "sub" is a keyword
830 832 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
831 833 try:
832 834 yield patre.sub(rpl, src)
833 835 except re.error:
834 836 # i18n: "sub" is a keyword
835 837 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
836 838
837 839 @templatefunc('startswith(pattern, text)')
838 840 def startswith(context, mapping, args):
839 841 """Returns the value from the "text" argument
840 842 if it begins with the content from the "pattern" argument."""
841 843 if len(args) != 2:
842 844 # i18n: "startswith" is a keyword
843 845 raise error.ParseError(_("startswith expects two arguments"))
844 846
845 847 patn = evalstring(context, mapping, args[0])
846 848 text = evalstring(context, mapping, args[1])
847 849 if text.startswith(patn):
848 850 return text
849 851 return ''
850 852
851 853 @templatefunc('word(number, text[, separator])')
852 854 def word(context, mapping, args):
853 855 """Return the nth word from a string."""
854 856 if not (2 <= len(args) <= 3):
855 857 # i18n: "word" is a keyword
856 858 raise error.ParseError(_("word expects two or three arguments, got %d")
857 859 % len(args))
858 860
859 861 num = evalinteger(context, mapping, args[0],
860 862 # i18n: "word" is a keyword
861 863 _("word expects an integer index"))
862 864 text = evalstring(context, mapping, args[1])
863 865 if len(args) == 3:
864 866 splitter = evalstring(context, mapping, args[2])
865 867 else:
866 868 splitter = None
867 869
868 870 tokens = text.split(splitter)
869 871 if num >= len(tokens) or num < -len(tokens):
870 872 return ''
871 873 else:
872 874 return tokens[num]
873 875
874 876 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
875 877 exprmethods = {
876 878 "integer": lambda e, c: (runinteger, e[1]),
877 879 "string": lambda e, c: (runstring, e[1]),
878 880 "symbol": lambda e, c: (runsymbol, e[1]),
879 881 "template": buildtemplate,
880 882 "group": lambda e, c: compileexp(e[1], c, exprmethods),
881 883 # ".": buildmember,
882 884 "|": buildfilter,
883 885 "%": buildmap,
884 886 "func": buildfunc,
885 887 }
886 888
887 889 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
888 890 methods = exprmethods.copy()
889 891 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
890 892
891 893 class _aliasrules(parser.basealiasrules):
892 894 """Parsing and expansion rule set of template aliases"""
893 895 _section = _('template alias')
894 896 _parse = staticmethod(_parseexpr)
895 897
896 898 @staticmethod
897 899 def _trygetfunc(tree):
898 900 """Return (name, args) if tree is func(...) or ...|filter; otherwise
899 901 None"""
900 902 if tree[0] == 'func' and tree[1][0] == 'symbol':
901 903 return tree[1][1], getlist(tree[2])
902 904 if tree[0] == '|' and tree[2][0] == 'symbol':
903 905 return tree[2][1], [tree[1]]
904 906
905 907 def expandaliases(tree, aliases):
906 908 """Return new tree of aliases are expanded"""
907 909 aliasmap = _aliasrules.buildmap(aliases)
908 910 return _aliasrules.expand(aliasmap, tree)
909 911
910 912 # template engine
911 913
912 914 stringify = templatefilters.stringify
913 915
914 916 def _flatten(thing):
915 917 '''yield a single stream from a possibly nested set of iterators'''
916 918 if isinstance(thing, str):
917 919 yield thing
918 920 elif not util.safehasattr(thing, '__iter__'):
919 921 if thing is not None:
920 922 yield str(thing)
921 923 else:
922 924 for i in thing:
923 925 if isinstance(i, str):
924 926 yield i
925 927 elif not util.safehasattr(i, '__iter__'):
926 928 if i is not None:
927 929 yield str(i)
928 930 elif i is not None:
929 931 for j in _flatten(i):
930 932 yield j
931 933
932 934 def unquotestring(s):
933 935 '''unwrap quotes if any; otherwise returns unmodified string'''
934 936 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
935 937 return s
936 938 return s[1:-1]
937 939
938 940 class engine(object):
939 941 '''template expansion engine.
940 942
941 943 template expansion works like this. a map file contains key=value
942 944 pairs. if value is quoted, it is treated as string. otherwise, it
943 945 is treated as name of template file.
944 946
945 947 templater is asked to expand a key in map. it looks up key, and
946 948 looks for strings like this: {foo}. it expands {foo} by looking up
947 949 foo in map, and substituting it. expansion is recursive: it stops
948 950 when there is no more {foo} to replace.
949 951
950 952 expansion also allows formatting and filtering.
951 953
952 954 format uses key to expand each item in list. syntax is
953 955 {key%format}.
954 956
955 957 filter uses function to transform value. syntax is
956 958 {key|filter1|filter2|...}.'''
957 959
958 960 def __init__(self, loader, filters=None, defaults=None, aliases=()):
959 961 self._loader = loader
960 962 if filters is None:
961 963 filters = {}
962 964 self._filters = filters
963 965 if defaults is None:
964 966 defaults = {}
965 967 self._defaults = defaults
966 968 self._aliasmap = _aliasrules.buildmap(aliases)
967 969 self._cache = {} # key: (func, data)
968 970
969 971 def _load(self, t):
970 972 '''load, parse, and cache a template'''
971 973 if t not in self._cache:
972 974 # put poison to cut recursion while compiling 't'
973 975 self._cache[t] = (_runrecursivesymbol, t)
974 976 try:
975 977 x = parse(self._loader(t))
976 978 if self._aliasmap:
977 979 x = _aliasrules.expand(self._aliasmap, x)
978 980 self._cache[t] = compileexp(x, self, methods)
979 981 except: # re-raises
980 982 del self._cache[t]
981 983 raise
982 984 return self._cache[t]
983 985
984 986 def process(self, t, mapping):
985 987 '''Perform expansion. t is name of map element to expand.
986 988 mapping contains added elements for use during expansion. Is a
987 989 generator.'''
988 990 func, data = self._load(t)
989 991 return _flatten(func(self, mapping, data))
990 992
991 993 engines = {'default': engine}
992 994
993 995 def stylelist():
994 996 paths = templatepaths()
995 997 if not paths:
996 998 return _('no templates found, try `hg debuginstall` for more info')
997 999 dirlist = os.listdir(paths[0])
998 1000 stylelist = []
999 1001 for file in dirlist:
1000 1002 split = file.split(".")
1001 1003 if split[-1] in ('orig', 'rej'):
1002 1004 continue
1003 1005 if split[0] == "map-cmdline":
1004 1006 stylelist.append(split[1])
1005 1007 return ", ".join(sorted(stylelist))
1006 1008
1007 1009 def _readmapfile(mapfile):
1008 1010 """Load template elements from the given map file"""
1009 1011 if not os.path.exists(mapfile):
1010 1012 raise error.Abort(_("style '%s' not found") % mapfile,
1011 1013 hint=_("available styles: %s") % stylelist())
1012 1014
1013 1015 base = os.path.dirname(mapfile)
1014 1016 conf = config.config(includepaths=templatepaths())
1015 1017 conf.read(mapfile)
1016 1018
1017 1019 cache = {}
1018 1020 tmap = {}
1019 1021 for key, val in conf[''].items():
1020 1022 if not val:
1021 1023 raise error.ParseError(_('missing value'), conf.source('', key))
1022 1024 if val[0] in "'\"":
1023 1025 if val[0] != val[-1]:
1024 1026 raise error.ParseError(_('unmatched quotes'),
1025 1027 conf.source('', key))
1026 1028 cache[key] = unquotestring(val)
1027 1029 else:
1028 1030 val = 'default', val
1029 1031 if ':' in val[1]:
1030 1032 val = val[1].split(':', 1)
1031 1033 tmap[key] = val[0], os.path.join(base, val[1])
1032 1034 return cache, tmap
1033 1035
1034 1036 class TemplateNotFound(error.Abort):
1035 1037 pass
1036 1038
1037 1039 class templater(object):
1038 1040
1039 1041 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1040 1042 minchunk=1024, maxchunk=65536):
1041 1043 '''set up template engine.
1042 1044 filters is dict of functions. each transforms a value into another.
1043 1045 defaults is dict of default map definitions.
1044 1046 aliases is list of alias (name, replacement) pairs.
1045 1047 '''
1046 1048 if filters is None:
1047 1049 filters = {}
1048 1050 if defaults is None:
1049 1051 defaults = {}
1050 1052 if cache is None:
1051 1053 cache = {}
1052 1054 self.cache = cache.copy()
1053 1055 self.map = {}
1054 1056 self.filters = templatefilters.filters.copy()
1055 1057 self.filters.update(filters)
1056 1058 self.defaults = defaults
1057 1059 self._aliases = aliases
1058 1060 self.minchunk, self.maxchunk = minchunk, maxchunk
1059 1061 self.ecache = {}
1060 1062
1061 1063 @classmethod
1062 1064 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1063 1065 minchunk=1024, maxchunk=65536):
1064 1066 """Create templater from the specified map file"""
1065 1067 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1066 1068 cache, tmap = _readmapfile(mapfile)
1067 1069 t.cache.update(cache)
1068 1070 t.map = tmap
1069 1071 return t
1070 1072
1071 1073 def __contains__(self, key):
1072 1074 return key in self.cache or key in self.map
1073 1075
1074 1076 def load(self, t):
1075 1077 '''Get the template for the given template name. Use a local cache.'''
1076 1078 if t not in self.cache:
1077 1079 try:
1078 1080 self.cache[t] = util.readfile(self.map[t][1])
1079 1081 except KeyError as inst:
1080 1082 raise TemplateNotFound(_('"%s" not in template map') %
1081 1083 inst.args[0])
1082 1084 except IOError as inst:
1083 1085 raise IOError(inst.args[0], _('template file %s: %s') %
1084 1086 (self.map[t][1], inst.args[1]))
1085 1087 return self.cache[t]
1086 1088
1087 1089 def __call__(self, t, **mapping):
1088 1090 ttype = t in self.map and self.map[t][0] or 'default'
1089 1091 if ttype not in self.ecache:
1090 1092 try:
1091 1093 ecls = engines[ttype]
1092 1094 except KeyError:
1093 1095 raise error.Abort(_('invalid template engine: %s') % ttype)
1094 1096 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1095 1097 self._aliases)
1096 1098 proc = self.ecache[ttype]
1097 1099
1098 1100 stream = proc.process(t, mapping)
1099 1101 if self.minchunk:
1100 1102 stream = util.increasingchunks(stream, min=self.minchunk,
1101 1103 max=self.maxchunk)
1102 1104 return stream
1103 1105
1104 1106 def templatepaths():
1105 1107 '''return locations used for template files.'''
1106 1108 pathsrel = ['templates']
1107 1109 paths = [os.path.normpath(os.path.join(util.datapath, f))
1108 1110 for f in pathsrel]
1109 1111 return [p for p in paths if os.path.isdir(p)]
1110 1112
1111 1113 def templatepath(name):
1112 1114 '''return location of template file. returns None if not found.'''
1113 1115 for p in templatepaths():
1114 1116 f = os.path.join(p, name)
1115 1117 if os.path.exists(f):
1116 1118 return f
1117 1119 return None
1118 1120
1119 1121 def stylemap(styles, paths=None):
1120 1122 """Return path to mapfile for a given style.
1121 1123
1122 1124 Searches mapfile in the following locations:
1123 1125 1. templatepath/style/map
1124 1126 2. templatepath/map-style
1125 1127 3. templatepath/map
1126 1128 """
1127 1129
1128 1130 if paths is None:
1129 1131 paths = templatepaths()
1130 1132 elif isinstance(paths, str):
1131 1133 paths = [paths]
1132 1134
1133 1135 if isinstance(styles, str):
1134 1136 styles = [styles]
1135 1137
1136 1138 for style in styles:
1137 1139 # only plain name is allowed to honor template paths
1138 1140 if (not style
1139 1141 or style in (os.curdir, os.pardir)
1140 1142 or os.sep in style
1141 1143 or os.altsep and os.altsep in style):
1142 1144 continue
1143 1145 locations = [os.path.join(style, 'map'), 'map-' + style]
1144 1146 locations.append('map')
1145 1147
1146 1148 for path in paths:
1147 1149 for location in locations:
1148 1150 mapfile = os.path.join(path, location)
1149 1151 if os.path.isfile(mapfile):
1150 1152 return style, mapfile
1151 1153
1152 1154 raise RuntimeError("No hgweb templates found in %r" % paths)
1153 1155
1154 1156 def loadfunction(ui, extname, registrarobj):
1155 1157 """Load template function from specified registrarobj
1156 1158 """
1157 1159 for name, func in registrarobj._table.iteritems():
1158 1160 funcs[name] = func
1159 1161
1160 1162 # tell hggettext to extract docstrings from these functions:
1161 1163 i18nfunctions = funcs.values()
@@ -1,2879 +1,2882 b''
1 1 # util.py - Mercurial utility functions and platform specific implementations
2 2 #
3 3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 """Mercurial utility functions and platform specific implementations.
11 11
12 12 This contains helper routines that are independent of the SCM core and
13 13 hide platform-specific details from the core.
14 14 """
15 15
16 16 from __future__ import absolute_import
17 17
18 18 import bz2
19 19 import calendar
20 20 import collections
21 21 import datetime
22 22 import errno
23 23 import gc
24 24 import hashlib
25 25 import imp
26 26 import os
27 27 import re as remod
28 28 import shutil
29 29 import signal
30 30 import socket
31 31 import subprocess
32 32 import sys
33 33 import tempfile
34 34 import textwrap
35 35 import time
36 36 import traceback
37 37 import zlib
38 38
39 39 from . import (
40 40 encoding,
41 41 error,
42 42 i18n,
43 43 osutil,
44 44 parsers,
45 45 pycompat,
46 46 )
47 47
48 48 for attr in (
49 49 'empty',
50 50 'httplib',
51 51 'httpserver',
52 52 'pickle',
53 53 'queue',
54 54 'urlerr',
55 55 'urlparse',
56 56 # we do import urlreq, but we do it outside the loop
57 57 #'urlreq',
58 58 'stringio',
59 59 'socketserver',
60 60 'xmlrpclib',
61 61 ):
62 62 globals()[attr] = getattr(pycompat, attr)
63 63
64 64 # This line is to make pyflakes happy:
65 65 urlreq = pycompat.urlreq
66 66
67 67 if os.name == 'nt':
68 68 from . import windows as platform
69 69 else:
70 70 from . import posix as platform
71 71
72 72 _ = i18n._
73 73
74 74 bindunixsocket = platform.bindunixsocket
75 75 cachestat = platform.cachestat
76 76 checkexec = platform.checkexec
77 77 checklink = platform.checklink
78 78 copymode = platform.copymode
79 79 executablepath = platform.executablepath
80 80 expandglobs = platform.expandglobs
81 81 explainexit = platform.explainexit
82 82 findexe = platform.findexe
83 83 gethgcmd = platform.gethgcmd
84 84 getuser = platform.getuser
85 85 getpid = os.getpid
86 86 groupmembers = platform.groupmembers
87 87 groupname = platform.groupname
88 88 hidewindow = platform.hidewindow
89 89 isexec = platform.isexec
90 90 isowner = platform.isowner
91 91 localpath = platform.localpath
92 92 lookupreg = platform.lookupreg
93 93 makedir = platform.makedir
94 94 nlinks = platform.nlinks
95 95 normpath = platform.normpath
96 96 normcase = platform.normcase
97 97 normcasespec = platform.normcasespec
98 98 normcasefallback = platform.normcasefallback
99 99 openhardlinks = platform.openhardlinks
100 100 oslink = platform.oslink
101 101 parsepatchoutput = platform.parsepatchoutput
102 102 pconvert = platform.pconvert
103 103 poll = platform.poll
104 104 popen = platform.popen
105 105 posixfile = platform.posixfile
106 106 quotecommand = platform.quotecommand
107 107 readpipe = platform.readpipe
108 108 rename = platform.rename
109 109 removedirs = platform.removedirs
110 110 samedevice = platform.samedevice
111 111 samefile = platform.samefile
112 112 samestat = platform.samestat
113 113 setbinary = platform.setbinary
114 114 setflags = platform.setflags
115 115 setsignalhandler = platform.setsignalhandler
116 116 shellquote = platform.shellquote
117 117 spawndetached = platform.spawndetached
118 118 split = platform.split
119 119 sshargs = platform.sshargs
120 120 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
121 121 statisexec = platform.statisexec
122 122 statislink = platform.statislink
123 123 termwidth = platform.termwidth
124 124 testpid = platform.testpid
125 125 umask = platform.umask
126 126 unlink = platform.unlink
127 127 unlinkpath = platform.unlinkpath
128 128 username = platform.username
129 129
130 130 # Python compatibility
131 131
132 132 _notset = object()
133 133
134 134 # disable Python's problematic floating point timestamps (issue4836)
135 135 # (Python hypocritically says you shouldn't change this behavior in
136 136 # libraries, and sure enough Mercurial is not a library.)
137 137 os.stat_float_times(False)
138 138
139 139 def safehasattr(thing, attr):
140 140 return getattr(thing, attr, _notset) is not _notset
141 141
142 142 DIGESTS = {
143 143 'md5': hashlib.md5,
144 144 'sha1': hashlib.sha1,
145 145 'sha512': hashlib.sha512,
146 146 }
147 147 # List of digest types from strongest to weakest
148 148 DIGESTS_BY_STRENGTH = ['sha512', 'sha1', 'md5']
149 149
150 150 for k in DIGESTS_BY_STRENGTH:
151 151 assert k in DIGESTS
152 152
153 153 class digester(object):
154 154 """helper to compute digests.
155 155
156 156 This helper can be used to compute one or more digests given their name.
157 157
158 158 >>> d = digester(['md5', 'sha1'])
159 159 >>> d.update('foo')
160 160 >>> [k for k in sorted(d)]
161 161 ['md5', 'sha1']
162 162 >>> d['md5']
163 163 'acbd18db4cc2f85cedef654fccc4a4d8'
164 164 >>> d['sha1']
165 165 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
166 166 >>> digester.preferred(['md5', 'sha1'])
167 167 'sha1'
168 168 """
169 169
170 170 def __init__(self, digests, s=''):
171 171 self._hashes = {}
172 172 for k in digests:
173 173 if k not in DIGESTS:
174 174 raise Abort(_('unknown digest type: %s') % k)
175 175 self._hashes[k] = DIGESTS[k]()
176 176 if s:
177 177 self.update(s)
178 178
179 179 def update(self, data):
180 180 for h in self._hashes.values():
181 181 h.update(data)
182 182
183 183 def __getitem__(self, key):
184 184 if key not in DIGESTS:
185 185 raise Abort(_('unknown digest type: %s') % k)
186 186 return self._hashes[key].hexdigest()
187 187
188 188 def __iter__(self):
189 189 return iter(self._hashes)
190 190
191 191 @staticmethod
192 192 def preferred(supported):
193 193 """returns the strongest digest type in both supported and DIGESTS."""
194 194
195 195 for k in DIGESTS_BY_STRENGTH:
196 196 if k in supported:
197 197 return k
198 198 return None
199 199
200 200 class digestchecker(object):
201 201 """file handle wrapper that additionally checks content against a given
202 202 size and digests.
203 203
204 204 d = digestchecker(fh, size, {'md5': '...'})
205 205
206 206 When multiple digests are given, all of them are validated.
207 207 """
208 208
209 209 def __init__(self, fh, size, digests):
210 210 self._fh = fh
211 211 self._size = size
212 212 self._got = 0
213 213 self._digests = dict(digests)
214 214 self._digester = digester(self._digests.keys())
215 215
216 216 def read(self, length=-1):
217 217 content = self._fh.read(length)
218 218 self._digester.update(content)
219 219 self._got += len(content)
220 220 return content
221 221
222 222 def validate(self):
223 223 if self._size != self._got:
224 224 raise Abort(_('size mismatch: expected %d, got %d') %
225 225 (self._size, self._got))
226 226 for k, v in self._digests.items():
227 227 if v != self._digester[k]:
228 228 # i18n: first parameter is a digest name
229 229 raise Abort(_('%s mismatch: expected %s, got %s') %
230 230 (k, v, self._digester[k]))
231 231
232 232 try:
233 233 buffer = buffer
234 234 except NameError:
235 235 if sys.version_info[0] < 3:
236 236 def buffer(sliceable, offset=0):
237 237 return sliceable[offset:]
238 238 else:
239 239 def buffer(sliceable, offset=0):
240 240 return memoryview(sliceable)[offset:]
241 241
242 242 closefds = os.name == 'posix'
243 243
244 244 _chunksize = 4096
245 245
246 246 class bufferedinputpipe(object):
247 247 """a manually buffered input pipe
248 248
249 249 Python will not let us use buffered IO and lazy reading with 'polling' at
250 250 the same time. We cannot probe the buffer state and select will not detect
251 251 that data are ready to read if they are already buffered.
252 252
253 253 This class let us work around that by implementing its own buffering
254 254 (allowing efficient readline) while offering a way to know if the buffer is
255 255 empty from the output (allowing collaboration of the buffer with polling).
256 256
257 257 This class lives in the 'util' module because it makes use of the 'os'
258 258 module from the python stdlib.
259 259 """
260 260
261 261 def __init__(self, input):
262 262 self._input = input
263 263 self._buffer = []
264 264 self._eof = False
265 265 self._lenbuf = 0
266 266
267 267 @property
268 268 def hasbuffer(self):
269 269 """True is any data is currently buffered
270 270
271 271 This will be used externally a pre-step for polling IO. If there is
272 272 already data then no polling should be set in place."""
273 273 return bool(self._buffer)
274 274
275 275 @property
276 276 def closed(self):
277 277 return self._input.closed
278 278
279 279 def fileno(self):
280 280 return self._input.fileno()
281 281
282 282 def close(self):
283 283 return self._input.close()
284 284
285 285 def read(self, size):
286 286 while (not self._eof) and (self._lenbuf < size):
287 287 self._fillbuffer()
288 288 return self._frombuffer(size)
289 289
290 290 def readline(self, *args, **kwargs):
291 291 if 1 < len(self._buffer):
292 292 # this should not happen because both read and readline end with a
293 293 # _frombuffer call that collapse it.
294 294 self._buffer = [''.join(self._buffer)]
295 295 self._lenbuf = len(self._buffer[0])
296 296 lfi = -1
297 297 if self._buffer:
298 298 lfi = self._buffer[-1].find('\n')
299 299 while (not self._eof) and lfi < 0:
300 300 self._fillbuffer()
301 301 if self._buffer:
302 302 lfi = self._buffer[-1].find('\n')
303 303 size = lfi + 1
304 304 if lfi < 0: # end of file
305 305 size = self._lenbuf
306 306 elif 1 < len(self._buffer):
307 307 # we need to take previous chunks into account
308 308 size += self._lenbuf - len(self._buffer[-1])
309 309 return self._frombuffer(size)
310 310
311 311 def _frombuffer(self, size):
312 312 """return at most 'size' data from the buffer
313 313
314 314 The data are removed from the buffer."""
315 315 if size == 0 or not self._buffer:
316 316 return ''
317 317 buf = self._buffer[0]
318 318 if 1 < len(self._buffer):
319 319 buf = ''.join(self._buffer)
320 320
321 321 data = buf[:size]
322 322 buf = buf[len(data):]
323 323 if buf:
324 324 self._buffer = [buf]
325 325 self._lenbuf = len(buf)
326 326 else:
327 327 self._buffer = []
328 328 self._lenbuf = 0
329 329 return data
330 330
331 331 def _fillbuffer(self):
332 332 """read data to the buffer"""
333 333 data = os.read(self._input.fileno(), _chunksize)
334 334 if not data:
335 335 self._eof = True
336 336 else:
337 337 self._lenbuf += len(data)
338 338 self._buffer.append(data)
339 339
340 340 def popen2(cmd, env=None, newlines=False):
341 341 # Setting bufsize to -1 lets the system decide the buffer size.
342 342 # The default for bufsize is 0, meaning unbuffered. This leads to
343 343 # poor performance on Mac OS X: http://bugs.python.org/issue4194
344 344 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
345 345 close_fds=closefds,
346 346 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
347 347 universal_newlines=newlines,
348 348 env=env)
349 349 return p.stdin, p.stdout
350 350
351 351 def popen3(cmd, env=None, newlines=False):
352 352 stdin, stdout, stderr, p = popen4(cmd, env, newlines)
353 353 return stdin, stdout, stderr
354 354
355 355 def popen4(cmd, env=None, newlines=False, bufsize=-1):
356 356 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
357 357 close_fds=closefds,
358 358 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
359 359 stderr=subprocess.PIPE,
360 360 universal_newlines=newlines,
361 361 env=env)
362 362 return p.stdin, p.stdout, p.stderr, p
363 363
364 364 def version():
365 365 """Return version information if available."""
366 366 try:
367 367 from . import __version__
368 368 return __version__.version
369 369 except ImportError:
370 370 return 'unknown'
371 371
372 372 def versiontuple(v=None, n=4):
373 373 """Parses a Mercurial version string into an N-tuple.
374 374
375 375 The version string to be parsed is specified with the ``v`` argument.
376 376 If it isn't defined, the current Mercurial version string will be parsed.
377 377
378 378 ``n`` can be 2, 3, or 4. Here is how some version strings map to
379 379 returned values:
380 380
381 381 >>> v = '3.6.1+190-df9b73d2d444'
382 382 >>> versiontuple(v, 2)
383 383 (3, 6)
384 384 >>> versiontuple(v, 3)
385 385 (3, 6, 1)
386 386 >>> versiontuple(v, 4)
387 387 (3, 6, 1, '190-df9b73d2d444')
388 388
389 389 >>> versiontuple('3.6.1+190-df9b73d2d444+20151118')
390 390 (3, 6, 1, '190-df9b73d2d444+20151118')
391 391
392 392 >>> v = '3.6'
393 393 >>> versiontuple(v, 2)
394 394 (3, 6)
395 395 >>> versiontuple(v, 3)
396 396 (3, 6, None)
397 397 >>> versiontuple(v, 4)
398 398 (3, 6, None, None)
399 399
400 400 >>> v = '3.9-rc'
401 401 >>> versiontuple(v, 2)
402 402 (3, 9)
403 403 >>> versiontuple(v, 3)
404 404 (3, 9, None)
405 405 >>> versiontuple(v, 4)
406 406 (3, 9, None, 'rc')
407 407
408 408 >>> v = '3.9-rc+2-02a8fea4289b'
409 409 >>> versiontuple(v, 2)
410 410 (3, 9)
411 411 >>> versiontuple(v, 3)
412 412 (3, 9, None)
413 413 >>> versiontuple(v, 4)
414 414 (3, 9, None, 'rc+2-02a8fea4289b')
415 415 """
416 416 if not v:
417 417 v = version()
418 418 parts = remod.split('[\+-]', v, 1)
419 419 if len(parts) == 1:
420 420 vparts, extra = parts[0], None
421 421 else:
422 422 vparts, extra = parts
423 423
424 424 vints = []
425 425 for i in vparts.split('.'):
426 426 try:
427 427 vints.append(int(i))
428 428 except ValueError:
429 429 break
430 430 # (3, 6) -> (3, 6, None)
431 431 while len(vints) < 3:
432 432 vints.append(None)
433 433
434 434 if n == 2:
435 435 return (vints[0], vints[1])
436 436 if n == 3:
437 437 return (vints[0], vints[1], vints[2])
438 438 if n == 4:
439 439 return (vints[0], vints[1], vints[2], extra)
440 440
441 441 # used by parsedate
442 442 defaultdateformats = (
443 443 '%Y-%m-%d %H:%M:%S',
444 444 '%Y-%m-%d %I:%M:%S%p',
445 445 '%Y-%m-%d %H:%M',
446 446 '%Y-%m-%d %I:%M%p',
447 447 '%Y-%m-%d',
448 448 '%m-%d',
449 449 '%m/%d',
450 450 '%m/%d/%y',
451 451 '%m/%d/%Y',
452 452 '%a %b %d %H:%M:%S %Y',
453 453 '%a %b %d %I:%M:%S%p %Y',
454 454 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
455 455 '%b %d %H:%M:%S %Y',
456 456 '%b %d %I:%M:%S%p %Y',
457 457 '%b %d %H:%M:%S',
458 458 '%b %d %I:%M:%S%p',
459 459 '%b %d %H:%M',
460 460 '%b %d %I:%M%p',
461 461 '%b %d %Y',
462 462 '%b %d',
463 463 '%H:%M:%S',
464 464 '%I:%M:%S%p',
465 465 '%H:%M',
466 466 '%I:%M%p',
467 467 )
468 468
469 469 extendeddateformats = defaultdateformats + (
470 470 "%Y",
471 471 "%Y-%m",
472 472 "%b",
473 473 "%b %Y",
474 474 )
475 475
476 476 def cachefunc(func):
477 477 '''cache the result of function calls'''
478 478 # XXX doesn't handle keywords args
479 479 if func.__code__.co_argcount == 0:
480 480 cache = []
481 481 def f():
482 482 if len(cache) == 0:
483 483 cache.append(func())
484 484 return cache[0]
485 485 return f
486 486 cache = {}
487 487 if func.__code__.co_argcount == 1:
488 488 # we gain a small amount of time because
489 489 # we don't need to pack/unpack the list
490 490 def f(arg):
491 491 if arg not in cache:
492 492 cache[arg] = func(arg)
493 493 return cache[arg]
494 494 else:
495 495 def f(*args):
496 496 if args not in cache:
497 497 cache[args] = func(*args)
498 498 return cache[args]
499 499
500 500 return f
501 501
502 502 class sortdict(dict):
503 503 '''a simple sorted dictionary'''
504 504 def __init__(self, data=None):
505 505 self._list = []
506 506 if data:
507 507 self.update(data)
508 508 def copy(self):
509 509 return sortdict(self)
510 510 def __setitem__(self, key, val):
511 511 if key in self:
512 512 self._list.remove(key)
513 513 self._list.append(key)
514 514 dict.__setitem__(self, key, val)
515 515 def __iter__(self):
516 516 return self._list.__iter__()
517 517 def update(self, src):
518 518 if isinstance(src, dict):
519 519 src = src.iteritems()
520 520 for k, v in src:
521 521 self[k] = v
522 522 def clear(self):
523 523 dict.clear(self)
524 524 self._list = []
525 525 def items(self):
526 526 return [(k, self[k]) for k in self._list]
527 527 def __delitem__(self, key):
528 528 dict.__delitem__(self, key)
529 529 self._list.remove(key)
530 530 def pop(self, key, *args, **kwargs):
531 531 dict.pop(self, key, *args, **kwargs)
532 532 try:
533 533 self._list.remove(key)
534 534 except ValueError:
535 535 pass
536 536 def keys(self):
537 537 return self._list
538 538 def iterkeys(self):
539 539 return self._list.__iter__()
540 540 def iteritems(self):
541 541 for k in self._list:
542 542 yield k, self[k]
543 543 def insert(self, index, key, val):
544 544 self._list.insert(index, key)
545 545 dict.__setitem__(self, key, val)
546 546 def __repr__(self):
547 547 if not self:
548 548 return '%s()' % self.__class__.__name__
549 549 return '%s(%r)' % (self.__class__.__name__, self.items())
550 550
551 551 class _lrucachenode(object):
552 552 """A node in a doubly linked list.
553 553
554 554 Holds a reference to nodes on either side as well as a key-value
555 555 pair for the dictionary entry.
556 556 """
557 557 __slots__ = ('next', 'prev', 'key', 'value')
558 558
559 559 def __init__(self):
560 560 self.next = None
561 561 self.prev = None
562 562
563 563 self.key = _notset
564 564 self.value = None
565 565
566 566 def markempty(self):
567 567 """Mark the node as emptied."""
568 568 self.key = _notset
569 569
570 570 class lrucachedict(object):
571 571 """Dict that caches most recent accesses and sets.
572 572
573 573 The dict consists of an actual backing dict - indexed by original
574 574 key - and a doubly linked circular list defining the order of entries in
575 575 the cache.
576 576
577 577 The head node is the newest entry in the cache. If the cache is full,
578 578 we recycle head.prev and make it the new head. Cache accesses result in
579 579 the node being moved to before the existing head and being marked as the
580 580 new head node.
581 581 """
582 582 def __init__(self, max):
583 583 self._cache = {}
584 584
585 585 self._head = head = _lrucachenode()
586 586 head.prev = head
587 587 head.next = head
588 588 self._size = 1
589 589 self._capacity = max
590 590
591 591 def __len__(self):
592 592 return len(self._cache)
593 593
594 594 def __contains__(self, k):
595 595 return k in self._cache
596 596
597 597 def __iter__(self):
598 598 # We don't have to iterate in cache order, but why not.
599 599 n = self._head
600 600 for i in range(len(self._cache)):
601 601 yield n.key
602 602 n = n.next
603 603
604 604 def __getitem__(self, k):
605 605 node = self._cache[k]
606 606 self._movetohead(node)
607 607 return node.value
608 608
609 609 def __setitem__(self, k, v):
610 610 node = self._cache.get(k)
611 611 # Replace existing value and mark as newest.
612 612 if node is not None:
613 613 node.value = v
614 614 self._movetohead(node)
615 615 return
616 616
617 617 if self._size < self._capacity:
618 618 node = self._addcapacity()
619 619 else:
620 620 # Grab the last/oldest item.
621 621 node = self._head.prev
622 622
623 623 # At capacity. Kill the old entry.
624 624 if node.key is not _notset:
625 625 del self._cache[node.key]
626 626
627 627 node.key = k
628 628 node.value = v
629 629 self._cache[k] = node
630 630 # And mark it as newest entry. No need to adjust order since it
631 631 # is already self._head.prev.
632 632 self._head = node
633 633
634 634 def __delitem__(self, k):
635 635 node = self._cache.pop(k)
636 636 node.markempty()
637 637
638 638 # Temporarily mark as newest item before re-adjusting head to make
639 639 # this node the oldest item.
640 640 self._movetohead(node)
641 641 self._head = node.next
642 642
643 643 # Additional dict methods.
644 644
645 645 def get(self, k, default=None):
646 646 try:
647 647 return self._cache[k]
648 648 except KeyError:
649 649 return default
650 650
651 651 def clear(self):
652 652 n = self._head
653 653 while n.key is not _notset:
654 654 n.markempty()
655 655 n = n.next
656 656
657 657 self._cache.clear()
658 658
659 659 def copy(self):
660 660 result = lrucachedict(self._capacity)
661 661 n = self._head.prev
662 662 # Iterate in oldest-to-newest order, so the copy has the right ordering
663 663 for i in range(len(self._cache)):
664 664 result[n.key] = n.value
665 665 n = n.prev
666 666 return result
667 667
668 668 def _movetohead(self, node):
669 669 """Mark a node as the newest, making it the new head.
670 670
671 671 When a node is accessed, it becomes the freshest entry in the LRU
672 672 list, which is denoted by self._head.
673 673
674 674 Visually, let's make ``N`` the new head node (* denotes head):
675 675
676 676 previous/oldest <-> head <-> next/next newest
677 677
678 678 ----<->--- A* ---<->-----
679 679 | |
680 680 E <-> D <-> N <-> C <-> B
681 681
682 682 To:
683 683
684 684 ----<->--- N* ---<->-----
685 685 | |
686 686 E <-> D <-> C <-> B <-> A
687 687
688 688 This requires the following moves:
689 689
690 690 C.next = D (node.prev.next = node.next)
691 691 D.prev = C (node.next.prev = node.prev)
692 692 E.next = N (head.prev.next = node)
693 693 N.prev = E (node.prev = head.prev)
694 694 N.next = A (node.next = head)
695 695 A.prev = N (head.prev = node)
696 696 """
697 697 head = self._head
698 698 # C.next = D
699 699 node.prev.next = node.next
700 700 # D.prev = C
701 701 node.next.prev = node.prev
702 702 # N.prev = E
703 703 node.prev = head.prev
704 704 # N.next = A
705 705 # It is tempting to do just "head" here, however if node is
706 706 # adjacent to head, this will do bad things.
707 707 node.next = head.prev.next
708 708 # E.next = N
709 709 node.next.prev = node
710 710 # A.prev = N
711 711 node.prev.next = node
712 712
713 713 self._head = node
714 714
715 715 def _addcapacity(self):
716 716 """Add a node to the circular linked list.
717 717
718 718 The new node is inserted before the head node.
719 719 """
720 720 head = self._head
721 721 node = _lrucachenode()
722 722 head.prev.next = node
723 723 node.prev = head.prev
724 724 node.next = head
725 725 head.prev = node
726 726 self._size += 1
727 727 return node
728 728
729 729 def lrucachefunc(func):
730 730 '''cache most recent results of function calls'''
731 731 cache = {}
732 732 order = collections.deque()
733 733 if func.__code__.co_argcount == 1:
734 734 def f(arg):
735 735 if arg not in cache:
736 736 if len(cache) > 20:
737 737 del cache[order.popleft()]
738 738 cache[arg] = func(arg)
739 739 else:
740 740 order.remove(arg)
741 741 order.append(arg)
742 742 return cache[arg]
743 743 else:
744 744 def f(*args):
745 745 if args not in cache:
746 746 if len(cache) > 20:
747 747 del cache[order.popleft()]
748 748 cache[args] = func(*args)
749 749 else:
750 750 order.remove(args)
751 751 order.append(args)
752 752 return cache[args]
753 753
754 754 return f
755 755
756 756 class propertycache(object):
757 757 def __init__(self, func):
758 758 self.func = func
759 759 self.name = func.__name__
760 760 def __get__(self, obj, type=None):
761 761 result = self.func(obj)
762 762 self.cachevalue(obj, result)
763 763 return result
764 764
765 765 def cachevalue(self, obj, value):
766 766 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
767 767 obj.__dict__[self.name] = value
768 768
769 769 def pipefilter(s, cmd):
770 770 '''filter string S through command CMD, returning its output'''
771 771 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
772 772 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
773 773 pout, perr = p.communicate(s)
774 774 return pout
775 775
776 776 def tempfilter(s, cmd):
777 777 '''filter string S through a pair of temporary files with CMD.
778 778 CMD is used as a template to create the real command to be run,
779 779 with the strings INFILE and OUTFILE replaced by the real names of
780 780 the temporary files generated.'''
781 781 inname, outname = None, None
782 782 try:
783 783 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
784 784 fp = os.fdopen(infd, 'wb')
785 785 fp.write(s)
786 786 fp.close()
787 787 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
788 788 os.close(outfd)
789 789 cmd = cmd.replace('INFILE', inname)
790 790 cmd = cmd.replace('OUTFILE', outname)
791 791 code = os.system(cmd)
792 792 if sys.platform == 'OpenVMS' and code & 1:
793 793 code = 0
794 794 if code:
795 795 raise Abort(_("command '%s' failed: %s") %
796 796 (cmd, explainexit(code)))
797 797 return readfile(outname)
798 798 finally:
799 799 try:
800 800 if inname:
801 801 os.unlink(inname)
802 802 except OSError:
803 803 pass
804 804 try:
805 805 if outname:
806 806 os.unlink(outname)
807 807 except OSError:
808 808 pass
809 809
810 810 filtertable = {
811 811 'tempfile:': tempfilter,
812 812 'pipe:': pipefilter,
813 813 }
814 814
815 815 def filter(s, cmd):
816 816 "filter a string through a command that transforms its input to its output"
817 817 for name, fn in filtertable.iteritems():
818 818 if cmd.startswith(name):
819 819 return fn(s, cmd[len(name):].lstrip())
820 820 return pipefilter(s, cmd)
821 821
822 822 def binary(s):
823 823 """return true if a string is binary data"""
824 824 return bool(s and '\0' in s)
825 825
826 826 def increasingchunks(source, min=1024, max=65536):
827 827 '''return no less than min bytes per chunk while data remains,
828 828 doubling min after each chunk until it reaches max'''
829 829 def log2(x):
830 830 if not x:
831 831 return 0
832 832 i = 0
833 833 while x:
834 834 x >>= 1
835 835 i += 1
836 836 return i - 1
837 837
838 838 buf = []
839 839 blen = 0
840 840 for chunk in source:
841 841 buf.append(chunk)
842 842 blen += len(chunk)
843 843 if blen >= min:
844 844 if min < max:
845 845 min = min << 1
846 846 nmin = 1 << log2(blen)
847 847 if nmin > min:
848 848 min = nmin
849 849 if min > max:
850 850 min = max
851 851 yield ''.join(buf)
852 852 blen = 0
853 853 buf = []
854 854 if buf:
855 855 yield ''.join(buf)
856 856
857 857 Abort = error.Abort
858 858
859 859 def always(fn):
860 860 return True
861 861
862 862 def never(fn):
863 863 return False
864 864
865 865 def nogc(func):
866 866 """disable garbage collector
867 867
868 868 Python's garbage collector triggers a GC each time a certain number of
869 869 container objects (the number being defined by gc.get_threshold()) are
870 870 allocated even when marked not to be tracked by the collector. Tracking has
871 871 no effect on when GCs are triggered, only on what objects the GC looks
872 872 into. As a workaround, disable GC while building complex (huge)
873 873 containers.
874 874
875 875 This garbage collector issue have been fixed in 2.7.
876 876 """
877 877 def wrapper(*args, **kwargs):
878 878 gcenabled = gc.isenabled()
879 879 gc.disable()
880 880 try:
881 881 return func(*args, **kwargs)
882 882 finally:
883 883 if gcenabled:
884 884 gc.enable()
885 885 return wrapper
886 886
887 887 def pathto(root, n1, n2):
888 888 '''return the relative path from one place to another.
889 889 root should use os.sep to separate directories
890 890 n1 should use os.sep to separate directories
891 891 n2 should use "/" to separate directories
892 892 returns an os.sep-separated path.
893 893
894 894 If n1 is a relative path, it's assumed it's
895 895 relative to root.
896 896 n2 should always be relative to root.
897 897 '''
898 898 if not n1:
899 899 return localpath(n2)
900 900 if os.path.isabs(n1):
901 901 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
902 902 return os.path.join(root, localpath(n2))
903 903 n2 = '/'.join((pconvert(root), n2))
904 904 a, b = splitpath(n1), n2.split('/')
905 905 a.reverse()
906 906 b.reverse()
907 907 while a and b and a[-1] == b[-1]:
908 908 a.pop()
909 909 b.pop()
910 910 b.reverse()
911 911 return os.sep.join((['..'] * len(a)) + b) or '.'
912 912
913 913 def mainfrozen():
914 914 """return True if we are a frozen executable.
915 915
916 916 The code supports py2exe (most common, Windows only) and tools/freeze
917 917 (portable, not much used).
918 918 """
919 919 return (safehasattr(sys, "frozen") or # new py2exe
920 920 safehasattr(sys, "importers") or # old py2exe
921 921 imp.is_frozen("__main__")) # tools/freeze
922 922
923 923 # the location of data files matching the source code
924 924 if mainfrozen() and getattr(sys, 'frozen', None) != 'macosx_app':
925 925 # executable version (py2exe) doesn't support __file__
926 926 datapath = os.path.dirname(sys.executable)
927 927 else:
928 928 datapath = os.path.dirname(__file__)
929 929
930 930 i18n.setdatapath(datapath)
931 931
932 932 _hgexecutable = None
933 933
934 934 def hgexecutable():
935 935 """return location of the 'hg' executable.
936 936
937 937 Defaults to $HG or 'hg' in the search path.
938 938 """
939 939 if _hgexecutable is None:
940 940 hg = os.environ.get('HG')
941 941 mainmod = sys.modules['__main__']
942 942 if hg:
943 943 _sethgexecutable(hg)
944 944 elif mainfrozen():
945 945 if getattr(sys, 'frozen', None) == 'macosx_app':
946 946 # Env variable set by py2app
947 947 _sethgexecutable(os.environ['EXECUTABLEPATH'])
948 948 else:
949 949 _sethgexecutable(sys.executable)
950 950 elif os.path.basename(getattr(mainmod, '__file__', '')) == 'hg':
951 951 _sethgexecutable(mainmod.__file__)
952 952 else:
953 953 exe = findexe('hg') or os.path.basename(sys.argv[0])
954 954 _sethgexecutable(exe)
955 955 return _hgexecutable
956 956
957 957 def _sethgexecutable(path):
958 958 """set location of the 'hg' executable"""
959 959 global _hgexecutable
960 960 _hgexecutable = path
961 961
962 962 def _isstdout(f):
963 963 fileno = getattr(f, 'fileno', None)
964 964 return fileno and fileno() == sys.__stdout__.fileno()
965 965
966 966 def system(cmd, environ=None, cwd=None, onerr=None, errprefix=None, out=None):
967 967 '''enhanced shell command execution.
968 968 run with environment maybe modified, maybe in different dir.
969 969
970 970 if command fails and onerr is None, return status, else raise onerr
971 971 object as exception.
972 972
973 973 if out is specified, it is assumed to be a file-like object that has a
974 974 write() method. stdout and stderr will be redirected to out.'''
975 975 if environ is None:
976 976 environ = {}
977 977 try:
978 978 sys.stdout.flush()
979 979 except Exception:
980 980 pass
981 981 def py2shell(val):
982 982 'convert python object into string that is useful to shell'
983 983 if val is None or val is False:
984 984 return '0'
985 985 if val is True:
986 986 return '1'
987 987 return str(val)
988 988 origcmd = cmd
989 989 cmd = quotecommand(cmd)
990 990 if sys.platform == 'plan9' and (sys.version_info[0] == 2
991 991 and sys.version_info[1] < 7):
992 992 # subprocess kludge to work around issues in half-baked Python
993 993 # ports, notably bichued/python:
994 994 if not cwd is None:
995 995 os.chdir(cwd)
996 996 rc = os.system(cmd)
997 997 else:
998 998 env = dict(os.environ)
999 999 env.update((k, py2shell(v)) for k, v in environ.iteritems())
1000 1000 env['HG'] = hgexecutable()
1001 1001 if out is None or _isstdout(out):
1002 1002 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
1003 1003 env=env, cwd=cwd)
1004 1004 else:
1005 1005 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
1006 1006 env=env, cwd=cwd, stdout=subprocess.PIPE,
1007 1007 stderr=subprocess.STDOUT)
1008 1008 while True:
1009 1009 line = proc.stdout.readline()
1010 1010 if not line:
1011 1011 break
1012 1012 out.write(line)
1013 1013 proc.wait()
1014 1014 rc = proc.returncode
1015 1015 if sys.platform == 'OpenVMS' and rc & 1:
1016 1016 rc = 0
1017 1017 if rc and onerr:
1018 1018 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
1019 1019 explainexit(rc)[0])
1020 1020 if errprefix:
1021 1021 errmsg = '%s: %s' % (errprefix, errmsg)
1022 1022 raise onerr(errmsg)
1023 1023 return rc
1024 1024
1025 1025 def checksignature(func):
1026 1026 '''wrap a function with code to check for calling errors'''
1027 1027 def check(*args, **kwargs):
1028 1028 try:
1029 1029 return func(*args, **kwargs)
1030 1030 except TypeError:
1031 1031 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
1032 1032 raise error.SignatureError
1033 1033 raise
1034 1034
1035 1035 return check
1036 1036
1037 1037 def copyfile(src, dest, hardlink=False, copystat=False, checkambig=False):
1038 1038 '''copy a file, preserving mode and optionally other stat info like
1039 1039 atime/mtime
1040 1040
1041 1041 checkambig argument is used with filestat, and is useful only if
1042 1042 destination file is guarded by any lock (e.g. repo.lock or
1043 1043 repo.wlock).
1044 1044
1045 1045 copystat and checkambig should be exclusive.
1046 1046 '''
1047 1047 assert not (copystat and checkambig)
1048 1048 oldstat = None
1049 1049 if os.path.lexists(dest):
1050 1050 if checkambig:
1051 1051 oldstat = checkambig and filestat(dest)
1052 1052 unlink(dest)
1053 1053 # hardlinks are problematic on CIFS, quietly ignore this flag
1054 1054 # until we find a way to work around it cleanly (issue4546)
1055 1055 if False and hardlink:
1056 1056 try:
1057 1057 oslink(src, dest)
1058 1058 return
1059 1059 except (IOError, OSError):
1060 1060 pass # fall back to normal copy
1061 1061 if os.path.islink(src):
1062 1062 os.symlink(os.readlink(src), dest)
1063 1063 # copytime is ignored for symlinks, but in general copytime isn't needed
1064 1064 # for them anyway
1065 1065 else:
1066 1066 try:
1067 1067 shutil.copyfile(src, dest)
1068 1068 if copystat:
1069 1069 # copystat also copies mode
1070 1070 shutil.copystat(src, dest)
1071 1071 else:
1072 1072 shutil.copymode(src, dest)
1073 1073 if oldstat and oldstat.stat:
1074 1074 newstat = filestat(dest)
1075 1075 if newstat.isambig(oldstat):
1076 1076 # stat of copied file is ambiguous to original one
1077 1077 advanced = (oldstat.stat.st_mtime + 1) & 0x7fffffff
1078 1078 os.utime(dest, (advanced, advanced))
1079 1079 except shutil.Error as inst:
1080 1080 raise Abort(str(inst))
1081 1081
1082 1082 def copyfiles(src, dst, hardlink=None, progress=lambda t, pos: None):
1083 1083 """Copy a directory tree using hardlinks if possible."""
1084 1084 num = 0
1085 1085
1086 1086 if hardlink is None:
1087 1087 hardlink = (os.stat(src).st_dev ==
1088 1088 os.stat(os.path.dirname(dst)).st_dev)
1089 1089 if hardlink:
1090 1090 topic = _('linking')
1091 1091 else:
1092 1092 topic = _('copying')
1093 1093
1094 1094 if os.path.isdir(src):
1095 1095 os.mkdir(dst)
1096 1096 for name, kind in osutil.listdir(src):
1097 1097 srcname = os.path.join(src, name)
1098 1098 dstname = os.path.join(dst, name)
1099 1099 def nprog(t, pos):
1100 1100 if pos is not None:
1101 1101 return progress(t, pos + num)
1102 1102 hardlink, n = copyfiles(srcname, dstname, hardlink, progress=nprog)
1103 1103 num += n
1104 1104 else:
1105 1105 if hardlink:
1106 1106 try:
1107 1107 oslink(src, dst)
1108 1108 except (IOError, OSError):
1109 1109 hardlink = False
1110 1110 shutil.copy(src, dst)
1111 1111 else:
1112 1112 shutil.copy(src, dst)
1113 1113 num += 1
1114 1114 progress(topic, num)
1115 1115 progress(topic, None)
1116 1116
1117 1117 return hardlink, num
1118 1118
1119 1119 _winreservednames = '''con prn aux nul
1120 1120 com1 com2 com3 com4 com5 com6 com7 com8 com9
1121 1121 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split()
1122 1122 _winreservedchars = ':*?"<>|'
1123 1123 def checkwinfilename(path):
1124 1124 r'''Check that the base-relative path is a valid filename on Windows.
1125 1125 Returns None if the path is ok, or a UI string describing the problem.
1126 1126
1127 1127 >>> checkwinfilename("just/a/normal/path")
1128 1128 >>> checkwinfilename("foo/bar/con.xml")
1129 1129 "filename contains 'con', which is reserved on Windows"
1130 1130 >>> checkwinfilename("foo/con.xml/bar")
1131 1131 "filename contains 'con', which is reserved on Windows"
1132 1132 >>> checkwinfilename("foo/bar/xml.con")
1133 1133 >>> checkwinfilename("foo/bar/AUX/bla.txt")
1134 1134 "filename contains 'AUX', which is reserved on Windows"
1135 1135 >>> checkwinfilename("foo/bar/bla:.txt")
1136 1136 "filename contains ':', which is reserved on Windows"
1137 1137 >>> checkwinfilename("foo/bar/b\07la.txt")
1138 1138 "filename contains '\\x07', which is invalid on Windows"
1139 1139 >>> checkwinfilename("foo/bar/bla ")
1140 1140 "filename ends with ' ', which is not allowed on Windows"
1141 1141 >>> checkwinfilename("../bar")
1142 1142 >>> checkwinfilename("foo\\")
1143 1143 "filename ends with '\\', which is invalid on Windows"
1144 1144 >>> checkwinfilename("foo\\/bar")
1145 1145 "directory name ends with '\\', which is invalid on Windows"
1146 1146 '''
1147 1147 if path.endswith('\\'):
1148 1148 return _("filename ends with '\\', which is invalid on Windows")
1149 1149 if '\\/' in path:
1150 1150 return _("directory name ends with '\\', which is invalid on Windows")
1151 1151 for n in path.replace('\\', '/').split('/'):
1152 1152 if not n:
1153 1153 continue
1154 1154 for c in n:
1155 1155 if c in _winreservedchars:
1156 1156 return _("filename contains '%s', which is reserved "
1157 1157 "on Windows") % c
1158 1158 if ord(c) <= 31:
1159 1159 return _("filename contains %r, which is invalid "
1160 1160 "on Windows") % c
1161 1161 base = n.split('.')[0]
1162 1162 if base and base.lower() in _winreservednames:
1163 1163 return _("filename contains '%s', which is reserved "
1164 1164 "on Windows") % base
1165 1165 t = n[-1]
1166 1166 if t in '. ' and n not in '..':
1167 1167 return _("filename ends with '%s', which is not allowed "
1168 1168 "on Windows") % t
1169 1169
1170 1170 if os.name == 'nt':
1171 1171 checkosfilename = checkwinfilename
1172 1172 else:
1173 1173 checkosfilename = platform.checkosfilename
1174 1174
1175 1175 def makelock(info, pathname):
1176 1176 try:
1177 1177 return os.symlink(info, pathname)
1178 1178 except OSError as why:
1179 1179 if why.errno == errno.EEXIST:
1180 1180 raise
1181 1181 except AttributeError: # no symlink in os
1182 1182 pass
1183 1183
1184 1184 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
1185 1185 os.write(ld, info)
1186 1186 os.close(ld)
1187 1187
1188 1188 def readlock(pathname):
1189 1189 try:
1190 1190 return os.readlink(pathname)
1191 1191 except OSError as why:
1192 1192 if why.errno not in (errno.EINVAL, errno.ENOSYS):
1193 1193 raise
1194 1194 except AttributeError: # no symlink in os
1195 1195 pass
1196 1196 fp = posixfile(pathname)
1197 1197 r = fp.read()
1198 1198 fp.close()
1199 1199 return r
1200 1200
1201 1201 def fstat(fp):
1202 1202 '''stat file object that may not have fileno method.'''
1203 1203 try:
1204 1204 return os.fstat(fp.fileno())
1205 1205 except AttributeError:
1206 1206 return os.stat(fp.name)
1207 1207
1208 1208 # File system features
1209 1209
1210 1210 def checkcase(path):
1211 1211 """
1212 1212 Return true if the given path is on a case-sensitive filesystem
1213 1213
1214 1214 Requires a path (like /foo/.hg) ending with a foldable final
1215 1215 directory component.
1216 1216 """
1217 1217 s1 = os.lstat(path)
1218 1218 d, b = os.path.split(path)
1219 1219 b2 = b.upper()
1220 1220 if b == b2:
1221 1221 b2 = b.lower()
1222 1222 if b == b2:
1223 1223 return True # no evidence against case sensitivity
1224 1224 p2 = os.path.join(d, b2)
1225 1225 try:
1226 1226 s2 = os.lstat(p2)
1227 1227 if s2 == s1:
1228 1228 return False
1229 1229 return True
1230 1230 except OSError:
1231 1231 return True
1232 1232
1233 1233 try:
1234 1234 import re2
1235 1235 _re2 = None
1236 1236 except ImportError:
1237 1237 _re2 = False
1238 1238
1239 1239 class _re(object):
1240 1240 def _checkre2(self):
1241 1241 global _re2
1242 1242 try:
1243 1243 # check if match works, see issue3964
1244 1244 _re2 = bool(re2.match(r'\[([^\[]+)\]', '[ui]'))
1245 1245 except ImportError:
1246 1246 _re2 = False
1247 1247
1248 1248 def compile(self, pat, flags=0):
1249 1249 '''Compile a regular expression, using re2 if possible
1250 1250
1251 1251 For best performance, use only re2-compatible regexp features. The
1252 1252 only flags from the re module that are re2-compatible are
1253 1253 IGNORECASE and MULTILINE.'''
1254 1254 if _re2 is None:
1255 1255 self._checkre2()
1256 1256 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
1257 1257 if flags & remod.IGNORECASE:
1258 1258 pat = '(?i)' + pat
1259 1259 if flags & remod.MULTILINE:
1260 1260 pat = '(?m)' + pat
1261 1261 try:
1262 1262 return re2.compile(pat)
1263 1263 except re2.error:
1264 1264 pass
1265 1265 return remod.compile(pat, flags)
1266 1266
1267 1267 @propertycache
1268 1268 def escape(self):
1269 1269 '''Return the version of escape corresponding to self.compile.
1270 1270
1271 1271 This is imperfect because whether re2 or re is used for a particular
1272 1272 function depends on the flags, etc, but it's the best we can do.
1273 1273 '''
1274 1274 global _re2
1275 1275 if _re2 is None:
1276 1276 self._checkre2()
1277 1277 if _re2:
1278 1278 return re2.escape
1279 1279 else:
1280 1280 return remod.escape
1281 1281
1282 1282 re = _re()
1283 1283
1284 1284 _fspathcache = {}
1285 1285 def fspath(name, root):
1286 1286 '''Get name in the case stored in the filesystem
1287 1287
1288 1288 The name should be relative to root, and be normcase-ed for efficiency.
1289 1289
1290 1290 Note that this function is unnecessary, and should not be
1291 1291 called, for case-sensitive filesystems (simply because it's expensive).
1292 1292
1293 1293 The root should be normcase-ed, too.
1294 1294 '''
1295 1295 def _makefspathcacheentry(dir):
1296 1296 return dict((normcase(n), n) for n in os.listdir(dir))
1297 1297
1298 1298 seps = os.sep
1299 1299 if os.altsep:
1300 1300 seps = seps + os.altsep
1301 1301 # Protect backslashes. This gets silly very quickly.
1302 1302 seps.replace('\\','\\\\')
1303 1303 pattern = remod.compile(r'([^%s]+)|([%s]+)' % (seps, seps))
1304 1304 dir = os.path.normpath(root)
1305 1305 result = []
1306 1306 for part, sep in pattern.findall(name):
1307 1307 if sep:
1308 1308 result.append(sep)
1309 1309 continue
1310 1310
1311 1311 if dir not in _fspathcache:
1312 1312 _fspathcache[dir] = _makefspathcacheentry(dir)
1313 1313 contents = _fspathcache[dir]
1314 1314
1315 1315 found = contents.get(part)
1316 1316 if not found:
1317 1317 # retry "once per directory" per "dirstate.walk" which
1318 1318 # may take place for each patches of "hg qpush", for example
1319 1319 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
1320 1320 found = contents.get(part)
1321 1321
1322 1322 result.append(found or part)
1323 1323 dir = os.path.join(dir, part)
1324 1324
1325 1325 return ''.join(result)
1326 1326
1327 1327 def checknlink(testfile):
1328 1328 '''check whether hardlink count reporting works properly'''
1329 1329
1330 1330 # testfile may be open, so we need a separate file for checking to
1331 1331 # work around issue2543 (or testfile may get lost on Samba shares)
1332 1332 f1 = testfile + ".hgtmp1"
1333 1333 if os.path.lexists(f1):
1334 1334 return False
1335 1335 try:
1336 1336 posixfile(f1, 'w').close()
1337 1337 except IOError:
1338 1338 return False
1339 1339
1340 1340 f2 = testfile + ".hgtmp2"
1341 1341 fd = None
1342 1342 try:
1343 1343 oslink(f1, f2)
1344 1344 # nlinks() may behave differently for files on Windows shares if
1345 1345 # the file is open.
1346 1346 fd = posixfile(f2)
1347 1347 return nlinks(f2) > 1
1348 1348 except OSError:
1349 1349 return False
1350 1350 finally:
1351 1351 if fd is not None:
1352 1352 fd.close()
1353 1353 for f in (f1, f2):
1354 1354 try:
1355 1355 os.unlink(f)
1356 1356 except OSError:
1357 1357 pass
1358 1358
1359 1359 def endswithsep(path):
1360 1360 '''Check path ends with os.sep or os.altsep.'''
1361 1361 return path.endswith(os.sep) or os.altsep and path.endswith(os.altsep)
1362 1362
1363 1363 def splitpath(path):
1364 1364 '''Split path by os.sep.
1365 1365 Note that this function does not use os.altsep because this is
1366 1366 an alternative of simple "xxx.split(os.sep)".
1367 1367 It is recommended to use os.path.normpath() before using this
1368 1368 function if need.'''
1369 1369 return path.split(os.sep)
1370 1370
1371 1371 def gui():
1372 1372 '''Are we running in a GUI?'''
1373 1373 if sys.platform == 'darwin':
1374 1374 if 'SSH_CONNECTION' in os.environ:
1375 1375 # handle SSH access to a box where the user is logged in
1376 1376 return False
1377 1377 elif getattr(osutil, 'isgui', None):
1378 1378 # check if a CoreGraphics session is available
1379 1379 return osutil.isgui()
1380 1380 else:
1381 1381 # pure build; use a safe default
1382 1382 return True
1383 1383 else:
1384 1384 return os.name == "nt" or os.environ.get("DISPLAY")
1385 1385
1386 1386 def mktempcopy(name, emptyok=False, createmode=None):
1387 1387 """Create a temporary file with the same contents from name
1388 1388
1389 1389 The permission bits are copied from the original file.
1390 1390
1391 1391 If the temporary file is going to be truncated immediately, you
1392 1392 can use emptyok=True as an optimization.
1393 1393
1394 1394 Returns the name of the temporary file.
1395 1395 """
1396 1396 d, fn = os.path.split(name)
1397 1397 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1398 1398 os.close(fd)
1399 1399 # Temporary files are created with mode 0600, which is usually not
1400 1400 # what we want. If the original file already exists, just copy
1401 1401 # its mode. Otherwise, manually obey umask.
1402 1402 copymode(name, temp, createmode)
1403 1403 if emptyok:
1404 1404 return temp
1405 1405 try:
1406 1406 try:
1407 1407 ifp = posixfile(name, "rb")
1408 1408 except IOError as inst:
1409 1409 if inst.errno == errno.ENOENT:
1410 1410 return temp
1411 1411 if not getattr(inst, 'filename', None):
1412 1412 inst.filename = name
1413 1413 raise
1414 1414 ofp = posixfile(temp, "wb")
1415 1415 for chunk in filechunkiter(ifp):
1416 1416 ofp.write(chunk)
1417 1417 ifp.close()
1418 1418 ofp.close()
1419 1419 except: # re-raises
1420 1420 try: os.unlink(temp)
1421 1421 except OSError: pass
1422 1422 raise
1423 1423 return temp
1424 1424
1425 1425 class filestat(object):
1426 1426 """help to exactly detect change of a file
1427 1427
1428 1428 'stat' attribute is result of 'os.stat()' if specified 'path'
1429 1429 exists. Otherwise, it is None. This can avoid preparative
1430 1430 'exists()' examination on client side of this class.
1431 1431 """
1432 1432 def __init__(self, path):
1433 1433 try:
1434 1434 self.stat = os.stat(path)
1435 1435 except OSError as err:
1436 1436 if err.errno != errno.ENOENT:
1437 1437 raise
1438 1438 self.stat = None
1439 1439
1440 1440 __hash__ = object.__hash__
1441 1441
1442 1442 def __eq__(self, old):
1443 1443 try:
1444 1444 # if ambiguity between stat of new and old file is
1445 1445 # avoided, comparision of size, ctime and mtime is enough
1446 1446 # to exactly detect change of a file regardless of platform
1447 1447 return (self.stat.st_size == old.stat.st_size and
1448 1448 self.stat.st_ctime == old.stat.st_ctime and
1449 1449 self.stat.st_mtime == old.stat.st_mtime)
1450 1450 except AttributeError:
1451 1451 return False
1452 1452
1453 1453 def isambig(self, old):
1454 1454 """Examine whether new (= self) stat is ambiguous against old one
1455 1455
1456 1456 "S[N]" below means stat of a file at N-th change:
1457 1457
1458 1458 - S[n-1].ctime < S[n].ctime: can detect change of a file
1459 1459 - S[n-1].ctime == S[n].ctime
1460 1460 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
1461 1461 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
1462 1462 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
1463 1463 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
1464 1464
1465 1465 Case (*2) above means that a file was changed twice or more at
1466 1466 same time in sec (= S[n-1].ctime), and comparison of timestamp
1467 1467 is ambiguous.
1468 1468
1469 1469 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
1470 1470 timestamp is ambiguous".
1471 1471
1472 1472 But advancing mtime only in case (*2) doesn't work as
1473 1473 expected, because naturally advanced S[n].mtime in case (*1)
1474 1474 might be equal to manually advanced S[n-1 or earlier].mtime.
1475 1475
1476 1476 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
1477 1477 treated as ambiguous regardless of mtime, to avoid overlooking
1478 1478 by confliction between such mtime.
1479 1479
1480 1480 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
1481 1481 S[n].mtime", even if size of a file isn't changed.
1482 1482 """
1483 1483 try:
1484 1484 return (self.stat.st_ctime == old.stat.st_ctime)
1485 1485 except AttributeError:
1486 1486 return False
1487 1487
1488 1488 def __ne__(self, other):
1489 1489 return not self == other
1490 1490
1491 1491 class atomictempfile(object):
1492 1492 '''writable file object that atomically updates a file
1493 1493
1494 1494 All writes will go to a temporary copy of the original file. Call
1495 1495 close() when you are done writing, and atomictempfile will rename
1496 1496 the temporary copy to the original name, making the changes
1497 1497 visible. If the object is destroyed without being closed, all your
1498 1498 writes are discarded.
1499 1499
1500 1500 checkambig argument of constructor is used with filestat, and is
1501 1501 useful only if target file is guarded by any lock (e.g. repo.lock
1502 1502 or repo.wlock).
1503 1503 '''
1504 1504 def __init__(self, name, mode='w+b', createmode=None, checkambig=False):
1505 1505 self.__name = name # permanent name
1506 1506 self._tempname = mktempcopy(name, emptyok=('w' in mode),
1507 1507 createmode=createmode)
1508 1508 self._fp = posixfile(self._tempname, mode)
1509 1509 self._checkambig = checkambig
1510 1510
1511 1511 # delegated methods
1512 1512 self.read = self._fp.read
1513 1513 self.write = self._fp.write
1514 1514 self.seek = self._fp.seek
1515 1515 self.tell = self._fp.tell
1516 1516 self.fileno = self._fp.fileno
1517 1517
1518 1518 def close(self):
1519 1519 if not self._fp.closed:
1520 1520 self._fp.close()
1521 1521 filename = localpath(self.__name)
1522 1522 oldstat = self._checkambig and filestat(filename)
1523 1523 if oldstat and oldstat.stat:
1524 1524 rename(self._tempname, filename)
1525 1525 newstat = filestat(filename)
1526 1526 if newstat.isambig(oldstat):
1527 1527 # stat of changed file is ambiguous to original one
1528 1528 advanced = (oldstat.stat.st_mtime + 1) & 0x7fffffff
1529 1529 os.utime(filename, (advanced, advanced))
1530 1530 else:
1531 1531 rename(self._tempname, filename)
1532 1532
1533 1533 def discard(self):
1534 1534 if not self._fp.closed:
1535 1535 try:
1536 1536 os.unlink(self._tempname)
1537 1537 except OSError:
1538 1538 pass
1539 1539 self._fp.close()
1540 1540
1541 1541 def __del__(self):
1542 1542 if safehasattr(self, '_fp'): # constructor actually did something
1543 1543 self.discard()
1544 1544
1545 1545 def __enter__(self):
1546 1546 return self
1547 1547
1548 1548 def __exit__(self, exctype, excvalue, traceback):
1549 1549 if exctype is not None:
1550 1550 self.discard()
1551 1551 else:
1552 1552 self.close()
1553 1553
1554 1554 def makedirs(name, mode=None, notindexed=False):
1555 1555 """recursive directory creation with parent mode inheritance
1556 1556
1557 1557 Newly created directories are marked as "not to be indexed by
1558 1558 the content indexing service", if ``notindexed`` is specified
1559 1559 for "write" mode access.
1560 1560 """
1561 1561 try:
1562 1562 makedir(name, notindexed)
1563 1563 except OSError as err:
1564 1564 if err.errno == errno.EEXIST:
1565 1565 return
1566 1566 if err.errno != errno.ENOENT or not name:
1567 1567 raise
1568 1568 parent = os.path.dirname(os.path.abspath(name))
1569 1569 if parent == name:
1570 1570 raise
1571 1571 makedirs(parent, mode, notindexed)
1572 1572 try:
1573 1573 makedir(name, notindexed)
1574 1574 except OSError as err:
1575 1575 # Catch EEXIST to handle races
1576 1576 if err.errno == errno.EEXIST:
1577 1577 return
1578 1578 raise
1579 1579 if mode is not None:
1580 1580 os.chmod(name, mode)
1581 1581
1582 1582 def readfile(path):
1583 1583 with open(path, 'rb') as fp:
1584 1584 return fp.read()
1585 1585
1586 1586 def writefile(path, text):
1587 1587 with open(path, 'wb') as fp:
1588 1588 fp.write(text)
1589 1589
1590 1590 def appendfile(path, text):
1591 1591 with open(path, 'ab') as fp:
1592 1592 fp.write(text)
1593 1593
1594 1594 class chunkbuffer(object):
1595 1595 """Allow arbitrary sized chunks of data to be efficiently read from an
1596 1596 iterator over chunks of arbitrary size."""
1597 1597
1598 1598 def __init__(self, in_iter):
1599 1599 """in_iter is the iterator that's iterating over the input chunks.
1600 1600 targetsize is how big a buffer to try to maintain."""
1601 1601 def splitbig(chunks):
1602 1602 for chunk in chunks:
1603 1603 if len(chunk) > 2**20:
1604 1604 pos = 0
1605 1605 while pos < len(chunk):
1606 1606 end = pos + 2 ** 18
1607 1607 yield chunk[pos:end]
1608 1608 pos = end
1609 1609 else:
1610 1610 yield chunk
1611 1611 self.iter = splitbig(in_iter)
1612 1612 self._queue = collections.deque()
1613 1613 self._chunkoffset = 0
1614 1614
1615 1615 def read(self, l=None):
1616 1616 """Read L bytes of data from the iterator of chunks of data.
1617 1617 Returns less than L bytes if the iterator runs dry.
1618 1618
1619 1619 If size parameter is omitted, read everything"""
1620 1620 if l is None:
1621 1621 return ''.join(self.iter)
1622 1622
1623 1623 left = l
1624 1624 buf = []
1625 1625 queue = self._queue
1626 1626 while left > 0:
1627 1627 # refill the queue
1628 1628 if not queue:
1629 1629 target = 2**18
1630 1630 for chunk in self.iter:
1631 1631 queue.append(chunk)
1632 1632 target -= len(chunk)
1633 1633 if target <= 0:
1634 1634 break
1635 1635 if not queue:
1636 1636 break
1637 1637
1638 1638 # The easy way to do this would be to queue.popleft(), modify the
1639 1639 # chunk (if necessary), then queue.appendleft(). However, for cases
1640 1640 # where we read partial chunk content, this incurs 2 dequeue
1641 1641 # mutations and creates a new str for the remaining chunk in the
1642 1642 # queue. Our code below avoids this overhead.
1643 1643
1644 1644 chunk = queue[0]
1645 1645 chunkl = len(chunk)
1646 1646 offset = self._chunkoffset
1647 1647
1648 1648 # Use full chunk.
1649 1649 if offset == 0 and left >= chunkl:
1650 1650 left -= chunkl
1651 1651 queue.popleft()
1652 1652 buf.append(chunk)
1653 1653 # self._chunkoffset remains at 0.
1654 1654 continue
1655 1655
1656 1656 chunkremaining = chunkl - offset
1657 1657
1658 1658 # Use all of unconsumed part of chunk.
1659 1659 if left >= chunkremaining:
1660 1660 left -= chunkremaining
1661 1661 queue.popleft()
1662 1662 # offset == 0 is enabled by block above, so this won't merely
1663 1663 # copy via ``chunk[0:]``.
1664 1664 buf.append(chunk[offset:])
1665 1665 self._chunkoffset = 0
1666 1666
1667 1667 # Partial chunk needed.
1668 1668 else:
1669 1669 buf.append(chunk[offset:offset + left])
1670 1670 self._chunkoffset += left
1671 1671 left -= chunkremaining
1672 1672
1673 1673 return ''.join(buf)
1674 1674
1675 1675 def filechunkiter(f, size=65536, limit=None):
1676 1676 """Create a generator that produces the data in the file size
1677 1677 (default 65536) bytes at a time, up to optional limit (default is
1678 1678 to read all data). Chunks may be less than size bytes if the
1679 1679 chunk is the last chunk in the file, or the file is a socket or
1680 1680 some other type of file that sometimes reads less data than is
1681 1681 requested."""
1682 1682 assert size >= 0
1683 1683 assert limit is None or limit >= 0
1684 1684 while True:
1685 1685 if limit is None:
1686 1686 nbytes = size
1687 1687 else:
1688 1688 nbytes = min(limit, size)
1689 1689 s = nbytes and f.read(nbytes)
1690 1690 if not s:
1691 1691 break
1692 1692 if limit:
1693 1693 limit -= len(s)
1694 1694 yield s
1695 1695
1696 1696 def makedate(timestamp=None):
1697 1697 '''Return a unix timestamp (or the current time) as a (unixtime,
1698 1698 offset) tuple based off the local timezone.'''
1699 1699 if timestamp is None:
1700 1700 timestamp = time.time()
1701 1701 if timestamp < 0:
1702 1702 hint = _("check your clock")
1703 1703 raise Abort(_("negative timestamp: %d") % timestamp, hint=hint)
1704 1704 delta = (datetime.datetime.utcfromtimestamp(timestamp) -
1705 1705 datetime.datetime.fromtimestamp(timestamp))
1706 1706 tz = delta.days * 86400 + delta.seconds
1707 1707 return timestamp, tz
1708 1708
1709 1709 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
1710 1710 """represent a (unixtime, offset) tuple as a localized time.
1711 1711 unixtime is seconds since the epoch, and offset is the time zone's
1712 1712 number of seconds away from UTC.
1713 1713
1714 1714 >>> datestr((0, 0))
1715 1715 'Thu Jan 01 00:00:00 1970 +0000'
1716 1716 >>> datestr((42, 0))
1717 1717 'Thu Jan 01 00:00:42 1970 +0000'
1718 1718 >>> datestr((-42, 0))
1719 1719 'Wed Dec 31 23:59:18 1969 +0000'
1720 1720 >>> datestr((0x7fffffff, 0))
1721 1721 'Tue Jan 19 03:14:07 2038 +0000'
1722 1722 >>> datestr((-0x80000000, 0))
1723 1723 'Fri Dec 13 20:45:52 1901 +0000'
1724 1724 """
1725 1725 t, tz = date or makedate()
1726 1726 if "%1" in format or "%2" in format or "%z" in format:
1727 1727 sign = (tz > 0) and "-" or "+"
1728 1728 minutes = abs(tz) // 60
1729 1729 q, r = divmod(minutes, 60)
1730 1730 format = format.replace("%z", "%1%2")
1731 1731 format = format.replace("%1", "%c%02d" % (sign, q))
1732 1732 format = format.replace("%2", "%02d" % r)
1733 1733 d = t - tz
1734 1734 if d > 0x7fffffff:
1735 1735 d = 0x7fffffff
1736 1736 elif d < -0x80000000:
1737 1737 d = -0x80000000
1738 1738 # Never use time.gmtime() and datetime.datetime.fromtimestamp()
1739 1739 # because they use the gmtime() system call which is buggy on Windows
1740 1740 # for negative values.
1741 1741 t = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=d)
1742 1742 s = t.strftime(format)
1743 1743 return s
1744 1744
1745 1745 def shortdate(date=None):
1746 1746 """turn (timestamp, tzoff) tuple into iso 8631 date."""
1747 1747 return datestr(date, format='%Y-%m-%d')
1748 1748
1749 def parsetimezone(tz):
1750 """parse a timezone string and return an offset integer"""
1751 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1752 sign = (tz[0] == "+") and 1 or -1
1753 hours = int(tz[1:3])
1754 minutes = int(tz[3:5])
1755 return -sign * (hours * 60 + minutes) * 60
1756 if tz == "GMT" or tz == "UTC":
1757 return 0
1758 return None
1749 def parsetimezone(s):
1750 """find a trailing timezone, if any, in string, and return a
1751 (offset, remainder) pair"""
1752
1753 if s.endswith("GMT") or s.endswith("UTC"):
1754 return 0, s[:-3].rstrip()
1755
1756 # Unix-style timezones [+-]hhmm
1757 if len(s) >= 5 and s[-5] in "+-" and s[-4:].isdigit():
1758 sign = (s[-5] == "+") and 1 or -1
1759 hours = int(s[-4:-2])
1760 minutes = int(s[-2:])
1761 return -sign * (hours * 60 + minutes) * 60, s[:-5].rstrip()
1762
1763 return None, s
1759 1764
1760 1765 def strdate(string, format, defaults=[]):
1761 1766 """parse a localized time string and return a (unixtime, offset) tuple.
1762 1767 if the string cannot be parsed, ValueError is raised."""
1763 1768 # NOTE: unixtime = localunixtime + offset
1764 offset, date = parsetimezone(string.split()[-1]), string
1765 if offset is not None:
1766 date = " ".join(string.split()[:-1])
1769 offset, date = parsetimezone(string)
1767 1770
1768 1771 # add missing elements from defaults
1769 1772 usenow = False # default to using biased defaults
1770 1773 for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity
1771 1774 found = [True for p in part if ("%"+p) in format]
1772 1775 if not found:
1773 1776 date += "@" + defaults[part][usenow]
1774 1777 format += "@%" + part[0]
1775 1778 else:
1776 1779 # We've found a specific time element, less specific time
1777 1780 # elements are relative to today
1778 1781 usenow = True
1779 1782
1780 1783 timetuple = time.strptime(date, format)
1781 1784 localunixtime = int(calendar.timegm(timetuple))
1782 1785 if offset is None:
1783 1786 # local timezone
1784 1787 unixtime = int(time.mktime(timetuple))
1785 1788 offset = unixtime - localunixtime
1786 1789 else:
1787 1790 unixtime = localunixtime + offset
1788 1791 return unixtime, offset
1789 1792
1790 1793 def parsedate(date, formats=None, bias=None):
1791 1794 """parse a localized date/time and return a (unixtime, offset) tuple.
1792 1795
1793 1796 The date may be a "unixtime offset" string or in one of the specified
1794 1797 formats. If the date already is a (unixtime, offset) tuple, it is returned.
1795 1798
1796 1799 >>> parsedate(' today ') == parsedate(\
1797 1800 datetime.date.today().strftime('%b %d'))
1798 1801 True
1799 1802 >>> parsedate( 'yesterday ') == parsedate((datetime.date.today() -\
1800 1803 datetime.timedelta(days=1)\
1801 1804 ).strftime('%b %d'))
1802 1805 True
1803 1806 >>> now, tz = makedate()
1804 1807 >>> strnow, strtz = parsedate('now')
1805 1808 >>> (strnow - now) < 1
1806 1809 True
1807 1810 >>> tz == strtz
1808 1811 True
1809 1812 """
1810 1813 if bias is None:
1811 1814 bias = {}
1812 1815 if not date:
1813 1816 return 0, 0
1814 1817 if isinstance(date, tuple) and len(date) == 2:
1815 1818 return date
1816 1819 if not formats:
1817 1820 formats = defaultdateformats
1818 1821 date = date.strip()
1819 1822
1820 1823 if date == 'now' or date == _('now'):
1821 1824 return makedate()
1822 1825 if date == 'today' or date == _('today'):
1823 1826 date = datetime.date.today().strftime('%b %d')
1824 1827 elif date == 'yesterday' or date == _('yesterday'):
1825 1828 date = (datetime.date.today() -
1826 1829 datetime.timedelta(days=1)).strftime('%b %d')
1827 1830
1828 1831 try:
1829 1832 when, offset = map(int, date.split(' '))
1830 1833 except ValueError:
1831 1834 # fill out defaults
1832 1835 now = makedate()
1833 1836 defaults = {}
1834 1837 for part in ("d", "mb", "yY", "HI", "M", "S"):
1835 1838 # this piece is for rounding the specific end of unknowns
1836 1839 b = bias.get(part)
1837 1840 if b is None:
1838 1841 if part[0] in "HMS":
1839 1842 b = "00"
1840 1843 else:
1841 1844 b = "0"
1842 1845
1843 1846 # this piece is for matching the generic end to today's date
1844 1847 n = datestr(now, "%" + part[0])
1845 1848
1846 1849 defaults[part] = (b, n)
1847 1850
1848 1851 for format in formats:
1849 1852 try:
1850 1853 when, offset = strdate(date, format, defaults)
1851 1854 except (ValueError, OverflowError):
1852 1855 pass
1853 1856 else:
1854 1857 break
1855 1858 else:
1856 1859 raise Abort(_('invalid date: %r') % date)
1857 1860 # validate explicit (probably user-specified) date and
1858 1861 # time zone offset. values must fit in signed 32 bits for
1859 1862 # current 32-bit linux runtimes. timezones go from UTC-12
1860 1863 # to UTC+14
1861 1864 if when < -0x80000000 or when > 0x7fffffff:
1862 1865 raise Abort(_('date exceeds 32 bits: %d') % when)
1863 1866 if offset < -50400 or offset > 43200:
1864 1867 raise Abort(_('impossible time zone offset: %d') % offset)
1865 1868 return when, offset
1866 1869
1867 1870 def matchdate(date):
1868 1871 """Return a function that matches a given date match specifier
1869 1872
1870 1873 Formats include:
1871 1874
1872 1875 '{date}' match a given date to the accuracy provided
1873 1876
1874 1877 '<{date}' on or before a given date
1875 1878
1876 1879 '>{date}' on or after a given date
1877 1880
1878 1881 >>> p1 = parsedate("10:29:59")
1879 1882 >>> p2 = parsedate("10:30:00")
1880 1883 >>> p3 = parsedate("10:30:59")
1881 1884 >>> p4 = parsedate("10:31:00")
1882 1885 >>> p5 = parsedate("Sep 15 10:30:00 1999")
1883 1886 >>> f = matchdate("10:30")
1884 1887 >>> f(p1[0])
1885 1888 False
1886 1889 >>> f(p2[0])
1887 1890 True
1888 1891 >>> f(p3[0])
1889 1892 True
1890 1893 >>> f(p4[0])
1891 1894 False
1892 1895 >>> f(p5[0])
1893 1896 False
1894 1897 """
1895 1898
1896 1899 def lower(date):
1897 1900 d = {'mb': "1", 'd': "1"}
1898 1901 return parsedate(date, extendeddateformats, d)[0]
1899 1902
1900 1903 def upper(date):
1901 1904 d = {'mb': "12", 'HI': "23", 'M': "59", 'S': "59"}
1902 1905 for days in ("31", "30", "29"):
1903 1906 try:
1904 1907 d["d"] = days
1905 1908 return parsedate(date, extendeddateformats, d)[0]
1906 1909 except Abort:
1907 1910 pass
1908 1911 d["d"] = "28"
1909 1912 return parsedate(date, extendeddateformats, d)[0]
1910 1913
1911 1914 date = date.strip()
1912 1915
1913 1916 if not date:
1914 1917 raise Abort(_("dates cannot consist entirely of whitespace"))
1915 1918 elif date[0] == "<":
1916 1919 if not date[1:]:
1917 1920 raise Abort(_("invalid day spec, use '<DATE'"))
1918 1921 when = upper(date[1:])
1919 1922 return lambda x: x <= when
1920 1923 elif date[0] == ">":
1921 1924 if not date[1:]:
1922 1925 raise Abort(_("invalid day spec, use '>DATE'"))
1923 1926 when = lower(date[1:])
1924 1927 return lambda x: x >= when
1925 1928 elif date[0] == "-":
1926 1929 try:
1927 1930 days = int(date[1:])
1928 1931 except ValueError:
1929 1932 raise Abort(_("invalid day spec: %s") % date[1:])
1930 1933 if days < 0:
1931 1934 raise Abort(_('%s must be nonnegative (see "hg help dates")')
1932 1935 % date[1:])
1933 1936 when = makedate()[0] - days * 3600 * 24
1934 1937 return lambda x: x >= when
1935 1938 elif " to " in date:
1936 1939 a, b = date.split(" to ")
1937 1940 start, stop = lower(a), upper(b)
1938 1941 return lambda x: x >= start and x <= stop
1939 1942 else:
1940 1943 start, stop = lower(date), upper(date)
1941 1944 return lambda x: x >= start and x <= stop
1942 1945
1943 1946 def stringmatcher(pattern):
1944 1947 """
1945 1948 accepts a string, possibly starting with 're:' or 'literal:' prefix.
1946 1949 returns the matcher name, pattern, and matcher function.
1947 1950 missing or unknown prefixes are treated as literal matches.
1948 1951
1949 1952 helper for tests:
1950 1953 >>> def test(pattern, *tests):
1951 1954 ... kind, pattern, matcher = stringmatcher(pattern)
1952 1955 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
1953 1956
1954 1957 exact matching (no prefix):
1955 1958 >>> test('abcdefg', 'abc', 'def', 'abcdefg')
1956 1959 ('literal', 'abcdefg', [False, False, True])
1957 1960
1958 1961 regex matching ('re:' prefix)
1959 1962 >>> test('re:a.+b', 'nomatch', 'fooadef', 'fooadefbar')
1960 1963 ('re', 'a.+b', [False, False, True])
1961 1964
1962 1965 force exact matches ('literal:' prefix)
1963 1966 >>> test('literal:re:foobar', 'foobar', 're:foobar')
1964 1967 ('literal', 're:foobar', [False, True])
1965 1968
1966 1969 unknown prefixes are ignored and treated as literals
1967 1970 >>> test('foo:bar', 'foo', 'bar', 'foo:bar')
1968 1971 ('literal', 'foo:bar', [False, False, True])
1969 1972 """
1970 1973 if pattern.startswith('re:'):
1971 1974 pattern = pattern[3:]
1972 1975 try:
1973 1976 regex = remod.compile(pattern)
1974 1977 except remod.error as e:
1975 1978 raise error.ParseError(_('invalid regular expression: %s')
1976 1979 % e)
1977 1980 return 're', pattern, regex.search
1978 1981 elif pattern.startswith('literal:'):
1979 1982 pattern = pattern[8:]
1980 1983 return 'literal', pattern, pattern.__eq__
1981 1984
1982 1985 def shortuser(user):
1983 1986 """Return a short representation of a user name or email address."""
1984 1987 f = user.find('@')
1985 1988 if f >= 0:
1986 1989 user = user[:f]
1987 1990 f = user.find('<')
1988 1991 if f >= 0:
1989 1992 user = user[f + 1:]
1990 1993 f = user.find(' ')
1991 1994 if f >= 0:
1992 1995 user = user[:f]
1993 1996 f = user.find('.')
1994 1997 if f >= 0:
1995 1998 user = user[:f]
1996 1999 return user
1997 2000
1998 2001 def emailuser(user):
1999 2002 """Return the user portion of an email address."""
2000 2003 f = user.find('@')
2001 2004 if f >= 0:
2002 2005 user = user[:f]
2003 2006 f = user.find('<')
2004 2007 if f >= 0:
2005 2008 user = user[f + 1:]
2006 2009 return user
2007 2010
2008 2011 def email(author):
2009 2012 '''get email of author.'''
2010 2013 r = author.find('>')
2011 2014 if r == -1:
2012 2015 r = None
2013 2016 return author[author.find('<') + 1:r]
2014 2017
2015 2018 def ellipsis(text, maxlength=400):
2016 2019 """Trim string to at most maxlength (default: 400) columns in display."""
2017 2020 return encoding.trim(text, maxlength, ellipsis='...')
2018 2021
2019 2022 def unitcountfn(*unittable):
2020 2023 '''return a function that renders a readable count of some quantity'''
2021 2024
2022 2025 def go(count):
2023 2026 for multiplier, divisor, format in unittable:
2024 2027 if count >= divisor * multiplier:
2025 2028 return format % (count / float(divisor))
2026 2029 return unittable[-1][2] % count
2027 2030
2028 2031 return go
2029 2032
2030 2033 bytecount = unitcountfn(
2031 2034 (100, 1 << 30, _('%.0f GB')),
2032 2035 (10, 1 << 30, _('%.1f GB')),
2033 2036 (1, 1 << 30, _('%.2f GB')),
2034 2037 (100, 1 << 20, _('%.0f MB')),
2035 2038 (10, 1 << 20, _('%.1f MB')),
2036 2039 (1, 1 << 20, _('%.2f MB')),
2037 2040 (100, 1 << 10, _('%.0f KB')),
2038 2041 (10, 1 << 10, _('%.1f KB')),
2039 2042 (1, 1 << 10, _('%.2f KB')),
2040 2043 (1, 1, _('%.0f bytes')),
2041 2044 )
2042 2045
2043 2046 def uirepr(s):
2044 2047 # Avoid double backslash in Windows path repr()
2045 2048 return repr(s).replace('\\\\', '\\')
2046 2049
2047 2050 # delay import of textwrap
2048 2051 def MBTextWrapper(**kwargs):
2049 2052 class tw(textwrap.TextWrapper):
2050 2053 """
2051 2054 Extend TextWrapper for width-awareness.
2052 2055
2053 2056 Neither number of 'bytes' in any encoding nor 'characters' is
2054 2057 appropriate to calculate terminal columns for specified string.
2055 2058
2056 2059 Original TextWrapper implementation uses built-in 'len()' directly,
2057 2060 so overriding is needed to use width information of each characters.
2058 2061
2059 2062 In addition, characters classified into 'ambiguous' width are
2060 2063 treated as wide in East Asian area, but as narrow in other.
2061 2064
2062 2065 This requires use decision to determine width of such characters.
2063 2066 """
2064 2067 def _cutdown(self, ucstr, space_left):
2065 2068 l = 0
2066 2069 colwidth = encoding.ucolwidth
2067 2070 for i in xrange(len(ucstr)):
2068 2071 l += colwidth(ucstr[i])
2069 2072 if space_left < l:
2070 2073 return (ucstr[:i], ucstr[i:])
2071 2074 return ucstr, ''
2072 2075
2073 2076 # overriding of base class
2074 2077 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
2075 2078 space_left = max(width - cur_len, 1)
2076 2079
2077 2080 if self.break_long_words:
2078 2081 cut, res = self._cutdown(reversed_chunks[-1], space_left)
2079 2082 cur_line.append(cut)
2080 2083 reversed_chunks[-1] = res
2081 2084 elif not cur_line:
2082 2085 cur_line.append(reversed_chunks.pop())
2083 2086
2084 2087 # this overriding code is imported from TextWrapper of Python 2.6
2085 2088 # to calculate columns of string by 'encoding.ucolwidth()'
2086 2089 def _wrap_chunks(self, chunks):
2087 2090 colwidth = encoding.ucolwidth
2088 2091
2089 2092 lines = []
2090 2093 if self.width <= 0:
2091 2094 raise ValueError("invalid width %r (must be > 0)" % self.width)
2092 2095
2093 2096 # Arrange in reverse order so items can be efficiently popped
2094 2097 # from a stack of chucks.
2095 2098 chunks.reverse()
2096 2099
2097 2100 while chunks:
2098 2101
2099 2102 # Start the list of chunks that will make up the current line.
2100 2103 # cur_len is just the length of all the chunks in cur_line.
2101 2104 cur_line = []
2102 2105 cur_len = 0
2103 2106
2104 2107 # Figure out which static string will prefix this line.
2105 2108 if lines:
2106 2109 indent = self.subsequent_indent
2107 2110 else:
2108 2111 indent = self.initial_indent
2109 2112
2110 2113 # Maximum width for this line.
2111 2114 width = self.width - len(indent)
2112 2115
2113 2116 # First chunk on line is whitespace -- drop it, unless this
2114 2117 # is the very beginning of the text (i.e. no lines started yet).
2115 2118 if self.drop_whitespace and chunks[-1].strip() == '' and lines:
2116 2119 del chunks[-1]
2117 2120
2118 2121 while chunks:
2119 2122 l = colwidth(chunks[-1])
2120 2123
2121 2124 # Can at least squeeze this chunk onto the current line.
2122 2125 if cur_len + l <= width:
2123 2126 cur_line.append(chunks.pop())
2124 2127 cur_len += l
2125 2128
2126 2129 # Nope, this line is full.
2127 2130 else:
2128 2131 break
2129 2132
2130 2133 # The current line is full, and the next chunk is too big to
2131 2134 # fit on *any* line (not just this one).
2132 2135 if chunks and colwidth(chunks[-1]) > width:
2133 2136 self._handle_long_word(chunks, cur_line, cur_len, width)
2134 2137
2135 2138 # If the last chunk on this line is all whitespace, drop it.
2136 2139 if (self.drop_whitespace and
2137 2140 cur_line and cur_line[-1].strip() == ''):
2138 2141 del cur_line[-1]
2139 2142
2140 2143 # Convert current line back to a string and store it in list
2141 2144 # of all lines (return value).
2142 2145 if cur_line:
2143 2146 lines.append(indent + ''.join(cur_line))
2144 2147
2145 2148 return lines
2146 2149
2147 2150 global MBTextWrapper
2148 2151 MBTextWrapper = tw
2149 2152 return tw(**kwargs)
2150 2153
2151 2154 def wrap(line, width, initindent='', hangindent=''):
2152 2155 maxindent = max(len(hangindent), len(initindent))
2153 2156 if width <= maxindent:
2154 2157 # adjust for weird terminal size
2155 2158 width = max(78, maxindent + 1)
2156 2159 line = line.decode(encoding.encoding, encoding.encodingmode)
2157 2160 initindent = initindent.decode(encoding.encoding, encoding.encodingmode)
2158 2161 hangindent = hangindent.decode(encoding.encoding, encoding.encodingmode)
2159 2162 wrapper = MBTextWrapper(width=width,
2160 2163 initial_indent=initindent,
2161 2164 subsequent_indent=hangindent)
2162 2165 return wrapper.fill(line).encode(encoding.encoding)
2163 2166
2164 2167 def iterlines(iterator):
2165 2168 for chunk in iterator:
2166 2169 for line in chunk.splitlines():
2167 2170 yield line
2168 2171
2169 2172 def expandpath(path):
2170 2173 return os.path.expanduser(os.path.expandvars(path))
2171 2174
2172 2175 def hgcmd():
2173 2176 """Return the command used to execute current hg
2174 2177
2175 2178 This is different from hgexecutable() because on Windows we want
2176 2179 to avoid things opening new shell windows like batch files, so we
2177 2180 get either the python call or current executable.
2178 2181 """
2179 2182 if mainfrozen():
2180 2183 if getattr(sys, 'frozen', None) == 'macosx_app':
2181 2184 # Env variable set by py2app
2182 2185 return [os.environ['EXECUTABLEPATH']]
2183 2186 else:
2184 2187 return [sys.executable]
2185 2188 return gethgcmd()
2186 2189
2187 2190 def rundetached(args, condfn):
2188 2191 """Execute the argument list in a detached process.
2189 2192
2190 2193 condfn is a callable which is called repeatedly and should return
2191 2194 True once the child process is known to have started successfully.
2192 2195 At this point, the child process PID is returned. If the child
2193 2196 process fails to start or finishes before condfn() evaluates to
2194 2197 True, return -1.
2195 2198 """
2196 2199 # Windows case is easier because the child process is either
2197 2200 # successfully starting and validating the condition or exiting
2198 2201 # on failure. We just poll on its PID. On Unix, if the child
2199 2202 # process fails to start, it will be left in a zombie state until
2200 2203 # the parent wait on it, which we cannot do since we expect a long
2201 2204 # running process on success. Instead we listen for SIGCHLD telling
2202 2205 # us our child process terminated.
2203 2206 terminated = set()
2204 2207 def handler(signum, frame):
2205 2208 terminated.add(os.wait())
2206 2209 prevhandler = None
2207 2210 SIGCHLD = getattr(signal, 'SIGCHLD', None)
2208 2211 if SIGCHLD is not None:
2209 2212 prevhandler = signal.signal(SIGCHLD, handler)
2210 2213 try:
2211 2214 pid = spawndetached(args)
2212 2215 while not condfn():
2213 2216 if ((pid in terminated or not testpid(pid))
2214 2217 and not condfn()):
2215 2218 return -1
2216 2219 time.sleep(0.1)
2217 2220 return pid
2218 2221 finally:
2219 2222 if prevhandler is not None:
2220 2223 signal.signal(signal.SIGCHLD, prevhandler)
2221 2224
2222 2225 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2223 2226 """Return the result of interpolating items in the mapping into string s.
2224 2227
2225 2228 prefix is a single character string, or a two character string with
2226 2229 a backslash as the first character if the prefix needs to be escaped in
2227 2230 a regular expression.
2228 2231
2229 2232 fn is an optional function that will be applied to the replacement text
2230 2233 just before replacement.
2231 2234
2232 2235 escape_prefix is an optional flag that allows using doubled prefix for
2233 2236 its escaping.
2234 2237 """
2235 2238 fn = fn or (lambda s: s)
2236 2239 patterns = '|'.join(mapping.keys())
2237 2240 if escape_prefix:
2238 2241 patterns += '|' + prefix
2239 2242 if len(prefix) > 1:
2240 2243 prefix_char = prefix[1:]
2241 2244 else:
2242 2245 prefix_char = prefix
2243 2246 mapping[prefix_char] = prefix_char
2244 2247 r = remod.compile(r'%s(%s)' % (prefix, patterns))
2245 2248 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2246 2249
2247 2250 def getport(port):
2248 2251 """Return the port for a given network service.
2249 2252
2250 2253 If port is an integer, it's returned as is. If it's a string, it's
2251 2254 looked up using socket.getservbyname(). If there's no matching
2252 2255 service, error.Abort is raised.
2253 2256 """
2254 2257 try:
2255 2258 return int(port)
2256 2259 except ValueError:
2257 2260 pass
2258 2261
2259 2262 try:
2260 2263 return socket.getservbyname(port)
2261 2264 except socket.error:
2262 2265 raise Abort(_("no port number associated with service '%s'") % port)
2263 2266
2264 2267 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
2265 2268 '0': False, 'no': False, 'false': False, 'off': False,
2266 2269 'never': False}
2267 2270
2268 2271 def parsebool(s):
2269 2272 """Parse s into a boolean.
2270 2273
2271 2274 If s is not a valid boolean, returns None.
2272 2275 """
2273 2276 return _booleans.get(s.lower(), None)
2274 2277
2275 2278 _hexdig = '0123456789ABCDEFabcdef'
2276 2279 _hextochr = dict((a + b, chr(int(a + b, 16)))
2277 2280 for a in _hexdig for b in _hexdig)
2278 2281
2279 2282 def _urlunquote(s):
2280 2283 """Decode HTTP/HTML % encoding.
2281 2284
2282 2285 >>> _urlunquote('abc%20def')
2283 2286 'abc def'
2284 2287 """
2285 2288 res = s.split('%')
2286 2289 # fastpath
2287 2290 if len(res) == 1:
2288 2291 return s
2289 2292 s = res[0]
2290 2293 for item in res[1:]:
2291 2294 try:
2292 2295 s += _hextochr[item[:2]] + item[2:]
2293 2296 except KeyError:
2294 2297 s += '%' + item
2295 2298 except UnicodeDecodeError:
2296 2299 s += unichr(int(item[:2], 16)) + item[2:]
2297 2300 return s
2298 2301
2299 2302 class url(object):
2300 2303 r"""Reliable URL parser.
2301 2304
2302 2305 This parses URLs and provides attributes for the following
2303 2306 components:
2304 2307
2305 2308 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
2306 2309
2307 2310 Missing components are set to None. The only exception is
2308 2311 fragment, which is set to '' if present but empty.
2309 2312
2310 2313 If parsefragment is False, fragment is included in query. If
2311 2314 parsequery is False, query is included in path. If both are
2312 2315 False, both fragment and query are included in path.
2313 2316
2314 2317 See http://www.ietf.org/rfc/rfc2396.txt for more information.
2315 2318
2316 2319 Note that for backward compatibility reasons, bundle URLs do not
2317 2320 take host names. That means 'bundle://../' has a path of '../'.
2318 2321
2319 2322 Examples:
2320 2323
2321 2324 >>> url('http://www.ietf.org/rfc/rfc2396.txt')
2322 2325 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
2323 2326 >>> url('ssh://[::1]:2200//home/joe/repo')
2324 2327 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
2325 2328 >>> url('file:///home/joe/repo')
2326 2329 <url scheme: 'file', path: '/home/joe/repo'>
2327 2330 >>> url('file:///c:/temp/foo/')
2328 2331 <url scheme: 'file', path: 'c:/temp/foo/'>
2329 2332 >>> url('bundle:foo')
2330 2333 <url scheme: 'bundle', path: 'foo'>
2331 2334 >>> url('bundle://../foo')
2332 2335 <url scheme: 'bundle', path: '../foo'>
2333 2336 >>> url(r'c:\foo\bar')
2334 2337 <url path: 'c:\\foo\\bar'>
2335 2338 >>> url(r'\\blah\blah\blah')
2336 2339 <url path: '\\\\blah\\blah\\blah'>
2337 2340 >>> url(r'\\blah\blah\blah#baz')
2338 2341 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
2339 2342 >>> url(r'file:///C:\users\me')
2340 2343 <url scheme: 'file', path: 'C:\\users\\me'>
2341 2344
2342 2345 Authentication credentials:
2343 2346
2344 2347 >>> url('ssh://joe:xyz@x/repo')
2345 2348 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
2346 2349 >>> url('ssh://joe@x/repo')
2347 2350 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
2348 2351
2349 2352 Query strings and fragments:
2350 2353
2351 2354 >>> url('http://host/a?b#c')
2352 2355 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
2353 2356 >>> url('http://host/a?b#c', parsequery=False, parsefragment=False)
2354 2357 <url scheme: 'http', host: 'host', path: 'a?b#c'>
2355 2358 """
2356 2359
2357 2360 _safechars = "!~*'()+"
2358 2361 _safepchars = "/!~*'()+:\\"
2359 2362 _matchscheme = remod.compile(r'^[a-zA-Z0-9+.\-]+:').match
2360 2363
2361 2364 def __init__(self, path, parsequery=True, parsefragment=True):
2362 2365 # We slowly chomp away at path until we have only the path left
2363 2366 self.scheme = self.user = self.passwd = self.host = None
2364 2367 self.port = self.path = self.query = self.fragment = None
2365 2368 self._localpath = True
2366 2369 self._hostport = ''
2367 2370 self._origpath = path
2368 2371
2369 2372 if parsefragment and '#' in path:
2370 2373 path, self.fragment = path.split('#', 1)
2371 2374 if not path:
2372 2375 path = None
2373 2376
2374 2377 # special case for Windows drive letters and UNC paths
2375 2378 if hasdriveletter(path) or path.startswith(r'\\'):
2376 2379 self.path = path
2377 2380 return
2378 2381
2379 2382 # For compatibility reasons, we can't handle bundle paths as
2380 2383 # normal URLS
2381 2384 if path.startswith('bundle:'):
2382 2385 self.scheme = 'bundle'
2383 2386 path = path[7:]
2384 2387 if path.startswith('//'):
2385 2388 path = path[2:]
2386 2389 self.path = path
2387 2390 return
2388 2391
2389 2392 if self._matchscheme(path):
2390 2393 parts = path.split(':', 1)
2391 2394 if parts[0]:
2392 2395 self.scheme, path = parts
2393 2396 self._localpath = False
2394 2397
2395 2398 if not path:
2396 2399 path = None
2397 2400 if self._localpath:
2398 2401 self.path = ''
2399 2402 return
2400 2403 else:
2401 2404 if self._localpath:
2402 2405 self.path = path
2403 2406 return
2404 2407
2405 2408 if parsequery and '?' in path:
2406 2409 path, self.query = path.split('?', 1)
2407 2410 if not path:
2408 2411 path = None
2409 2412 if not self.query:
2410 2413 self.query = None
2411 2414
2412 2415 # // is required to specify a host/authority
2413 2416 if path and path.startswith('//'):
2414 2417 parts = path[2:].split('/', 1)
2415 2418 if len(parts) > 1:
2416 2419 self.host, path = parts
2417 2420 else:
2418 2421 self.host = parts[0]
2419 2422 path = None
2420 2423 if not self.host:
2421 2424 self.host = None
2422 2425 # path of file:///d is /d
2423 2426 # path of file:///d:/ is d:/, not /d:/
2424 2427 if path and not hasdriveletter(path):
2425 2428 path = '/' + path
2426 2429
2427 2430 if self.host and '@' in self.host:
2428 2431 self.user, self.host = self.host.rsplit('@', 1)
2429 2432 if ':' in self.user:
2430 2433 self.user, self.passwd = self.user.split(':', 1)
2431 2434 if not self.host:
2432 2435 self.host = None
2433 2436
2434 2437 # Don't split on colons in IPv6 addresses without ports
2435 2438 if (self.host and ':' in self.host and
2436 2439 not (self.host.startswith('[') and self.host.endswith(']'))):
2437 2440 self._hostport = self.host
2438 2441 self.host, self.port = self.host.rsplit(':', 1)
2439 2442 if not self.host:
2440 2443 self.host = None
2441 2444
2442 2445 if (self.host and self.scheme == 'file' and
2443 2446 self.host not in ('localhost', '127.0.0.1', '[::1]')):
2444 2447 raise Abort(_('file:// URLs can only refer to localhost'))
2445 2448
2446 2449 self.path = path
2447 2450
2448 2451 # leave the query string escaped
2449 2452 for a in ('user', 'passwd', 'host', 'port',
2450 2453 'path', 'fragment'):
2451 2454 v = getattr(self, a)
2452 2455 if v is not None:
2453 2456 setattr(self, a, _urlunquote(v))
2454 2457
2455 2458 def __repr__(self):
2456 2459 attrs = []
2457 2460 for a in ('scheme', 'user', 'passwd', 'host', 'port', 'path',
2458 2461 'query', 'fragment'):
2459 2462 v = getattr(self, a)
2460 2463 if v is not None:
2461 2464 attrs.append('%s: %r' % (a, v))
2462 2465 return '<url %s>' % ', '.join(attrs)
2463 2466
2464 2467 def __str__(self):
2465 2468 r"""Join the URL's components back into a URL string.
2466 2469
2467 2470 Examples:
2468 2471
2469 2472 >>> str(url('http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
2470 2473 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
2471 2474 >>> str(url('http://user:pw@host:80/?foo=bar&baz=42'))
2472 2475 'http://user:pw@host:80/?foo=bar&baz=42'
2473 2476 >>> str(url('http://user:pw@host:80/?foo=bar%3dbaz'))
2474 2477 'http://user:pw@host:80/?foo=bar%3dbaz'
2475 2478 >>> str(url('ssh://user:pw@[::1]:2200//home/joe#'))
2476 2479 'ssh://user:pw@[::1]:2200//home/joe#'
2477 2480 >>> str(url('http://localhost:80//'))
2478 2481 'http://localhost:80//'
2479 2482 >>> str(url('http://localhost:80/'))
2480 2483 'http://localhost:80/'
2481 2484 >>> str(url('http://localhost:80'))
2482 2485 'http://localhost:80/'
2483 2486 >>> str(url('bundle:foo'))
2484 2487 'bundle:foo'
2485 2488 >>> str(url('bundle://../foo'))
2486 2489 'bundle:../foo'
2487 2490 >>> str(url('path'))
2488 2491 'path'
2489 2492 >>> str(url('file:///tmp/foo/bar'))
2490 2493 'file:///tmp/foo/bar'
2491 2494 >>> str(url('file:///c:/tmp/foo/bar'))
2492 2495 'file:///c:/tmp/foo/bar'
2493 2496 >>> print url(r'bundle:foo\bar')
2494 2497 bundle:foo\bar
2495 2498 >>> print url(r'file:///D:\data\hg')
2496 2499 file:///D:\data\hg
2497 2500 """
2498 2501 if self._localpath:
2499 2502 s = self.path
2500 2503 if self.scheme == 'bundle':
2501 2504 s = 'bundle:' + s
2502 2505 if self.fragment:
2503 2506 s += '#' + self.fragment
2504 2507 return s
2505 2508
2506 2509 s = self.scheme + ':'
2507 2510 if self.user or self.passwd or self.host:
2508 2511 s += '//'
2509 2512 elif self.scheme and (not self.path or self.path.startswith('/')
2510 2513 or hasdriveletter(self.path)):
2511 2514 s += '//'
2512 2515 if hasdriveletter(self.path):
2513 2516 s += '/'
2514 2517 if self.user:
2515 2518 s += urlreq.quote(self.user, safe=self._safechars)
2516 2519 if self.passwd:
2517 2520 s += ':' + urlreq.quote(self.passwd, safe=self._safechars)
2518 2521 if self.user or self.passwd:
2519 2522 s += '@'
2520 2523 if self.host:
2521 2524 if not (self.host.startswith('[') and self.host.endswith(']')):
2522 2525 s += urlreq.quote(self.host)
2523 2526 else:
2524 2527 s += self.host
2525 2528 if self.port:
2526 2529 s += ':' + urlreq.quote(self.port)
2527 2530 if self.host:
2528 2531 s += '/'
2529 2532 if self.path:
2530 2533 # TODO: similar to the query string, we should not unescape the
2531 2534 # path when we store it, the path might contain '%2f' = '/',
2532 2535 # which we should *not* escape.
2533 2536 s += urlreq.quote(self.path, safe=self._safepchars)
2534 2537 if self.query:
2535 2538 # we store the query in escaped form.
2536 2539 s += '?' + self.query
2537 2540 if self.fragment is not None:
2538 2541 s += '#' + urlreq.quote(self.fragment, safe=self._safepchars)
2539 2542 return s
2540 2543
2541 2544 def authinfo(self):
2542 2545 user, passwd = self.user, self.passwd
2543 2546 try:
2544 2547 self.user, self.passwd = None, None
2545 2548 s = str(self)
2546 2549 finally:
2547 2550 self.user, self.passwd = user, passwd
2548 2551 if not self.user:
2549 2552 return (s, None)
2550 2553 # authinfo[1] is passed to urllib2 password manager, and its
2551 2554 # URIs must not contain credentials. The host is passed in the
2552 2555 # URIs list because Python < 2.4.3 uses only that to search for
2553 2556 # a password.
2554 2557 return (s, (None, (s, self.host),
2555 2558 self.user, self.passwd or ''))
2556 2559
2557 2560 def isabs(self):
2558 2561 if self.scheme and self.scheme != 'file':
2559 2562 return True # remote URL
2560 2563 if hasdriveletter(self.path):
2561 2564 return True # absolute for our purposes - can't be joined()
2562 2565 if self.path.startswith(r'\\'):
2563 2566 return True # Windows UNC path
2564 2567 if self.path.startswith('/'):
2565 2568 return True # POSIX-style
2566 2569 return False
2567 2570
2568 2571 def localpath(self):
2569 2572 if self.scheme == 'file' or self.scheme == 'bundle':
2570 2573 path = self.path or '/'
2571 2574 # For Windows, we need to promote hosts containing drive
2572 2575 # letters to paths with drive letters.
2573 2576 if hasdriveletter(self._hostport):
2574 2577 path = self._hostport + '/' + self.path
2575 2578 elif (self.host is not None and self.path
2576 2579 and not hasdriveletter(path)):
2577 2580 path = '/' + path
2578 2581 return path
2579 2582 return self._origpath
2580 2583
2581 2584 def islocal(self):
2582 2585 '''whether localpath will return something that posixfile can open'''
2583 2586 return (not self.scheme or self.scheme == 'file'
2584 2587 or self.scheme == 'bundle')
2585 2588
2586 2589 def hasscheme(path):
2587 2590 return bool(url(path).scheme)
2588 2591
2589 2592 def hasdriveletter(path):
2590 2593 return path and path[1:2] == ':' and path[0:1].isalpha()
2591 2594
2592 2595 def urllocalpath(path):
2593 2596 return url(path, parsequery=False, parsefragment=False).localpath()
2594 2597
2595 2598 def hidepassword(u):
2596 2599 '''hide user credential in a url string'''
2597 2600 u = url(u)
2598 2601 if u.passwd:
2599 2602 u.passwd = '***'
2600 2603 return str(u)
2601 2604
2602 2605 def removeauth(u):
2603 2606 '''remove all authentication information from a url string'''
2604 2607 u = url(u)
2605 2608 u.user = u.passwd = None
2606 2609 return str(u)
2607 2610
2608 2611 def isatty(fp):
2609 2612 try:
2610 2613 return fp.isatty()
2611 2614 except AttributeError:
2612 2615 return False
2613 2616
2614 2617 timecount = unitcountfn(
2615 2618 (1, 1e3, _('%.0f s')),
2616 2619 (100, 1, _('%.1f s')),
2617 2620 (10, 1, _('%.2f s')),
2618 2621 (1, 1, _('%.3f s')),
2619 2622 (100, 0.001, _('%.1f ms')),
2620 2623 (10, 0.001, _('%.2f ms')),
2621 2624 (1, 0.001, _('%.3f ms')),
2622 2625 (100, 0.000001, _('%.1f us')),
2623 2626 (10, 0.000001, _('%.2f us')),
2624 2627 (1, 0.000001, _('%.3f us')),
2625 2628 (100, 0.000000001, _('%.1f ns')),
2626 2629 (10, 0.000000001, _('%.2f ns')),
2627 2630 (1, 0.000000001, _('%.3f ns')),
2628 2631 )
2629 2632
2630 2633 _timenesting = [0]
2631 2634
2632 2635 def timed(func):
2633 2636 '''Report the execution time of a function call to stderr.
2634 2637
2635 2638 During development, use as a decorator when you need to measure
2636 2639 the cost of a function, e.g. as follows:
2637 2640
2638 2641 @util.timed
2639 2642 def foo(a, b, c):
2640 2643 pass
2641 2644 '''
2642 2645
2643 2646 def wrapper(*args, **kwargs):
2644 2647 start = time.time()
2645 2648 indent = 2
2646 2649 _timenesting[0] += indent
2647 2650 try:
2648 2651 return func(*args, **kwargs)
2649 2652 finally:
2650 2653 elapsed = time.time() - start
2651 2654 _timenesting[0] -= indent
2652 2655 sys.stderr.write('%s%s: %s\n' %
2653 2656 (' ' * _timenesting[0], func.__name__,
2654 2657 timecount(elapsed)))
2655 2658 return wrapper
2656 2659
2657 2660 _sizeunits = (('m', 2**20), ('k', 2**10), ('g', 2**30),
2658 2661 ('kb', 2**10), ('mb', 2**20), ('gb', 2**30), ('b', 1))
2659 2662
2660 2663 def sizetoint(s):
2661 2664 '''Convert a space specifier to a byte count.
2662 2665
2663 2666 >>> sizetoint('30')
2664 2667 30
2665 2668 >>> sizetoint('2.2kb')
2666 2669 2252
2667 2670 >>> sizetoint('6M')
2668 2671 6291456
2669 2672 '''
2670 2673 t = s.strip().lower()
2671 2674 try:
2672 2675 for k, u in _sizeunits:
2673 2676 if t.endswith(k):
2674 2677 return int(float(t[:-len(k)]) * u)
2675 2678 return int(t)
2676 2679 except ValueError:
2677 2680 raise error.ParseError(_("couldn't parse size: %s") % s)
2678 2681
2679 2682 class hooks(object):
2680 2683 '''A collection of hook functions that can be used to extend a
2681 2684 function's behavior. Hooks are called in lexicographic order,
2682 2685 based on the names of their sources.'''
2683 2686
2684 2687 def __init__(self):
2685 2688 self._hooks = []
2686 2689
2687 2690 def add(self, source, hook):
2688 2691 self._hooks.append((source, hook))
2689 2692
2690 2693 def __call__(self, *args):
2691 2694 self._hooks.sort(key=lambda x: x[0])
2692 2695 results = []
2693 2696 for source, hook in self._hooks:
2694 2697 results.append(hook(*args))
2695 2698 return results
2696 2699
2697 2700 def getstackframes(skip=0, line=' %-*s in %s\n', fileline='%s:%s'):
2698 2701 '''Yields lines for a nicely formatted stacktrace.
2699 2702 Skips the 'skip' last entries.
2700 2703 Each file+linenumber is formatted according to fileline.
2701 2704 Each line is formatted according to line.
2702 2705 If line is None, it yields:
2703 2706 length of longest filepath+line number,
2704 2707 filepath+linenumber,
2705 2708 function
2706 2709
2707 2710 Not be used in production code but very convenient while developing.
2708 2711 '''
2709 2712 entries = [(fileline % (fn, ln), func)
2710 2713 for fn, ln, func, _text in traceback.extract_stack()[:-skip - 1]]
2711 2714 if entries:
2712 2715 fnmax = max(len(entry[0]) for entry in entries)
2713 2716 for fnln, func in entries:
2714 2717 if line is None:
2715 2718 yield (fnmax, fnln, func)
2716 2719 else:
2717 2720 yield line % (fnmax, fnln, func)
2718 2721
2719 2722 def debugstacktrace(msg='stacktrace', skip=0, f=sys.stderr, otherf=sys.stdout):
2720 2723 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
2721 2724 Skips the 'skip' last entries. By default it will flush stdout first.
2722 2725 It can be used everywhere and intentionally does not require an ui object.
2723 2726 Not be used in production code but very convenient while developing.
2724 2727 '''
2725 2728 if otherf:
2726 2729 otherf.flush()
2727 2730 f.write('%s at:\n' % msg)
2728 2731 for line in getstackframes(skip + 1):
2729 2732 f.write(line)
2730 2733 f.flush()
2731 2734
2732 2735 class dirs(object):
2733 2736 '''a multiset of directory names from a dirstate or manifest'''
2734 2737
2735 2738 def __init__(self, map, skip=None):
2736 2739 self._dirs = {}
2737 2740 addpath = self.addpath
2738 2741 if safehasattr(map, 'iteritems') and skip is not None:
2739 2742 for f, s in map.iteritems():
2740 2743 if s[0] != skip:
2741 2744 addpath(f)
2742 2745 else:
2743 2746 for f in map:
2744 2747 addpath(f)
2745 2748
2746 2749 def addpath(self, path):
2747 2750 dirs = self._dirs
2748 2751 for base in finddirs(path):
2749 2752 if base in dirs:
2750 2753 dirs[base] += 1
2751 2754 return
2752 2755 dirs[base] = 1
2753 2756
2754 2757 def delpath(self, path):
2755 2758 dirs = self._dirs
2756 2759 for base in finddirs(path):
2757 2760 if dirs[base] > 1:
2758 2761 dirs[base] -= 1
2759 2762 return
2760 2763 del dirs[base]
2761 2764
2762 2765 def __iter__(self):
2763 2766 return self._dirs.iterkeys()
2764 2767
2765 2768 def __contains__(self, d):
2766 2769 return d in self._dirs
2767 2770
2768 2771 if safehasattr(parsers, 'dirs'):
2769 2772 dirs = parsers.dirs
2770 2773
2771 2774 def finddirs(path):
2772 2775 pos = path.rfind('/')
2773 2776 while pos != -1:
2774 2777 yield path[:pos]
2775 2778 pos = path.rfind('/', 0, pos)
2776 2779
2777 2780 # compression utility
2778 2781
2779 2782 class nocompress(object):
2780 2783 def compress(self, x):
2781 2784 return x
2782 2785 def flush(self):
2783 2786 return ""
2784 2787
2785 2788 compressors = {
2786 2789 None: nocompress,
2787 2790 # lambda to prevent early import
2788 2791 'BZ': lambda: bz2.BZ2Compressor(),
2789 2792 'GZ': lambda: zlib.compressobj(),
2790 2793 }
2791 2794 # also support the old form by courtesies
2792 2795 compressors['UN'] = compressors[None]
2793 2796
2794 2797 def _makedecompressor(decompcls):
2795 2798 def generator(f):
2796 2799 d = decompcls()
2797 2800 for chunk in filechunkiter(f):
2798 2801 yield d.decompress(chunk)
2799 2802 def func(fh):
2800 2803 return chunkbuffer(generator(fh))
2801 2804 return func
2802 2805
2803 2806 class ctxmanager(object):
2804 2807 '''A context manager for use in 'with' blocks to allow multiple
2805 2808 contexts to be entered at once. This is both safer and more
2806 2809 flexible than contextlib.nested.
2807 2810
2808 2811 Once Mercurial supports Python 2.7+, this will become mostly
2809 2812 unnecessary.
2810 2813 '''
2811 2814
2812 2815 def __init__(self, *args):
2813 2816 '''Accepts a list of no-argument functions that return context
2814 2817 managers. These will be invoked at __call__ time.'''
2815 2818 self._pending = args
2816 2819 self._atexit = []
2817 2820
2818 2821 def __enter__(self):
2819 2822 return self
2820 2823
2821 2824 def enter(self):
2822 2825 '''Create and enter context managers in the order in which they were
2823 2826 passed to the constructor.'''
2824 2827 values = []
2825 2828 for func in self._pending:
2826 2829 obj = func()
2827 2830 values.append(obj.__enter__())
2828 2831 self._atexit.append(obj.__exit__)
2829 2832 del self._pending
2830 2833 return values
2831 2834
2832 2835 def atexit(self, func, *args, **kwargs):
2833 2836 '''Add a function to call when this context manager exits. The
2834 2837 ordering of multiple atexit calls is unspecified, save that
2835 2838 they will happen before any __exit__ functions.'''
2836 2839 def wrapper(exc_type, exc_val, exc_tb):
2837 2840 func(*args, **kwargs)
2838 2841 self._atexit.append(wrapper)
2839 2842 return func
2840 2843
2841 2844 def __exit__(self, exc_type, exc_val, exc_tb):
2842 2845 '''Context managers are exited in the reverse order from which
2843 2846 they were created.'''
2844 2847 received = exc_type is not None
2845 2848 suppressed = False
2846 2849 pending = None
2847 2850 self._atexit.reverse()
2848 2851 for exitfunc in self._atexit:
2849 2852 try:
2850 2853 if exitfunc(exc_type, exc_val, exc_tb):
2851 2854 suppressed = True
2852 2855 exc_type = None
2853 2856 exc_val = None
2854 2857 exc_tb = None
2855 2858 except BaseException:
2856 2859 pending = sys.exc_info()
2857 2860 exc_type, exc_val, exc_tb = pending = sys.exc_info()
2858 2861 del self._atexit
2859 2862 if pending:
2860 2863 raise exc_val
2861 2864 return received and suppressed
2862 2865
2863 2866 def _bz2():
2864 2867 d = bz2.BZ2Decompressor()
2865 2868 # Bzip2 stream start with BZ, but we stripped it.
2866 2869 # we put it back for good measure.
2867 2870 d.decompress('BZ')
2868 2871 return d
2869 2872
2870 2873 decompressors = {None: lambda fh: fh,
2871 2874 '_truncatedBZ': _makedecompressor(_bz2),
2872 2875 'BZ': _makedecompressor(lambda: bz2.BZ2Decompressor()),
2873 2876 'GZ': _makedecompressor(lambda: zlib.decompressobj()),
2874 2877 }
2875 2878 # also support the old form by courtesies
2876 2879 decompressors['UN'] = decompressors[None]
2877 2880
2878 2881 # convenient shortcut
2879 2882 dst = debugstacktrace
@@ -1,3864 +1,3867 b''
1 1 $ hg init a
2 2 $ cd a
3 3 $ echo a > a
4 4 $ hg add a
5 5 $ echo line 1 > b
6 6 $ echo line 2 >> b
7 7 $ hg commit -l b -d '1000000 0' -u 'User Name <user@hostname>'
8 8
9 9 $ hg add b
10 10 $ echo other 1 > c
11 11 $ echo other 2 >> c
12 12 $ echo >> c
13 13 $ echo other 3 >> c
14 14 $ hg commit -l c -d '1100000 0' -u 'A. N. Other <other@place>'
15 15
16 16 $ hg add c
17 17 $ hg commit -m 'no person' -d '1200000 0' -u 'other@place'
18 18 $ echo c >> c
19 19 $ hg commit -m 'no user, no domain' -d '1300000 0' -u 'person'
20 20
21 21 $ echo foo > .hg/branch
22 22 $ hg commit -m 'new branch' -d '1400000 0' -u 'person'
23 23
24 24 $ hg co -q 3
25 25 $ echo other 4 >> d
26 26 $ hg add d
27 27 $ hg commit -m 'new head' -d '1500000 0' -u 'person'
28 28
29 29 $ hg merge -q foo
30 30 $ hg commit -m 'merge' -d '1500001 0' -u 'person'
31 31
32 32 Second branch starting at nullrev:
33 33
34 34 $ hg update null
35 35 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
36 36 $ echo second > second
37 37 $ hg add second
38 38 $ hg commit -m second -d '1000000 0' -u 'User Name <user@hostname>'
39 39 created new head
40 40
41 41 $ echo third > third
42 42 $ hg add third
43 43 $ hg mv second fourth
44 44 $ hg commit -m third -d "2020-01-01 10:01"
45 45
46 46 $ hg log --template '{join(file_copies, ",\n")}\n' -r .
47 47 fourth (second)
48 48 $ hg log -T '{file_copies % "{source} -> {name}\n"}' -r .
49 49 second -> fourth
50 50 $ hg log -T '{rev} {ifcontains("fourth", file_copies, "t", "f")}\n' -r .:7
51 51 8 t
52 52 7 f
53 53
54 54 Working-directory revision has special identifiers, though they are still
55 55 experimental:
56 56
57 57 $ hg log -r 'wdir()' -T '{rev}:{node}\n'
58 58 2147483647:ffffffffffffffffffffffffffffffffffffffff
59 59
60 60 Some keywords are invalid for working-directory revision, but they should
61 61 never cause crash:
62 62
63 63 $ hg log -r 'wdir()' -T '{manifest}\n'
64 64
65 65
66 66 Quoting for ui.logtemplate
67 67
68 68 $ hg tip --config "ui.logtemplate={rev}\n"
69 69 8
70 70 $ hg tip --config "ui.logtemplate='{rev}\n'"
71 71 8
72 72 $ hg tip --config 'ui.logtemplate="{rev}\n"'
73 73 8
74 74 $ hg tip --config 'ui.logtemplate=n{rev}\n'
75 75 n8
76 76
77 77 Make sure user/global hgrc does not affect tests
78 78
79 79 $ echo '[ui]' > .hg/hgrc
80 80 $ echo 'logtemplate =' >> .hg/hgrc
81 81 $ echo 'style =' >> .hg/hgrc
82 82
83 83 Add some simple styles to settings
84 84
85 85 $ echo '[templates]' >> .hg/hgrc
86 86 $ printf 'simple = "{rev}\\n"\n' >> .hg/hgrc
87 87 $ printf 'simple2 = {rev}\\n\n' >> .hg/hgrc
88 88
89 89 $ hg log -l1 -Tsimple
90 90 8
91 91 $ hg log -l1 -Tsimple2
92 92 8
93 93
94 94 Test templates and style maps in files:
95 95
96 96 $ echo "{rev}" > tmpl
97 97 $ hg log -l1 -T./tmpl
98 98 8
99 99 $ hg log -l1 -Tblah/blah
100 100 blah/blah (no-eol)
101 101
102 102 $ printf 'changeset = "{rev}\\n"\n' > map-simple
103 103 $ hg log -l1 -T./map-simple
104 104 8
105 105
106 106 Template should precede style option
107 107
108 108 $ hg log -l1 --style default -T '{rev}\n'
109 109 8
110 110
111 111 Add a commit with empty description, to ensure that the templates
112 112 below will omit the description line.
113 113
114 114 $ echo c >> c
115 115 $ hg add c
116 116 $ hg commit -qm ' '
117 117
118 118 Default style is like normal output. Phases style should be the same
119 119 as default style, except for extra phase lines.
120 120
121 121 $ hg log > log.out
122 122 $ hg log --style default > style.out
123 123 $ cmp log.out style.out || diff -u log.out style.out
124 124 $ hg log -T phases > phases.out
125 125 $ diff -U 0 log.out phases.out | egrep -v '^---|^\+\+\+|^@@'
126 126 +phase: draft
127 127 +phase: draft
128 128 +phase: draft
129 129 +phase: draft
130 130 +phase: draft
131 131 +phase: draft
132 132 +phase: draft
133 133 +phase: draft
134 134 +phase: draft
135 135 +phase: draft
136 136
137 137 $ hg log -v > log.out
138 138 $ hg log -v --style default > style.out
139 139 $ cmp log.out style.out || diff -u log.out style.out
140 140 $ hg log -v -T phases > phases.out
141 141 $ diff -U 0 log.out phases.out | egrep -v '^---|^\+\+\+|^@@'
142 142 +phase: draft
143 143 +phase: draft
144 144 +phase: draft
145 145 +phase: draft
146 146 +phase: draft
147 147 +phase: draft
148 148 +phase: draft
149 149 +phase: draft
150 150 +phase: draft
151 151 +phase: draft
152 152
153 153 $ hg log -q > log.out
154 154 $ hg log -q --style default > style.out
155 155 $ cmp log.out style.out || diff -u log.out style.out
156 156 $ hg log -q -T phases > phases.out
157 157 $ cmp log.out phases.out || diff -u log.out phases.out
158 158
159 159 $ hg log --debug > log.out
160 160 $ hg log --debug --style default > style.out
161 161 $ cmp log.out style.out || diff -u log.out style.out
162 162 $ hg log --debug -T phases > phases.out
163 163 $ cmp log.out phases.out || diff -u log.out phases.out
164 164
165 165 Default style of working-directory revision should also be the same (but
166 166 date may change while running tests):
167 167
168 168 $ hg log -r 'wdir()' | sed 's|^date:.*|date:|' > log.out
169 169 $ hg log -r 'wdir()' --style default | sed 's|^date:.*|date:|' > style.out
170 170 $ cmp log.out style.out || diff -u log.out style.out
171 171
172 172 $ hg log -r 'wdir()' -v | sed 's|^date:.*|date:|' > log.out
173 173 $ hg log -r 'wdir()' -v --style default | sed 's|^date:.*|date:|' > style.out
174 174 $ cmp log.out style.out || diff -u log.out style.out
175 175
176 176 $ hg log -r 'wdir()' -q > log.out
177 177 $ hg log -r 'wdir()' -q --style default > style.out
178 178 $ cmp log.out style.out || diff -u log.out style.out
179 179
180 180 $ hg log -r 'wdir()' --debug | sed 's|^date:.*|date:|' > log.out
181 181 $ hg log -r 'wdir()' --debug --style default \
182 182 > | sed 's|^date:.*|date:|' > style.out
183 183 $ cmp log.out style.out || diff -u log.out style.out
184 184
185 185 Default style should also preserve color information (issue2866):
186 186
187 187 $ cp $HGRCPATH $HGRCPATH-bak
188 188 $ cat <<EOF >> $HGRCPATH
189 189 > [extensions]
190 190 > color=
191 191 > EOF
192 192
193 193 $ hg --color=debug log > log.out
194 194 $ hg --color=debug log --style default > style.out
195 195 $ cmp log.out style.out || diff -u log.out style.out
196 196 $ hg --color=debug log -T phases > phases.out
197 197 $ diff -U 0 log.out phases.out | egrep -v '^---|^\+\+\+|^@@'
198 198 +[log.phase|phase: draft]
199 199 +[log.phase|phase: draft]
200 200 +[log.phase|phase: draft]
201 201 +[log.phase|phase: draft]
202 202 +[log.phase|phase: draft]
203 203 +[log.phase|phase: draft]
204 204 +[log.phase|phase: draft]
205 205 +[log.phase|phase: draft]
206 206 +[log.phase|phase: draft]
207 207 +[log.phase|phase: draft]
208 208
209 209 $ hg --color=debug -v log > log.out
210 210 $ hg --color=debug -v log --style default > style.out
211 211 $ cmp log.out style.out || diff -u log.out style.out
212 212 $ hg --color=debug -v log -T phases > phases.out
213 213 $ diff -U 0 log.out phases.out | egrep -v '^---|^\+\+\+|^@@'
214 214 +[log.phase|phase: draft]
215 215 +[log.phase|phase: draft]
216 216 +[log.phase|phase: draft]
217 217 +[log.phase|phase: draft]
218 218 +[log.phase|phase: draft]
219 219 +[log.phase|phase: draft]
220 220 +[log.phase|phase: draft]
221 221 +[log.phase|phase: draft]
222 222 +[log.phase|phase: draft]
223 223 +[log.phase|phase: draft]
224 224
225 225 $ hg --color=debug -q log > log.out
226 226 $ hg --color=debug -q log --style default > style.out
227 227 $ cmp log.out style.out || diff -u log.out style.out
228 228 $ hg --color=debug -q log -T phases > phases.out
229 229 $ cmp log.out phases.out || diff -u log.out phases.out
230 230
231 231 $ hg --color=debug --debug log > log.out
232 232 $ hg --color=debug --debug log --style default > style.out
233 233 $ cmp log.out style.out || diff -u log.out style.out
234 234 $ hg --color=debug --debug log -T phases > phases.out
235 235 $ cmp log.out phases.out || diff -u log.out phases.out
236 236
237 237 $ mv $HGRCPATH-bak $HGRCPATH
238 238
239 239 Remove commit with empty commit message, so as to not pollute further
240 240 tests.
241 241
242 242 $ hg --config extensions.strip= strip -q .
243 243
244 244 Revision with no copies (used to print a traceback):
245 245
246 246 $ hg tip -v --template '\n'
247 247
248 248
249 249 Compact style works:
250 250
251 251 $ hg log -Tcompact
252 252 8[tip] 95c24699272e 2020-01-01 10:01 +0000 test
253 253 third
254 254
255 255 7:-1 29114dbae42b 1970-01-12 13:46 +0000 user
256 256 second
257 257
258 258 6:5,4 d41e714fe50d 1970-01-18 08:40 +0000 person
259 259 merge
260 260
261 261 5:3 13207e5a10d9 1970-01-18 08:40 +0000 person
262 262 new head
263 263
264 264 4 bbe44766e73d 1970-01-17 04:53 +0000 person
265 265 new branch
266 266
267 267 3 10e46f2dcbf4 1970-01-16 01:06 +0000 person
268 268 no user, no domain
269 269
270 270 2 97054abb4ab8 1970-01-14 21:20 +0000 other
271 271 no person
272 272
273 273 1 b608e9d1a3f0 1970-01-13 17:33 +0000 other
274 274 other 1
275 275
276 276 0 1e4e1b8f71e0 1970-01-12 13:46 +0000 user
277 277 line 1
278 278
279 279
280 280 $ hg log -v --style compact
281 281 8[tip] 95c24699272e 2020-01-01 10:01 +0000 test
282 282 third
283 283
284 284 7:-1 29114dbae42b 1970-01-12 13:46 +0000 User Name <user@hostname>
285 285 second
286 286
287 287 6:5,4 d41e714fe50d 1970-01-18 08:40 +0000 person
288 288 merge
289 289
290 290 5:3 13207e5a10d9 1970-01-18 08:40 +0000 person
291 291 new head
292 292
293 293 4 bbe44766e73d 1970-01-17 04:53 +0000 person
294 294 new branch
295 295
296 296 3 10e46f2dcbf4 1970-01-16 01:06 +0000 person
297 297 no user, no domain
298 298
299 299 2 97054abb4ab8 1970-01-14 21:20 +0000 other@place
300 300 no person
301 301
302 302 1 b608e9d1a3f0 1970-01-13 17:33 +0000 A. N. Other <other@place>
303 303 other 1
304 304 other 2
305 305
306 306 other 3
307 307
308 308 0 1e4e1b8f71e0 1970-01-12 13:46 +0000 User Name <user@hostname>
309 309 line 1
310 310 line 2
311 311
312 312
313 313 $ hg log --debug --style compact
314 314 8[tip]:7,-1 95c24699272e 2020-01-01 10:01 +0000 test
315 315 third
316 316
317 317 7:-1,-1 29114dbae42b 1970-01-12 13:46 +0000 User Name <user@hostname>
318 318 second
319 319
320 320 6:5,4 d41e714fe50d 1970-01-18 08:40 +0000 person
321 321 merge
322 322
323 323 5:3,-1 13207e5a10d9 1970-01-18 08:40 +0000 person
324 324 new head
325 325
326 326 4:3,-1 bbe44766e73d 1970-01-17 04:53 +0000 person
327 327 new branch
328 328
329 329 3:2,-1 10e46f2dcbf4 1970-01-16 01:06 +0000 person
330 330 no user, no domain
331 331
332 332 2:1,-1 97054abb4ab8 1970-01-14 21:20 +0000 other@place
333 333 no person
334 334
335 335 1:0,-1 b608e9d1a3f0 1970-01-13 17:33 +0000 A. N. Other <other@place>
336 336 other 1
337 337 other 2
338 338
339 339 other 3
340 340
341 341 0:-1,-1 1e4e1b8f71e0 1970-01-12 13:46 +0000 User Name <user@hostname>
342 342 line 1
343 343 line 2
344 344
345 345
346 346 Test xml styles:
347 347
348 348 $ hg log --style xml -r 'not all()'
349 349 <?xml version="1.0"?>
350 350 <log>
351 351 </log>
352 352
353 353 $ hg log --style xml
354 354 <?xml version="1.0"?>
355 355 <log>
356 356 <logentry revision="8" node="95c24699272ef57d062b8bccc32c878bf841784a">
357 357 <tag>tip</tag>
358 358 <author email="test">test</author>
359 359 <date>2020-01-01T10:01:00+00:00</date>
360 360 <msg xml:space="preserve">third</msg>
361 361 </logentry>
362 362 <logentry revision="7" node="29114dbae42b9f078cf2714dbe3a86bba8ec7453">
363 363 <parent revision="-1" node="0000000000000000000000000000000000000000" />
364 364 <author email="user@hostname">User Name</author>
365 365 <date>1970-01-12T13:46:40+00:00</date>
366 366 <msg xml:space="preserve">second</msg>
367 367 </logentry>
368 368 <logentry revision="6" node="d41e714fe50d9e4a5f11b4d595d543481b5f980b">
369 369 <parent revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f" />
370 370 <parent revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74" />
371 371 <author email="person">person</author>
372 372 <date>1970-01-18T08:40:01+00:00</date>
373 373 <msg xml:space="preserve">merge</msg>
374 374 </logentry>
375 375 <logentry revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f">
376 376 <parent revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47" />
377 377 <author email="person">person</author>
378 378 <date>1970-01-18T08:40:00+00:00</date>
379 379 <msg xml:space="preserve">new head</msg>
380 380 </logentry>
381 381 <logentry revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74">
382 382 <branch>foo</branch>
383 383 <author email="person">person</author>
384 384 <date>1970-01-17T04:53:20+00:00</date>
385 385 <msg xml:space="preserve">new branch</msg>
386 386 </logentry>
387 387 <logentry revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47">
388 388 <author email="person">person</author>
389 389 <date>1970-01-16T01:06:40+00:00</date>
390 390 <msg xml:space="preserve">no user, no domain</msg>
391 391 </logentry>
392 392 <logentry revision="2" node="97054abb4ab824450e9164180baf491ae0078465">
393 393 <author email="other@place">other</author>
394 394 <date>1970-01-14T21:20:00+00:00</date>
395 395 <msg xml:space="preserve">no person</msg>
396 396 </logentry>
397 397 <logentry revision="1" node="b608e9d1a3f0273ccf70fb85fd6866b3482bf965">
398 398 <author email="other@place">A. N. Other</author>
399 399 <date>1970-01-13T17:33:20+00:00</date>
400 400 <msg xml:space="preserve">other 1
401 401 other 2
402 402
403 403 other 3</msg>
404 404 </logentry>
405 405 <logentry revision="0" node="1e4e1b8f71e05681d422154f5421e385fec3454f">
406 406 <author email="user@hostname">User Name</author>
407 407 <date>1970-01-12T13:46:40+00:00</date>
408 408 <msg xml:space="preserve">line 1
409 409 line 2</msg>
410 410 </logentry>
411 411 </log>
412 412
413 413 $ hg log -v --style xml
414 414 <?xml version="1.0"?>
415 415 <log>
416 416 <logentry revision="8" node="95c24699272ef57d062b8bccc32c878bf841784a">
417 417 <tag>tip</tag>
418 418 <author email="test">test</author>
419 419 <date>2020-01-01T10:01:00+00:00</date>
420 420 <msg xml:space="preserve">third</msg>
421 421 <paths>
422 422 <path action="A">fourth</path>
423 423 <path action="A">third</path>
424 424 <path action="R">second</path>
425 425 </paths>
426 426 <copies>
427 427 <copy source="second">fourth</copy>
428 428 </copies>
429 429 </logentry>
430 430 <logentry revision="7" node="29114dbae42b9f078cf2714dbe3a86bba8ec7453">
431 431 <parent revision="-1" node="0000000000000000000000000000000000000000" />
432 432 <author email="user@hostname">User Name</author>
433 433 <date>1970-01-12T13:46:40+00:00</date>
434 434 <msg xml:space="preserve">second</msg>
435 435 <paths>
436 436 <path action="A">second</path>
437 437 </paths>
438 438 </logentry>
439 439 <logentry revision="6" node="d41e714fe50d9e4a5f11b4d595d543481b5f980b">
440 440 <parent revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f" />
441 441 <parent revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74" />
442 442 <author email="person">person</author>
443 443 <date>1970-01-18T08:40:01+00:00</date>
444 444 <msg xml:space="preserve">merge</msg>
445 445 <paths>
446 446 </paths>
447 447 </logentry>
448 448 <logentry revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f">
449 449 <parent revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47" />
450 450 <author email="person">person</author>
451 451 <date>1970-01-18T08:40:00+00:00</date>
452 452 <msg xml:space="preserve">new head</msg>
453 453 <paths>
454 454 <path action="A">d</path>
455 455 </paths>
456 456 </logentry>
457 457 <logentry revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74">
458 458 <branch>foo</branch>
459 459 <author email="person">person</author>
460 460 <date>1970-01-17T04:53:20+00:00</date>
461 461 <msg xml:space="preserve">new branch</msg>
462 462 <paths>
463 463 </paths>
464 464 </logentry>
465 465 <logentry revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47">
466 466 <author email="person">person</author>
467 467 <date>1970-01-16T01:06:40+00:00</date>
468 468 <msg xml:space="preserve">no user, no domain</msg>
469 469 <paths>
470 470 <path action="M">c</path>
471 471 </paths>
472 472 </logentry>
473 473 <logentry revision="2" node="97054abb4ab824450e9164180baf491ae0078465">
474 474 <author email="other@place">other</author>
475 475 <date>1970-01-14T21:20:00+00:00</date>
476 476 <msg xml:space="preserve">no person</msg>
477 477 <paths>
478 478 <path action="A">c</path>
479 479 </paths>
480 480 </logentry>
481 481 <logentry revision="1" node="b608e9d1a3f0273ccf70fb85fd6866b3482bf965">
482 482 <author email="other@place">A. N. Other</author>
483 483 <date>1970-01-13T17:33:20+00:00</date>
484 484 <msg xml:space="preserve">other 1
485 485 other 2
486 486
487 487 other 3</msg>
488 488 <paths>
489 489 <path action="A">b</path>
490 490 </paths>
491 491 </logentry>
492 492 <logentry revision="0" node="1e4e1b8f71e05681d422154f5421e385fec3454f">
493 493 <author email="user@hostname">User Name</author>
494 494 <date>1970-01-12T13:46:40+00:00</date>
495 495 <msg xml:space="preserve">line 1
496 496 line 2</msg>
497 497 <paths>
498 498 <path action="A">a</path>
499 499 </paths>
500 500 </logentry>
501 501 </log>
502 502
503 503 $ hg log --debug --style xml
504 504 <?xml version="1.0"?>
505 505 <log>
506 506 <logentry revision="8" node="95c24699272ef57d062b8bccc32c878bf841784a">
507 507 <tag>tip</tag>
508 508 <parent revision="7" node="29114dbae42b9f078cf2714dbe3a86bba8ec7453" />
509 509 <parent revision="-1" node="0000000000000000000000000000000000000000" />
510 510 <author email="test">test</author>
511 511 <date>2020-01-01T10:01:00+00:00</date>
512 512 <msg xml:space="preserve">third</msg>
513 513 <paths>
514 514 <path action="A">fourth</path>
515 515 <path action="A">third</path>
516 516 <path action="R">second</path>
517 517 </paths>
518 518 <copies>
519 519 <copy source="second">fourth</copy>
520 520 </copies>
521 521 <extra key="branch">default</extra>
522 522 </logentry>
523 523 <logentry revision="7" node="29114dbae42b9f078cf2714dbe3a86bba8ec7453">
524 524 <parent revision="-1" node="0000000000000000000000000000000000000000" />
525 525 <parent revision="-1" node="0000000000000000000000000000000000000000" />
526 526 <author email="user@hostname">User Name</author>
527 527 <date>1970-01-12T13:46:40+00:00</date>
528 528 <msg xml:space="preserve">second</msg>
529 529 <paths>
530 530 <path action="A">second</path>
531 531 </paths>
532 532 <extra key="branch">default</extra>
533 533 </logentry>
534 534 <logentry revision="6" node="d41e714fe50d9e4a5f11b4d595d543481b5f980b">
535 535 <parent revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f" />
536 536 <parent revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74" />
537 537 <author email="person">person</author>
538 538 <date>1970-01-18T08:40:01+00:00</date>
539 539 <msg xml:space="preserve">merge</msg>
540 540 <paths>
541 541 </paths>
542 542 <extra key="branch">default</extra>
543 543 </logentry>
544 544 <logentry revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f">
545 545 <parent revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47" />
546 546 <parent revision="-1" node="0000000000000000000000000000000000000000" />
547 547 <author email="person">person</author>
548 548 <date>1970-01-18T08:40:00+00:00</date>
549 549 <msg xml:space="preserve">new head</msg>
550 550 <paths>
551 551 <path action="A">d</path>
552 552 </paths>
553 553 <extra key="branch">default</extra>
554 554 </logentry>
555 555 <logentry revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74">
556 556 <branch>foo</branch>
557 557 <parent revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47" />
558 558 <parent revision="-1" node="0000000000000000000000000000000000000000" />
559 559 <author email="person">person</author>
560 560 <date>1970-01-17T04:53:20+00:00</date>
561 561 <msg xml:space="preserve">new branch</msg>
562 562 <paths>
563 563 </paths>
564 564 <extra key="branch">foo</extra>
565 565 </logentry>
566 566 <logentry revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47">
567 567 <parent revision="2" node="97054abb4ab824450e9164180baf491ae0078465" />
568 568 <parent revision="-1" node="0000000000000000000000000000000000000000" />
569 569 <author email="person">person</author>
570 570 <date>1970-01-16T01:06:40+00:00</date>
571 571 <msg xml:space="preserve">no user, no domain</msg>
572 572 <paths>
573 573 <path action="M">c</path>
574 574 </paths>
575 575 <extra key="branch">default</extra>
576 576 </logentry>
577 577 <logentry revision="2" node="97054abb4ab824450e9164180baf491ae0078465">
578 578 <parent revision="1" node="b608e9d1a3f0273ccf70fb85fd6866b3482bf965" />
579 579 <parent revision="-1" node="0000000000000000000000000000000000000000" />
580 580 <author email="other@place">other</author>
581 581 <date>1970-01-14T21:20:00+00:00</date>
582 582 <msg xml:space="preserve">no person</msg>
583 583 <paths>
584 584 <path action="A">c</path>
585 585 </paths>
586 586 <extra key="branch">default</extra>
587 587 </logentry>
588 588 <logentry revision="1" node="b608e9d1a3f0273ccf70fb85fd6866b3482bf965">
589 589 <parent revision="0" node="1e4e1b8f71e05681d422154f5421e385fec3454f" />
590 590 <parent revision="-1" node="0000000000000000000000000000000000000000" />
591 591 <author email="other@place">A. N. Other</author>
592 592 <date>1970-01-13T17:33:20+00:00</date>
593 593 <msg xml:space="preserve">other 1
594 594 other 2
595 595
596 596 other 3</msg>
597 597 <paths>
598 598 <path action="A">b</path>
599 599 </paths>
600 600 <extra key="branch">default</extra>
601 601 </logentry>
602 602 <logentry revision="0" node="1e4e1b8f71e05681d422154f5421e385fec3454f">
603 603 <parent revision="-1" node="0000000000000000000000000000000000000000" />
604 604 <parent revision="-1" node="0000000000000000000000000000000000000000" />
605 605 <author email="user@hostname">User Name</author>
606 606 <date>1970-01-12T13:46:40+00:00</date>
607 607 <msg xml:space="preserve">line 1
608 608 line 2</msg>
609 609 <paths>
610 610 <path action="A">a</path>
611 611 </paths>
612 612 <extra key="branch">default</extra>
613 613 </logentry>
614 614 </log>
615 615
616 616
617 617 Test JSON style:
618 618
619 619 $ hg log -k nosuch -Tjson
620 620 []
621 621
622 622 $ hg log -qr . -Tjson
623 623 [
624 624 {
625 625 "rev": 8,
626 626 "node": "95c24699272ef57d062b8bccc32c878bf841784a"
627 627 }
628 628 ]
629 629
630 630 $ hg log -vpr . -Tjson --stat
631 631 [
632 632 {
633 633 "rev": 8,
634 634 "node": "95c24699272ef57d062b8bccc32c878bf841784a",
635 635 "branch": "default",
636 636 "phase": "draft",
637 637 "user": "test",
638 638 "date": [1577872860, 0],
639 639 "desc": "third",
640 640 "bookmarks": [],
641 641 "tags": ["tip"],
642 642 "parents": ["29114dbae42b9f078cf2714dbe3a86bba8ec7453"],
643 643 "files": ["fourth", "second", "third"],
644 644 "diffstat": " fourth | 1 +\n second | 1 -\n third | 1 +\n 3 files changed, 2 insertions(+), 1 deletions(-)\n",
645 645 "diff": "diff -r 29114dbae42b -r 95c24699272e fourth\n--- /dev/null\tThu Jan 01 00:00:00 1970 +0000\n+++ b/fourth\tWed Jan 01 10:01:00 2020 +0000\n@@ -0,0 +1,1 @@\n+second\ndiff -r 29114dbae42b -r 95c24699272e second\n--- a/second\tMon Jan 12 13:46:40 1970 +0000\n+++ /dev/null\tThu Jan 01 00:00:00 1970 +0000\n@@ -1,1 +0,0 @@\n-second\ndiff -r 29114dbae42b -r 95c24699272e third\n--- /dev/null\tThu Jan 01 00:00:00 1970 +0000\n+++ b/third\tWed Jan 01 10:01:00 2020 +0000\n@@ -0,0 +1,1 @@\n+third\n"
646 646 }
647 647 ]
648 648
649 649 honor --git but not format-breaking diffopts
650 650 $ hg --config diff.noprefix=True log --git -vpr . -Tjson
651 651 [
652 652 {
653 653 "rev": 8,
654 654 "node": "95c24699272ef57d062b8bccc32c878bf841784a",
655 655 "branch": "default",
656 656 "phase": "draft",
657 657 "user": "test",
658 658 "date": [1577872860, 0],
659 659 "desc": "third",
660 660 "bookmarks": [],
661 661 "tags": ["tip"],
662 662 "parents": ["29114dbae42b9f078cf2714dbe3a86bba8ec7453"],
663 663 "files": ["fourth", "second", "third"],
664 664 "diff": "diff --git a/second b/fourth\nrename from second\nrename to fourth\ndiff --git a/third b/third\nnew file mode 100644\n--- /dev/null\n+++ b/third\n@@ -0,0 +1,1 @@\n+third\n"
665 665 }
666 666 ]
667 667
668 668 $ hg log -T json
669 669 [
670 670 {
671 671 "rev": 8,
672 672 "node": "95c24699272ef57d062b8bccc32c878bf841784a",
673 673 "branch": "default",
674 674 "phase": "draft",
675 675 "user": "test",
676 676 "date": [1577872860, 0],
677 677 "desc": "third",
678 678 "bookmarks": [],
679 679 "tags": ["tip"],
680 680 "parents": ["29114dbae42b9f078cf2714dbe3a86bba8ec7453"]
681 681 },
682 682 {
683 683 "rev": 7,
684 684 "node": "29114dbae42b9f078cf2714dbe3a86bba8ec7453",
685 685 "branch": "default",
686 686 "phase": "draft",
687 687 "user": "User Name <user@hostname>",
688 688 "date": [1000000, 0],
689 689 "desc": "second",
690 690 "bookmarks": [],
691 691 "tags": [],
692 692 "parents": ["0000000000000000000000000000000000000000"]
693 693 },
694 694 {
695 695 "rev": 6,
696 696 "node": "d41e714fe50d9e4a5f11b4d595d543481b5f980b",
697 697 "branch": "default",
698 698 "phase": "draft",
699 699 "user": "person",
700 700 "date": [1500001, 0],
701 701 "desc": "merge",
702 702 "bookmarks": [],
703 703 "tags": [],
704 704 "parents": ["13207e5a10d9fd28ec424934298e176197f2c67f", "bbe44766e73d5f11ed2177f1838de10c53ef3e74"]
705 705 },
706 706 {
707 707 "rev": 5,
708 708 "node": "13207e5a10d9fd28ec424934298e176197f2c67f",
709 709 "branch": "default",
710 710 "phase": "draft",
711 711 "user": "person",
712 712 "date": [1500000, 0],
713 713 "desc": "new head",
714 714 "bookmarks": [],
715 715 "tags": [],
716 716 "parents": ["10e46f2dcbf4823578cf180f33ecf0b957964c47"]
717 717 },
718 718 {
719 719 "rev": 4,
720 720 "node": "bbe44766e73d5f11ed2177f1838de10c53ef3e74",
721 721 "branch": "foo",
722 722 "phase": "draft",
723 723 "user": "person",
724 724 "date": [1400000, 0],
725 725 "desc": "new branch",
726 726 "bookmarks": [],
727 727 "tags": [],
728 728 "parents": ["10e46f2dcbf4823578cf180f33ecf0b957964c47"]
729 729 },
730 730 {
731 731 "rev": 3,
732 732 "node": "10e46f2dcbf4823578cf180f33ecf0b957964c47",
733 733 "branch": "default",
734 734 "phase": "draft",
735 735 "user": "person",
736 736 "date": [1300000, 0],
737 737 "desc": "no user, no domain",
738 738 "bookmarks": [],
739 739 "tags": [],
740 740 "parents": ["97054abb4ab824450e9164180baf491ae0078465"]
741 741 },
742 742 {
743 743 "rev": 2,
744 744 "node": "97054abb4ab824450e9164180baf491ae0078465",
745 745 "branch": "default",
746 746 "phase": "draft",
747 747 "user": "other@place",
748 748 "date": [1200000, 0],
749 749 "desc": "no person",
750 750 "bookmarks": [],
751 751 "tags": [],
752 752 "parents": ["b608e9d1a3f0273ccf70fb85fd6866b3482bf965"]
753 753 },
754 754 {
755 755 "rev": 1,
756 756 "node": "b608e9d1a3f0273ccf70fb85fd6866b3482bf965",
757 757 "branch": "default",
758 758 "phase": "draft",
759 759 "user": "A. N. Other <other@place>",
760 760 "date": [1100000, 0],
761 761 "desc": "other 1\nother 2\n\nother 3",
762 762 "bookmarks": [],
763 763 "tags": [],
764 764 "parents": ["1e4e1b8f71e05681d422154f5421e385fec3454f"]
765 765 },
766 766 {
767 767 "rev": 0,
768 768 "node": "1e4e1b8f71e05681d422154f5421e385fec3454f",
769 769 "branch": "default",
770 770 "phase": "draft",
771 771 "user": "User Name <user@hostname>",
772 772 "date": [1000000, 0],
773 773 "desc": "line 1\nline 2",
774 774 "bookmarks": [],
775 775 "tags": [],
776 776 "parents": ["0000000000000000000000000000000000000000"]
777 777 }
778 778 ]
779 779
780 780 $ hg heads -v -Tjson
781 781 [
782 782 {
783 783 "rev": 8,
784 784 "node": "95c24699272ef57d062b8bccc32c878bf841784a",
785 785 "branch": "default",
786 786 "phase": "draft",
787 787 "user": "test",
788 788 "date": [1577872860, 0],
789 789 "desc": "third",
790 790 "bookmarks": [],
791 791 "tags": ["tip"],
792 792 "parents": ["29114dbae42b9f078cf2714dbe3a86bba8ec7453"],
793 793 "files": ["fourth", "second", "third"]
794 794 },
795 795 {
796 796 "rev": 6,
797 797 "node": "d41e714fe50d9e4a5f11b4d595d543481b5f980b",
798 798 "branch": "default",
799 799 "phase": "draft",
800 800 "user": "person",
801 801 "date": [1500001, 0],
802 802 "desc": "merge",
803 803 "bookmarks": [],
804 804 "tags": [],
805 805 "parents": ["13207e5a10d9fd28ec424934298e176197f2c67f", "bbe44766e73d5f11ed2177f1838de10c53ef3e74"],
806 806 "files": []
807 807 },
808 808 {
809 809 "rev": 4,
810 810 "node": "bbe44766e73d5f11ed2177f1838de10c53ef3e74",
811 811 "branch": "foo",
812 812 "phase": "draft",
813 813 "user": "person",
814 814 "date": [1400000, 0],
815 815 "desc": "new branch",
816 816 "bookmarks": [],
817 817 "tags": [],
818 818 "parents": ["10e46f2dcbf4823578cf180f33ecf0b957964c47"],
819 819 "files": []
820 820 }
821 821 ]
822 822
823 823 $ hg log --debug -Tjson
824 824 [
825 825 {
826 826 "rev": 8,
827 827 "node": "95c24699272ef57d062b8bccc32c878bf841784a",
828 828 "branch": "default",
829 829 "phase": "draft",
830 830 "user": "test",
831 831 "date": [1577872860, 0],
832 832 "desc": "third",
833 833 "bookmarks": [],
834 834 "tags": ["tip"],
835 835 "parents": ["29114dbae42b9f078cf2714dbe3a86bba8ec7453"],
836 836 "manifest": "94961b75a2da554b4df6fb599e5bfc7d48de0c64",
837 837 "extra": {"branch": "default"},
838 838 "modified": [],
839 839 "added": ["fourth", "third"],
840 840 "removed": ["second"]
841 841 },
842 842 {
843 843 "rev": 7,
844 844 "node": "29114dbae42b9f078cf2714dbe3a86bba8ec7453",
845 845 "branch": "default",
846 846 "phase": "draft",
847 847 "user": "User Name <user@hostname>",
848 848 "date": [1000000, 0],
849 849 "desc": "second",
850 850 "bookmarks": [],
851 851 "tags": [],
852 852 "parents": ["0000000000000000000000000000000000000000"],
853 853 "manifest": "f2dbc354b94e5ec0b4f10680ee0cee816101d0bf",
854 854 "extra": {"branch": "default"},
855 855 "modified": [],
856 856 "added": ["second"],
857 857 "removed": []
858 858 },
859 859 {
860 860 "rev": 6,
861 861 "node": "d41e714fe50d9e4a5f11b4d595d543481b5f980b",
862 862 "branch": "default",
863 863 "phase": "draft",
864 864 "user": "person",
865 865 "date": [1500001, 0],
866 866 "desc": "merge",
867 867 "bookmarks": [],
868 868 "tags": [],
869 869 "parents": ["13207e5a10d9fd28ec424934298e176197f2c67f", "bbe44766e73d5f11ed2177f1838de10c53ef3e74"],
870 870 "manifest": "4dc3def4f9b4c6e8de820f6ee74737f91e96a216",
871 871 "extra": {"branch": "default"},
872 872 "modified": [],
873 873 "added": [],
874 874 "removed": []
875 875 },
876 876 {
877 877 "rev": 5,
878 878 "node": "13207e5a10d9fd28ec424934298e176197f2c67f",
879 879 "branch": "default",
880 880 "phase": "draft",
881 881 "user": "person",
882 882 "date": [1500000, 0],
883 883 "desc": "new head",
884 884 "bookmarks": [],
885 885 "tags": [],
886 886 "parents": ["10e46f2dcbf4823578cf180f33ecf0b957964c47"],
887 887 "manifest": "4dc3def4f9b4c6e8de820f6ee74737f91e96a216",
888 888 "extra": {"branch": "default"},
889 889 "modified": [],
890 890 "added": ["d"],
891 891 "removed": []
892 892 },
893 893 {
894 894 "rev": 4,
895 895 "node": "bbe44766e73d5f11ed2177f1838de10c53ef3e74",
896 896 "branch": "foo",
897 897 "phase": "draft",
898 898 "user": "person",
899 899 "date": [1400000, 0],
900 900 "desc": "new branch",
901 901 "bookmarks": [],
902 902 "tags": [],
903 903 "parents": ["10e46f2dcbf4823578cf180f33ecf0b957964c47"],
904 904 "manifest": "cb5a1327723bada42f117e4c55a303246eaf9ccc",
905 905 "extra": {"branch": "foo"},
906 906 "modified": [],
907 907 "added": [],
908 908 "removed": []
909 909 },
910 910 {
911 911 "rev": 3,
912 912 "node": "10e46f2dcbf4823578cf180f33ecf0b957964c47",
913 913 "branch": "default",
914 914 "phase": "draft",
915 915 "user": "person",
916 916 "date": [1300000, 0],
917 917 "desc": "no user, no domain",
918 918 "bookmarks": [],
919 919 "tags": [],
920 920 "parents": ["97054abb4ab824450e9164180baf491ae0078465"],
921 921 "manifest": "cb5a1327723bada42f117e4c55a303246eaf9ccc",
922 922 "extra": {"branch": "default"},
923 923 "modified": ["c"],
924 924 "added": [],
925 925 "removed": []
926 926 },
927 927 {
928 928 "rev": 2,
929 929 "node": "97054abb4ab824450e9164180baf491ae0078465",
930 930 "branch": "default",
931 931 "phase": "draft",
932 932 "user": "other@place",
933 933 "date": [1200000, 0],
934 934 "desc": "no person",
935 935 "bookmarks": [],
936 936 "tags": [],
937 937 "parents": ["b608e9d1a3f0273ccf70fb85fd6866b3482bf965"],
938 938 "manifest": "6e0e82995c35d0d57a52aca8da4e56139e06b4b1",
939 939 "extra": {"branch": "default"},
940 940 "modified": [],
941 941 "added": ["c"],
942 942 "removed": []
943 943 },
944 944 {
945 945 "rev": 1,
946 946 "node": "b608e9d1a3f0273ccf70fb85fd6866b3482bf965",
947 947 "branch": "default",
948 948 "phase": "draft",
949 949 "user": "A. N. Other <other@place>",
950 950 "date": [1100000, 0],
951 951 "desc": "other 1\nother 2\n\nother 3",
952 952 "bookmarks": [],
953 953 "tags": [],
954 954 "parents": ["1e4e1b8f71e05681d422154f5421e385fec3454f"],
955 955 "manifest": "4e8d705b1e53e3f9375e0e60dc7b525d8211fe55",
956 956 "extra": {"branch": "default"},
957 957 "modified": [],
958 958 "added": ["b"],
959 959 "removed": []
960 960 },
961 961 {
962 962 "rev": 0,
963 963 "node": "1e4e1b8f71e05681d422154f5421e385fec3454f",
964 964 "branch": "default",
965 965 "phase": "draft",
966 966 "user": "User Name <user@hostname>",
967 967 "date": [1000000, 0],
968 968 "desc": "line 1\nline 2",
969 969 "bookmarks": [],
970 970 "tags": [],
971 971 "parents": ["0000000000000000000000000000000000000000"],
972 972 "manifest": "a0c8bcbbb45c63b90b70ad007bf38961f64f2af0",
973 973 "extra": {"branch": "default"},
974 974 "modified": [],
975 975 "added": ["a"],
976 976 "removed": []
977 977 }
978 978 ]
979 979
980 980 Error if style not readable:
981 981
982 982 #if unix-permissions no-root
983 983 $ touch q
984 984 $ chmod 0 q
985 985 $ hg log --style ./q
986 986 abort: Permission denied: ./q
987 987 [255]
988 988 #endif
989 989
990 990 Error if no style:
991 991
992 992 $ hg log --style notexist
993 993 abort: style 'notexist' not found
994 994 (available styles: bisect, changelog, compact, default, phases, status, xml)
995 995 [255]
996 996
997 997 $ hg log -T list
998 998 available styles: bisect, changelog, compact, default, phases, status, xml
999 999 abort: specify a template
1000 1000 [255]
1001 1001
1002 1002 Error if style missing key:
1003 1003
1004 1004 $ echo 'q = q' > t
1005 1005 $ hg log --style ./t
1006 1006 abort: "changeset" not in template map
1007 1007 [255]
1008 1008
1009 1009 Error if style missing value:
1010 1010
1011 1011 $ echo 'changeset =' > t
1012 1012 $ hg log --style t
1013 1013 hg: parse error at t:1: missing value
1014 1014 [255]
1015 1015
1016 1016 Error if include fails:
1017 1017
1018 1018 $ echo 'changeset = q' >> t
1019 1019 #if unix-permissions no-root
1020 1020 $ hg log --style ./t
1021 1021 abort: template file ./q: Permission denied
1022 1022 [255]
1023 1023 $ rm -f q
1024 1024 #endif
1025 1025
1026 1026 Include works:
1027 1027
1028 1028 $ echo '{rev}' > q
1029 1029 $ hg log --style ./t
1030 1030 8
1031 1031 7
1032 1032 6
1033 1033 5
1034 1034 4
1035 1035 3
1036 1036 2
1037 1037 1
1038 1038 0
1039 1039
1040 1040 Check that recursive reference does not fall into RuntimeError (issue4758):
1041 1041
1042 1042 common mistake:
1043 1043
1044 1044 $ hg log -T '{changeset}\n'
1045 1045 abort: recursive reference 'changeset' in template
1046 1046 [255]
1047 1047
1048 1048 circular reference:
1049 1049
1050 1050 $ cat << EOF > issue4758
1051 1051 > changeset = '{foo}'
1052 1052 > foo = '{changeset}'
1053 1053 > EOF
1054 1054 $ hg log --style ./issue4758
1055 1055 abort: recursive reference 'foo' in template
1056 1056 [255]
1057 1057
1058 1058 buildmap() -> gettemplate(), where no thunk was made:
1059 1059
1060 1060 $ hg log -T '{files % changeset}\n'
1061 1061 abort: recursive reference 'changeset' in template
1062 1062 [255]
1063 1063
1064 1064 not a recursion if a keyword of the same name exists:
1065 1065
1066 1066 $ cat << EOF > issue4758
1067 1067 > changeset = '{tags % rev}'
1068 1068 > rev = '{rev} {tag}\n'
1069 1069 > EOF
1070 1070 $ hg log --style ./issue4758 -r tip
1071 1071 8 tip
1072 1072
1073 1073 Check that {phase} works correctly on parents:
1074 1074
1075 1075 $ cat << EOF > parentphase
1076 1076 > changeset_debug = '{rev} ({phase}):{parents}\n'
1077 1077 > parent = ' {rev} ({phase})'
1078 1078 > EOF
1079 1079 $ hg phase -r 5 --public
1080 1080 $ hg phase -r 7 --secret --force
1081 1081 $ hg log --debug -G --style ./parentphase
1082 1082 @ 8 (secret): 7 (secret) -1 (public)
1083 1083 |
1084 1084 o 7 (secret): -1 (public) -1 (public)
1085 1085
1086 1086 o 6 (draft): 5 (public) 4 (draft)
1087 1087 |\
1088 1088 | o 5 (public): 3 (public) -1 (public)
1089 1089 | |
1090 1090 o | 4 (draft): 3 (public) -1 (public)
1091 1091 |/
1092 1092 o 3 (public): 2 (public) -1 (public)
1093 1093 |
1094 1094 o 2 (public): 1 (public) -1 (public)
1095 1095 |
1096 1096 o 1 (public): 0 (public) -1 (public)
1097 1097 |
1098 1098 o 0 (public): -1 (public) -1 (public)
1099 1099
1100 1100
1101 1101 Missing non-standard names give no error (backward compatibility):
1102 1102
1103 1103 $ echo "changeset = '{c}'" > t
1104 1104 $ hg log --style ./t
1105 1105
1106 1106 Defining non-standard name works:
1107 1107
1108 1108 $ cat <<EOF > t
1109 1109 > changeset = '{c}'
1110 1110 > c = q
1111 1111 > EOF
1112 1112 $ hg log --style ./t
1113 1113 8
1114 1114 7
1115 1115 6
1116 1116 5
1117 1117 4
1118 1118 3
1119 1119 2
1120 1120 1
1121 1121 0
1122 1122
1123 1123 ui.style works:
1124 1124
1125 1125 $ echo '[ui]' > .hg/hgrc
1126 1126 $ echo 'style = t' >> .hg/hgrc
1127 1127 $ hg log
1128 1128 8
1129 1129 7
1130 1130 6
1131 1131 5
1132 1132 4
1133 1133 3
1134 1134 2
1135 1135 1
1136 1136 0
1137 1137
1138 1138
1139 1139 Issue338:
1140 1140
1141 1141 $ hg log --style=changelog > changelog
1142 1142
1143 1143 $ cat changelog
1144 1144 2020-01-01 test <test>
1145 1145
1146 1146 * fourth, second, third:
1147 1147 third
1148 1148 [95c24699272e] [tip]
1149 1149
1150 1150 1970-01-12 User Name <user@hostname>
1151 1151
1152 1152 * second:
1153 1153 second
1154 1154 [29114dbae42b]
1155 1155
1156 1156 1970-01-18 person <person>
1157 1157
1158 1158 * merge
1159 1159 [d41e714fe50d]
1160 1160
1161 1161 * d:
1162 1162 new head
1163 1163 [13207e5a10d9]
1164 1164
1165 1165 1970-01-17 person <person>
1166 1166
1167 1167 * new branch
1168 1168 [bbe44766e73d] <foo>
1169 1169
1170 1170 1970-01-16 person <person>
1171 1171
1172 1172 * c:
1173 1173 no user, no domain
1174 1174 [10e46f2dcbf4]
1175 1175
1176 1176 1970-01-14 other <other@place>
1177 1177
1178 1178 * c:
1179 1179 no person
1180 1180 [97054abb4ab8]
1181 1181
1182 1182 1970-01-13 A. N. Other <other@place>
1183 1183
1184 1184 * b:
1185 1185 other 1 other 2
1186 1186
1187 1187 other 3
1188 1188 [b608e9d1a3f0]
1189 1189
1190 1190 1970-01-12 User Name <user@hostname>
1191 1191
1192 1192 * a:
1193 1193 line 1 line 2
1194 1194 [1e4e1b8f71e0]
1195 1195
1196 1196
1197 1197 Issue2130: xml output for 'hg heads' is malformed
1198 1198
1199 1199 $ hg heads --style changelog
1200 1200 2020-01-01 test <test>
1201 1201
1202 1202 * fourth, second, third:
1203 1203 third
1204 1204 [95c24699272e] [tip]
1205 1205
1206 1206 1970-01-18 person <person>
1207 1207
1208 1208 * merge
1209 1209 [d41e714fe50d]
1210 1210
1211 1211 1970-01-17 person <person>
1212 1212
1213 1213 * new branch
1214 1214 [bbe44766e73d] <foo>
1215 1215
1216 1216
1217 1217 Keys work:
1218 1218
1219 1219 $ for key in author branch branches date desc file_adds file_dels file_mods \
1220 1220 > file_copies file_copies_switch files \
1221 1221 > manifest node parents rev tags diffstat extras \
1222 1222 > p1rev p2rev p1node p2node; do
1223 1223 > for mode in '' --verbose --debug; do
1224 1224 > hg log $mode --template "$key$mode: {$key}\n"
1225 1225 > done
1226 1226 > done
1227 1227 author: test
1228 1228 author: User Name <user@hostname>
1229 1229 author: person
1230 1230 author: person
1231 1231 author: person
1232 1232 author: person
1233 1233 author: other@place
1234 1234 author: A. N. Other <other@place>
1235 1235 author: User Name <user@hostname>
1236 1236 author--verbose: test
1237 1237 author--verbose: User Name <user@hostname>
1238 1238 author--verbose: person
1239 1239 author--verbose: person
1240 1240 author--verbose: person
1241 1241 author--verbose: person
1242 1242 author--verbose: other@place
1243 1243 author--verbose: A. N. Other <other@place>
1244 1244 author--verbose: User Name <user@hostname>
1245 1245 author--debug: test
1246 1246 author--debug: User Name <user@hostname>
1247 1247 author--debug: person
1248 1248 author--debug: person
1249 1249 author--debug: person
1250 1250 author--debug: person
1251 1251 author--debug: other@place
1252 1252 author--debug: A. N. Other <other@place>
1253 1253 author--debug: User Name <user@hostname>
1254 1254 branch: default
1255 1255 branch: default
1256 1256 branch: default
1257 1257 branch: default
1258 1258 branch: foo
1259 1259 branch: default
1260 1260 branch: default
1261 1261 branch: default
1262 1262 branch: default
1263 1263 branch--verbose: default
1264 1264 branch--verbose: default
1265 1265 branch--verbose: default
1266 1266 branch--verbose: default
1267 1267 branch--verbose: foo
1268 1268 branch--verbose: default
1269 1269 branch--verbose: default
1270 1270 branch--verbose: default
1271 1271 branch--verbose: default
1272 1272 branch--debug: default
1273 1273 branch--debug: default
1274 1274 branch--debug: default
1275 1275 branch--debug: default
1276 1276 branch--debug: foo
1277 1277 branch--debug: default
1278 1278 branch--debug: default
1279 1279 branch--debug: default
1280 1280 branch--debug: default
1281 1281 branches:
1282 1282 branches:
1283 1283 branches:
1284 1284 branches:
1285 1285 branches: foo
1286 1286 branches:
1287 1287 branches:
1288 1288 branches:
1289 1289 branches:
1290 1290 branches--verbose:
1291 1291 branches--verbose:
1292 1292 branches--verbose:
1293 1293 branches--verbose:
1294 1294 branches--verbose: foo
1295 1295 branches--verbose:
1296 1296 branches--verbose:
1297 1297 branches--verbose:
1298 1298 branches--verbose:
1299 1299 branches--debug:
1300 1300 branches--debug:
1301 1301 branches--debug:
1302 1302 branches--debug:
1303 1303 branches--debug: foo
1304 1304 branches--debug:
1305 1305 branches--debug:
1306 1306 branches--debug:
1307 1307 branches--debug:
1308 1308 date: 1577872860.00
1309 1309 date: 1000000.00
1310 1310 date: 1500001.00
1311 1311 date: 1500000.00
1312 1312 date: 1400000.00
1313 1313 date: 1300000.00
1314 1314 date: 1200000.00
1315 1315 date: 1100000.00
1316 1316 date: 1000000.00
1317 1317 date--verbose: 1577872860.00
1318 1318 date--verbose: 1000000.00
1319 1319 date--verbose: 1500001.00
1320 1320 date--verbose: 1500000.00
1321 1321 date--verbose: 1400000.00
1322 1322 date--verbose: 1300000.00
1323 1323 date--verbose: 1200000.00
1324 1324 date--verbose: 1100000.00
1325 1325 date--verbose: 1000000.00
1326 1326 date--debug: 1577872860.00
1327 1327 date--debug: 1000000.00
1328 1328 date--debug: 1500001.00
1329 1329 date--debug: 1500000.00
1330 1330 date--debug: 1400000.00
1331 1331 date--debug: 1300000.00
1332 1332 date--debug: 1200000.00
1333 1333 date--debug: 1100000.00
1334 1334 date--debug: 1000000.00
1335 1335 desc: third
1336 1336 desc: second
1337 1337 desc: merge
1338 1338 desc: new head
1339 1339 desc: new branch
1340 1340 desc: no user, no domain
1341 1341 desc: no person
1342 1342 desc: other 1
1343 1343 other 2
1344 1344
1345 1345 other 3
1346 1346 desc: line 1
1347 1347 line 2
1348 1348 desc--verbose: third
1349 1349 desc--verbose: second
1350 1350 desc--verbose: merge
1351 1351 desc--verbose: new head
1352 1352 desc--verbose: new branch
1353 1353 desc--verbose: no user, no domain
1354 1354 desc--verbose: no person
1355 1355 desc--verbose: other 1
1356 1356 other 2
1357 1357
1358 1358 other 3
1359 1359 desc--verbose: line 1
1360 1360 line 2
1361 1361 desc--debug: third
1362 1362 desc--debug: second
1363 1363 desc--debug: merge
1364 1364 desc--debug: new head
1365 1365 desc--debug: new branch
1366 1366 desc--debug: no user, no domain
1367 1367 desc--debug: no person
1368 1368 desc--debug: other 1
1369 1369 other 2
1370 1370
1371 1371 other 3
1372 1372 desc--debug: line 1
1373 1373 line 2
1374 1374 file_adds: fourth third
1375 1375 file_adds: second
1376 1376 file_adds:
1377 1377 file_adds: d
1378 1378 file_adds:
1379 1379 file_adds:
1380 1380 file_adds: c
1381 1381 file_adds: b
1382 1382 file_adds: a
1383 1383 file_adds--verbose: fourth third
1384 1384 file_adds--verbose: second
1385 1385 file_adds--verbose:
1386 1386 file_adds--verbose: d
1387 1387 file_adds--verbose:
1388 1388 file_adds--verbose:
1389 1389 file_adds--verbose: c
1390 1390 file_adds--verbose: b
1391 1391 file_adds--verbose: a
1392 1392 file_adds--debug: fourth third
1393 1393 file_adds--debug: second
1394 1394 file_adds--debug:
1395 1395 file_adds--debug: d
1396 1396 file_adds--debug:
1397 1397 file_adds--debug:
1398 1398 file_adds--debug: c
1399 1399 file_adds--debug: b
1400 1400 file_adds--debug: a
1401 1401 file_dels: second
1402 1402 file_dels:
1403 1403 file_dels:
1404 1404 file_dels:
1405 1405 file_dels:
1406 1406 file_dels:
1407 1407 file_dels:
1408 1408 file_dels:
1409 1409 file_dels:
1410 1410 file_dels--verbose: second
1411 1411 file_dels--verbose:
1412 1412 file_dels--verbose:
1413 1413 file_dels--verbose:
1414 1414 file_dels--verbose:
1415 1415 file_dels--verbose:
1416 1416 file_dels--verbose:
1417 1417 file_dels--verbose:
1418 1418 file_dels--verbose:
1419 1419 file_dels--debug: second
1420 1420 file_dels--debug:
1421 1421 file_dels--debug:
1422 1422 file_dels--debug:
1423 1423 file_dels--debug:
1424 1424 file_dels--debug:
1425 1425 file_dels--debug:
1426 1426 file_dels--debug:
1427 1427 file_dels--debug:
1428 1428 file_mods:
1429 1429 file_mods:
1430 1430 file_mods:
1431 1431 file_mods:
1432 1432 file_mods:
1433 1433 file_mods: c
1434 1434 file_mods:
1435 1435 file_mods:
1436 1436 file_mods:
1437 1437 file_mods--verbose:
1438 1438 file_mods--verbose:
1439 1439 file_mods--verbose:
1440 1440 file_mods--verbose:
1441 1441 file_mods--verbose:
1442 1442 file_mods--verbose: c
1443 1443 file_mods--verbose:
1444 1444 file_mods--verbose:
1445 1445 file_mods--verbose:
1446 1446 file_mods--debug:
1447 1447 file_mods--debug:
1448 1448 file_mods--debug:
1449 1449 file_mods--debug:
1450 1450 file_mods--debug:
1451 1451 file_mods--debug: c
1452 1452 file_mods--debug:
1453 1453 file_mods--debug:
1454 1454 file_mods--debug:
1455 1455 file_copies: fourth (second)
1456 1456 file_copies:
1457 1457 file_copies:
1458 1458 file_copies:
1459 1459 file_copies:
1460 1460 file_copies:
1461 1461 file_copies:
1462 1462 file_copies:
1463 1463 file_copies:
1464 1464 file_copies--verbose: fourth (second)
1465 1465 file_copies--verbose:
1466 1466 file_copies--verbose:
1467 1467 file_copies--verbose:
1468 1468 file_copies--verbose:
1469 1469 file_copies--verbose:
1470 1470 file_copies--verbose:
1471 1471 file_copies--verbose:
1472 1472 file_copies--verbose:
1473 1473 file_copies--debug: fourth (second)
1474 1474 file_copies--debug:
1475 1475 file_copies--debug:
1476 1476 file_copies--debug:
1477 1477 file_copies--debug:
1478 1478 file_copies--debug:
1479 1479 file_copies--debug:
1480 1480 file_copies--debug:
1481 1481 file_copies--debug:
1482 1482 file_copies_switch:
1483 1483 file_copies_switch:
1484 1484 file_copies_switch:
1485 1485 file_copies_switch:
1486 1486 file_copies_switch:
1487 1487 file_copies_switch:
1488 1488 file_copies_switch:
1489 1489 file_copies_switch:
1490 1490 file_copies_switch:
1491 1491 file_copies_switch--verbose:
1492 1492 file_copies_switch--verbose:
1493 1493 file_copies_switch--verbose:
1494 1494 file_copies_switch--verbose:
1495 1495 file_copies_switch--verbose:
1496 1496 file_copies_switch--verbose:
1497 1497 file_copies_switch--verbose:
1498 1498 file_copies_switch--verbose:
1499 1499 file_copies_switch--verbose:
1500 1500 file_copies_switch--debug:
1501 1501 file_copies_switch--debug:
1502 1502 file_copies_switch--debug:
1503 1503 file_copies_switch--debug:
1504 1504 file_copies_switch--debug:
1505 1505 file_copies_switch--debug:
1506 1506 file_copies_switch--debug:
1507 1507 file_copies_switch--debug:
1508 1508 file_copies_switch--debug:
1509 1509 files: fourth second third
1510 1510 files: second
1511 1511 files:
1512 1512 files: d
1513 1513 files:
1514 1514 files: c
1515 1515 files: c
1516 1516 files: b
1517 1517 files: a
1518 1518 files--verbose: fourth second third
1519 1519 files--verbose: second
1520 1520 files--verbose:
1521 1521 files--verbose: d
1522 1522 files--verbose:
1523 1523 files--verbose: c
1524 1524 files--verbose: c
1525 1525 files--verbose: b
1526 1526 files--verbose: a
1527 1527 files--debug: fourth second third
1528 1528 files--debug: second
1529 1529 files--debug:
1530 1530 files--debug: d
1531 1531 files--debug:
1532 1532 files--debug: c
1533 1533 files--debug: c
1534 1534 files--debug: b
1535 1535 files--debug: a
1536 1536 manifest: 6:94961b75a2da
1537 1537 manifest: 5:f2dbc354b94e
1538 1538 manifest: 4:4dc3def4f9b4
1539 1539 manifest: 4:4dc3def4f9b4
1540 1540 manifest: 3:cb5a1327723b
1541 1541 manifest: 3:cb5a1327723b
1542 1542 manifest: 2:6e0e82995c35
1543 1543 manifest: 1:4e8d705b1e53
1544 1544 manifest: 0:a0c8bcbbb45c
1545 1545 manifest--verbose: 6:94961b75a2da
1546 1546 manifest--verbose: 5:f2dbc354b94e
1547 1547 manifest--verbose: 4:4dc3def4f9b4
1548 1548 manifest--verbose: 4:4dc3def4f9b4
1549 1549 manifest--verbose: 3:cb5a1327723b
1550 1550 manifest--verbose: 3:cb5a1327723b
1551 1551 manifest--verbose: 2:6e0e82995c35
1552 1552 manifest--verbose: 1:4e8d705b1e53
1553 1553 manifest--verbose: 0:a0c8bcbbb45c
1554 1554 manifest--debug: 6:94961b75a2da554b4df6fb599e5bfc7d48de0c64
1555 1555 manifest--debug: 5:f2dbc354b94e5ec0b4f10680ee0cee816101d0bf
1556 1556 manifest--debug: 4:4dc3def4f9b4c6e8de820f6ee74737f91e96a216
1557 1557 manifest--debug: 4:4dc3def4f9b4c6e8de820f6ee74737f91e96a216
1558 1558 manifest--debug: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc
1559 1559 manifest--debug: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc
1560 1560 manifest--debug: 2:6e0e82995c35d0d57a52aca8da4e56139e06b4b1
1561 1561 manifest--debug: 1:4e8d705b1e53e3f9375e0e60dc7b525d8211fe55
1562 1562 manifest--debug: 0:a0c8bcbbb45c63b90b70ad007bf38961f64f2af0
1563 1563 node: 95c24699272ef57d062b8bccc32c878bf841784a
1564 1564 node: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1565 1565 node: d41e714fe50d9e4a5f11b4d595d543481b5f980b
1566 1566 node: 13207e5a10d9fd28ec424934298e176197f2c67f
1567 1567 node: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1568 1568 node: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1569 1569 node: 97054abb4ab824450e9164180baf491ae0078465
1570 1570 node: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1571 1571 node: 1e4e1b8f71e05681d422154f5421e385fec3454f
1572 1572 node--verbose: 95c24699272ef57d062b8bccc32c878bf841784a
1573 1573 node--verbose: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1574 1574 node--verbose: d41e714fe50d9e4a5f11b4d595d543481b5f980b
1575 1575 node--verbose: 13207e5a10d9fd28ec424934298e176197f2c67f
1576 1576 node--verbose: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1577 1577 node--verbose: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1578 1578 node--verbose: 97054abb4ab824450e9164180baf491ae0078465
1579 1579 node--verbose: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1580 1580 node--verbose: 1e4e1b8f71e05681d422154f5421e385fec3454f
1581 1581 node--debug: 95c24699272ef57d062b8bccc32c878bf841784a
1582 1582 node--debug: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1583 1583 node--debug: d41e714fe50d9e4a5f11b4d595d543481b5f980b
1584 1584 node--debug: 13207e5a10d9fd28ec424934298e176197f2c67f
1585 1585 node--debug: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1586 1586 node--debug: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1587 1587 node--debug: 97054abb4ab824450e9164180baf491ae0078465
1588 1588 node--debug: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1589 1589 node--debug: 1e4e1b8f71e05681d422154f5421e385fec3454f
1590 1590 parents:
1591 1591 parents: -1:000000000000
1592 1592 parents: 5:13207e5a10d9 4:bbe44766e73d
1593 1593 parents: 3:10e46f2dcbf4
1594 1594 parents:
1595 1595 parents:
1596 1596 parents:
1597 1597 parents:
1598 1598 parents:
1599 1599 parents--verbose:
1600 1600 parents--verbose: -1:000000000000
1601 1601 parents--verbose: 5:13207e5a10d9 4:bbe44766e73d
1602 1602 parents--verbose: 3:10e46f2dcbf4
1603 1603 parents--verbose:
1604 1604 parents--verbose:
1605 1605 parents--verbose:
1606 1606 parents--verbose:
1607 1607 parents--verbose:
1608 1608 parents--debug: 7:29114dbae42b9f078cf2714dbe3a86bba8ec7453 -1:0000000000000000000000000000000000000000
1609 1609 parents--debug: -1:0000000000000000000000000000000000000000 -1:0000000000000000000000000000000000000000
1610 1610 parents--debug: 5:13207e5a10d9fd28ec424934298e176197f2c67f 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
1611 1611 parents--debug: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47 -1:0000000000000000000000000000000000000000
1612 1612 parents--debug: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47 -1:0000000000000000000000000000000000000000
1613 1613 parents--debug: 2:97054abb4ab824450e9164180baf491ae0078465 -1:0000000000000000000000000000000000000000
1614 1614 parents--debug: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965 -1:0000000000000000000000000000000000000000
1615 1615 parents--debug: 0:1e4e1b8f71e05681d422154f5421e385fec3454f -1:0000000000000000000000000000000000000000
1616 1616 parents--debug: -1:0000000000000000000000000000000000000000 -1:0000000000000000000000000000000000000000
1617 1617 rev: 8
1618 1618 rev: 7
1619 1619 rev: 6
1620 1620 rev: 5
1621 1621 rev: 4
1622 1622 rev: 3
1623 1623 rev: 2
1624 1624 rev: 1
1625 1625 rev: 0
1626 1626 rev--verbose: 8
1627 1627 rev--verbose: 7
1628 1628 rev--verbose: 6
1629 1629 rev--verbose: 5
1630 1630 rev--verbose: 4
1631 1631 rev--verbose: 3
1632 1632 rev--verbose: 2
1633 1633 rev--verbose: 1
1634 1634 rev--verbose: 0
1635 1635 rev--debug: 8
1636 1636 rev--debug: 7
1637 1637 rev--debug: 6
1638 1638 rev--debug: 5
1639 1639 rev--debug: 4
1640 1640 rev--debug: 3
1641 1641 rev--debug: 2
1642 1642 rev--debug: 1
1643 1643 rev--debug: 0
1644 1644 tags: tip
1645 1645 tags:
1646 1646 tags:
1647 1647 tags:
1648 1648 tags:
1649 1649 tags:
1650 1650 tags:
1651 1651 tags:
1652 1652 tags:
1653 1653 tags--verbose: tip
1654 1654 tags--verbose:
1655 1655 tags--verbose:
1656 1656 tags--verbose:
1657 1657 tags--verbose:
1658 1658 tags--verbose:
1659 1659 tags--verbose:
1660 1660 tags--verbose:
1661 1661 tags--verbose:
1662 1662 tags--debug: tip
1663 1663 tags--debug:
1664 1664 tags--debug:
1665 1665 tags--debug:
1666 1666 tags--debug:
1667 1667 tags--debug:
1668 1668 tags--debug:
1669 1669 tags--debug:
1670 1670 tags--debug:
1671 1671 diffstat: 3: +2/-1
1672 1672 diffstat: 1: +1/-0
1673 1673 diffstat: 0: +0/-0
1674 1674 diffstat: 1: +1/-0
1675 1675 diffstat: 0: +0/-0
1676 1676 diffstat: 1: +1/-0
1677 1677 diffstat: 1: +4/-0
1678 1678 diffstat: 1: +2/-0
1679 1679 diffstat: 1: +1/-0
1680 1680 diffstat--verbose: 3: +2/-1
1681 1681 diffstat--verbose: 1: +1/-0
1682 1682 diffstat--verbose: 0: +0/-0
1683 1683 diffstat--verbose: 1: +1/-0
1684 1684 diffstat--verbose: 0: +0/-0
1685 1685 diffstat--verbose: 1: +1/-0
1686 1686 diffstat--verbose: 1: +4/-0
1687 1687 diffstat--verbose: 1: +2/-0
1688 1688 diffstat--verbose: 1: +1/-0
1689 1689 diffstat--debug: 3: +2/-1
1690 1690 diffstat--debug: 1: +1/-0
1691 1691 diffstat--debug: 0: +0/-0
1692 1692 diffstat--debug: 1: +1/-0
1693 1693 diffstat--debug: 0: +0/-0
1694 1694 diffstat--debug: 1: +1/-0
1695 1695 diffstat--debug: 1: +4/-0
1696 1696 diffstat--debug: 1: +2/-0
1697 1697 diffstat--debug: 1: +1/-0
1698 1698 extras: branch=default
1699 1699 extras: branch=default
1700 1700 extras: branch=default
1701 1701 extras: branch=default
1702 1702 extras: branch=foo
1703 1703 extras: branch=default
1704 1704 extras: branch=default
1705 1705 extras: branch=default
1706 1706 extras: branch=default
1707 1707 extras--verbose: branch=default
1708 1708 extras--verbose: branch=default
1709 1709 extras--verbose: branch=default
1710 1710 extras--verbose: branch=default
1711 1711 extras--verbose: branch=foo
1712 1712 extras--verbose: branch=default
1713 1713 extras--verbose: branch=default
1714 1714 extras--verbose: branch=default
1715 1715 extras--verbose: branch=default
1716 1716 extras--debug: branch=default
1717 1717 extras--debug: branch=default
1718 1718 extras--debug: branch=default
1719 1719 extras--debug: branch=default
1720 1720 extras--debug: branch=foo
1721 1721 extras--debug: branch=default
1722 1722 extras--debug: branch=default
1723 1723 extras--debug: branch=default
1724 1724 extras--debug: branch=default
1725 1725 p1rev: 7
1726 1726 p1rev: -1
1727 1727 p1rev: 5
1728 1728 p1rev: 3
1729 1729 p1rev: 3
1730 1730 p1rev: 2
1731 1731 p1rev: 1
1732 1732 p1rev: 0
1733 1733 p1rev: -1
1734 1734 p1rev--verbose: 7
1735 1735 p1rev--verbose: -1
1736 1736 p1rev--verbose: 5
1737 1737 p1rev--verbose: 3
1738 1738 p1rev--verbose: 3
1739 1739 p1rev--verbose: 2
1740 1740 p1rev--verbose: 1
1741 1741 p1rev--verbose: 0
1742 1742 p1rev--verbose: -1
1743 1743 p1rev--debug: 7
1744 1744 p1rev--debug: -1
1745 1745 p1rev--debug: 5
1746 1746 p1rev--debug: 3
1747 1747 p1rev--debug: 3
1748 1748 p1rev--debug: 2
1749 1749 p1rev--debug: 1
1750 1750 p1rev--debug: 0
1751 1751 p1rev--debug: -1
1752 1752 p2rev: -1
1753 1753 p2rev: -1
1754 1754 p2rev: 4
1755 1755 p2rev: -1
1756 1756 p2rev: -1
1757 1757 p2rev: -1
1758 1758 p2rev: -1
1759 1759 p2rev: -1
1760 1760 p2rev: -1
1761 1761 p2rev--verbose: -1
1762 1762 p2rev--verbose: -1
1763 1763 p2rev--verbose: 4
1764 1764 p2rev--verbose: -1
1765 1765 p2rev--verbose: -1
1766 1766 p2rev--verbose: -1
1767 1767 p2rev--verbose: -1
1768 1768 p2rev--verbose: -1
1769 1769 p2rev--verbose: -1
1770 1770 p2rev--debug: -1
1771 1771 p2rev--debug: -1
1772 1772 p2rev--debug: 4
1773 1773 p2rev--debug: -1
1774 1774 p2rev--debug: -1
1775 1775 p2rev--debug: -1
1776 1776 p2rev--debug: -1
1777 1777 p2rev--debug: -1
1778 1778 p2rev--debug: -1
1779 1779 p1node: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1780 1780 p1node: 0000000000000000000000000000000000000000
1781 1781 p1node: 13207e5a10d9fd28ec424934298e176197f2c67f
1782 1782 p1node: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1783 1783 p1node: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1784 1784 p1node: 97054abb4ab824450e9164180baf491ae0078465
1785 1785 p1node: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1786 1786 p1node: 1e4e1b8f71e05681d422154f5421e385fec3454f
1787 1787 p1node: 0000000000000000000000000000000000000000
1788 1788 p1node--verbose: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1789 1789 p1node--verbose: 0000000000000000000000000000000000000000
1790 1790 p1node--verbose: 13207e5a10d9fd28ec424934298e176197f2c67f
1791 1791 p1node--verbose: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1792 1792 p1node--verbose: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1793 1793 p1node--verbose: 97054abb4ab824450e9164180baf491ae0078465
1794 1794 p1node--verbose: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1795 1795 p1node--verbose: 1e4e1b8f71e05681d422154f5421e385fec3454f
1796 1796 p1node--verbose: 0000000000000000000000000000000000000000
1797 1797 p1node--debug: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1798 1798 p1node--debug: 0000000000000000000000000000000000000000
1799 1799 p1node--debug: 13207e5a10d9fd28ec424934298e176197f2c67f
1800 1800 p1node--debug: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1801 1801 p1node--debug: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1802 1802 p1node--debug: 97054abb4ab824450e9164180baf491ae0078465
1803 1803 p1node--debug: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1804 1804 p1node--debug: 1e4e1b8f71e05681d422154f5421e385fec3454f
1805 1805 p1node--debug: 0000000000000000000000000000000000000000
1806 1806 p2node: 0000000000000000000000000000000000000000
1807 1807 p2node: 0000000000000000000000000000000000000000
1808 1808 p2node: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1809 1809 p2node: 0000000000000000000000000000000000000000
1810 1810 p2node: 0000000000000000000000000000000000000000
1811 1811 p2node: 0000000000000000000000000000000000000000
1812 1812 p2node: 0000000000000000000000000000000000000000
1813 1813 p2node: 0000000000000000000000000000000000000000
1814 1814 p2node: 0000000000000000000000000000000000000000
1815 1815 p2node--verbose: 0000000000000000000000000000000000000000
1816 1816 p2node--verbose: 0000000000000000000000000000000000000000
1817 1817 p2node--verbose: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1818 1818 p2node--verbose: 0000000000000000000000000000000000000000
1819 1819 p2node--verbose: 0000000000000000000000000000000000000000
1820 1820 p2node--verbose: 0000000000000000000000000000000000000000
1821 1821 p2node--verbose: 0000000000000000000000000000000000000000
1822 1822 p2node--verbose: 0000000000000000000000000000000000000000
1823 1823 p2node--verbose: 0000000000000000000000000000000000000000
1824 1824 p2node--debug: 0000000000000000000000000000000000000000
1825 1825 p2node--debug: 0000000000000000000000000000000000000000
1826 1826 p2node--debug: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1827 1827 p2node--debug: 0000000000000000000000000000000000000000
1828 1828 p2node--debug: 0000000000000000000000000000000000000000
1829 1829 p2node--debug: 0000000000000000000000000000000000000000
1830 1830 p2node--debug: 0000000000000000000000000000000000000000
1831 1831 p2node--debug: 0000000000000000000000000000000000000000
1832 1832 p2node--debug: 0000000000000000000000000000000000000000
1833 1833
1834 1834 Filters work:
1835 1835
1836 1836 $ hg log --template '{author|domain}\n'
1837 1837
1838 1838 hostname
1839 1839
1840 1840
1841 1841
1842 1842
1843 1843 place
1844 1844 place
1845 1845 hostname
1846 1846
1847 1847 $ hg log --template '{author|person}\n'
1848 1848 test
1849 1849 User Name
1850 1850 person
1851 1851 person
1852 1852 person
1853 1853 person
1854 1854 other
1855 1855 A. N. Other
1856 1856 User Name
1857 1857
1858 1858 $ hg log --template '{author|user}\n'
1859 1859 test
1860 1860 user
1861 1861 person
1862 1862 person
1863 1863 person
1864 1864 person
1865 1865 other
1866 1866 other
1867 1867 user
1868 1868
1869 1869 $ hg log --template '{date|date}\n'
1870 1870 Wed Jan 01 10:01:00 2020 +0000
1871 1871 Mon Jan 12 13:46:40 1970 +0000
1872 1872 Sun Jan 18 08:40:01 1970 +0000
1873 1873 Sun Jan 18 08:40:00 1970 +0000
1874 1874 Sat Jan 17 04:53:20 1970 +0000
1875 1875 Fri Jan 16 01:06:40 1970 +0000
1876 1876 Wed Jan 14 21:20:00 1970 +0000
1877 1877 Tue Jan 13 17:33:20 1970 +0000
1878 1878 Mon Jan 12 13:46:40 1970 +0000
1879 1879
1880 1880 $ hg log --template '{date|isodate}\n'
1881 1881 2020-01-01 10:01 +0000
1882 1882 1970-01-12 13:46 +0000
1883 1883 1970-01-18 08:40 +0000
1884 1884 1970-01-18 08:40 +0000
1885 1885 1970-01-17 04:53 +0000
1886 1886 1970-01-16 01:06 +0000
1887 1887 1970-01-14 21:20 +0000
1888 1888 1970-01-13 17:33 +0000
1889 1889 1970-01-12 13:46 +0000
1890 1890
1891 1891 $ hg log --template '{date|isodatesec}\n'
1892 1892 2020-01-01 10:01:00 +0000
1893 1893 1970-01-12 13:46:40 +0000
1894 1894 1970-01-18 08:40:01 +0000
1895 1895 1970-01-18 08:40:00 +0000
1896 1896 1970-01-17 04:53:20 +0000
1897 1897 1970-01-16 01:06:40 +0000
1898 1898 1970-01-14 21:20:00 +0000
1899 1899 1970-01-13 17:33:20 +0000
1900 1900 1970-01-12 13:46:40 +0000
1901 1901
1902 1902 $ hg log --template '{date|rfc822date}\n'
1903 1903 Wed, 01 Jan 2020 10:01:00 +0000
1904 1904 Mon, 12 Jan 1970 13:46:40 +0000
1905 1905 Sun, 18 Jan 1970 08:40:01 +0000
1906 1906 Sun, 18 Jan 1970 08:40:00 +0000
1907 1907 Sat, 17 Jan 1970 04:53:20 +0000
1908 1908 Fri, 16 Jan 1970 01:06:40 +0000
1909 1909 Wed, 14 Jan 1970 21:20:00 +0000
1910 1910 Tue, 13 Jan 1970 17:33:20 +0000
1911 1911 Mon, 12 Jan 1970 13:46:40 +0000
1912 1912
1913 1913 $ hg log --template '{desc|firstline}\n'
1914 1914 third
1915 1915 second
1916 1916 merge
1917 1917 new head
1918 1918 new branch
1919 1919 no user, no domain
1920 1920 no person
1921 1921 other 1
1922 1922 line 1
1923 1923
1924 1924 $ hg log --template '{node|short}\n'
1925 1925 95c24699272e
1926 1926 29114dbae42b
1927 1927 d41e714fe50d
1928 1928 13207e5a10d9
1929 1929 bbe44766e73d
1930 1930 10e46f2dcbf4
1931 1931 97054abb4ab8
1932 1932 b608e9d1a3f0
1933 1933 1e4e1b8f71e0
1934 1934
1935 1935 $ hg log --template '<changeset author="{author|xmlescape}"/>\n'
1936 1936 <changeset author="test"/>
1937 1937 <changeset author="User Name &lt;user@hostname&gt;"/>
1938 1938 <changeset author="person"/>
1939 1939 <changeset author="person"/>
1940 1940 <changeset author="person"/>
1941 1941 <changeset author="person"/>
1942 1942 <changeset author="other@place"/>
1943 1943 <changeset author="A. N. Other &lt;other@place&gt;"/>
1944 1944 <changeset author="User Name &lt;user@hostname&gt;"/>
1945 1945
1946 1946 $ hg log --template '{rev}: {children}\n'
1947 1947 8:
1948 1948 7: 8:95c24699272e
1949 1949 6:
1950 1950 5: 6:d41e714fe50d
1951 1951 4: 6:d41e714fe50d
1952 1952 3: 4:bbe44766e73d 5:13207e5a10d9
1953 1953 2: 3:10e46f2dcbf4
1954 1954 1: 2:97054abb4ab8
1955 1955 0: 1:b608e9d1a3f0
1956 1956
1957 1957 Formatnode filter works:
1958 1958
1959 1959 $ hg -q log -r 0 --template '{node|formatnode}\n'
1960 1960 1e4e1b8f71e0
1961 1961
1962 1962 $ hg log -r 0 --template '{node|formatnode}\n'
1963 1963 1e4e1b8f71e0
1964 1964
1965 1965 $ hg -v log -r 0 --template '{node|formatnode}\n'
1966 1966 1e4e1b8f71e0
1967 1967
1968 1968 $ hg --debug log -r 0 --template '{node|formatnode}\n'
1969 1969 1e4e1b8f71e05681d422154f5421e385fec3454f
1970 1970
1971 1971 Age filter:
1972 1972
1973 1973 $ hg init unstable-hash
1974 1974 $ cd unstable-hash
1975 1975 $ hg log --template '{date|age}\n' > /dev/null || exit 1
1976 1976
1977 1977 >>> from datetime import datetime, timedelta
1978 1978 >>> fp = open('a', 'w')
1979 1979 >>> n = datetime.now() + timedelta(366 * 7)
1980 1980 >>> fp.write('%d-%d-%d 00:00' % (n.year, n.month, n.day))
1981 1981 >>> fp.close()
1982 1982 $ hg add a
1983 1983 $ hg commit -m future -d "`cat a`"
1984 1984
1985 1985 $ hg log -l1 --template '{date|age}\n'
1986 1986 7 years from now
1987 1987
1988 1988 $ cd ..
1989 1989 $ rm -rf unstable-hash
1990 1990
1991 1991 Add a dummy commit to make up for the instability of the above:
1992 1992
1993 1993 $ echo a > a
1994 1994 $ hg add a
1995 1995 $ hg ci -m future
1996 1996
1997 1997 Count filter:
1998 1998
1999 1999 $ hg log -l1 --template '{node|count} {node|short|count}\n'
2000 2000 40 12
2001 2001
2002 2002 $ hg log -l1 --template '{revset("null^")|count} {revset(".")|count} {revset("0::3")|count}\n'
2003 2003 0 1 4
2004 2004
2005 2005 $ hg log -G --template '{rev}: children: {children|count}, \
2006 2006 > tags: {tags|count}, file_adds: {file_adds|count}, \
2007 2007 > ancestors: {revset("ancestors(%s)", rev)|count}'
2008 2008 @ 9: children: 0, tags: 1, file_adds: 1, ancestors: 3
2009 2009 |
2010 2010 o 8: children: 1, tags: 0, file_adds: 2, ancestors: 2
2011 2011 |
2012 2012 o 7: children: 1, tags: 0, file_adds: 1, ancestors: 1
2013 2013
2014 2014 o 6: children: 0, tags: 0, file_adds: 0, ancestors: 7
2015 2015 |\
2016 2016 | o 5: children: 1, tags: 0, file_adds: 1, ancestors: 5
2017 2017 | |
2018 2018 o | 4: children: 1, tags: 0, file_adds: 0, ancestors: 5
2019 2019 |/
2020 2020 o 3: children: 2, tags: 0, file_adds: 0, ancestors: 4
2021 2021 |
2022 2022 o 2: children: 1, tags: 0, file_adds: 1, ancestors: 3
2023 2023 |
2024 2024 o 1: children: 1, tags: 0, file_adds: 1, ancestors: 2
2025 2025 |
2026 2026 o 0: children: 1, tags: 0, file_adds: 1, ancestors: 1
2027 2027
2028 2028
2029 2029 Upper/lower filters:
2030 2030
2031 2031 $ hg log -r0 --template '{branch|upper}\n'
2032 2032 DEFAULT
2033 2033 $ hg log -r0 --template '{author|lower}\n'
2034 2034 user name <user@hostname>
2035 2035 $ hg log -r0 --template '{date|upper}\n'
2036 2036 abort: template filter 'upper' is not compatible with keyword 'date'
2037 2037 [255]
2038 2038
2039 2039 Add a commit that does all possible modifications at once
2040 2040
2041 2041 $ echo modify >> third
2042 2042 $ touch b
2043 2043 $ hg add b
2044 2044 $ hg mv fourth fifth
2045 2045 $ hg rm a
2046 2046 $ hg ci -m "Modify, add, remove, rename"
2047 2047
2048 2048 Check the status template
2049 2049
2050 2050 $ cat <<EOF >> $HGRCPATH
2051 2051 > [extensions]
2052 2052 > color=
2053 2053 > EOF
2054 2054
2055 2055 $ hg log -T status -r 10
2056 2056 changeset: 10:0f9759ec227a
2057 2057 tag: tip
2058 2058 user: test
2059 2059 date: Thu Jan 01 00:00:00 1970 +0000
2060 2060 summary: Modify, add, remove, rename
2061 2061 files:
2062 2062 M third
2063 2063 A b
2064 2064 A fifth
2065 2065 R a
2066 2066 R fourth
2067 2067
2068 2068 $ hg log -T status -C -r 10
2069 2069 changeset: 10:0f9759ec227a
2070 2070 tag: tip
2071 2071 user: test
2072 2072 date: Thu Jan 01 00:00:00 1970 +0000
2073 2073 summary: Modify, add, remove, rename
2074 2074 files:
2075 2075 M third
2076 2076 A b
2077 2077 A fifth
2078 2078 fourth
2079 2079 R a
2080 2080 R fourth
2081 2081
2082 2082 $ hg log -T status -C -r 10 -v
2083 2083 changeset: 10:0f9759ec227a
2084 2084 tag: tip
2085 2085 user: test
2086 2086 date: Thu Jan 01 00:00:00 1970 +0000
2087 2087 description:
2088 2088 Modify, add, remove, rename
2089 2089
2090 2090 files:
2091 2091 M third
2092 2092 A b
2093 2093 A fifth
2094 2094 fourth
2095 2095 R a
2096 2096 R fourth
2097 2097
2098 2098 $ hg log -T status -C -r 10 --debug
2099 2099 changeset: 10:0f9759ec227a4859c2014a345cd8a859022b7c6c
2100 2100 tag: tip
2101 2101 phase: secret
2102 2102 parent: 9:bf9dfba36635106d6a73ccc01e28b762da60e066
2103 2103 parent: -1:0000000000000000000000000000000000000000
2104 2104 manifest: 8:89dd546f2de0a9d6d664f58d86097eb97baba567
2105 2105 user: test
2106 2106 date: Thu Jan 01 00:00:00 1970 +0000
2107 2107 extra: branch=default
2108 2108 description:
2109 2109 Modify, add, remove, rename
2110 2110
2111 2111 files:
2112 2112 M third
2113 2113 A b
2114 2114 A fifth
2115 2115 fourth
2116 2116 R a
2117 2117 R fourth
2118 2118
2119 2119 $ hg log -T status -C -r 10 --quiet
2120 2120 10:0f9759ec227a
2121 2121 $ hg --color=debug log -T status -r 10
2122 2122 [log.changeset changeset.secret|changeset: 10:0f9759ec227a]
2123 2123 [log.tag|tag: tip]
2124 2124 [log.user|user: test]
2125 2125 [log.date|date: Thu Jan 01 00:00:00 1970 +0000]
2126 2126 [log.summary|summary: Modify, add, remove, rename]
2127 2127 [ui.note log.files|files:]
2128 2128 [status.modified|M third]
2129 2129 [status.added|A b]
2130 2130 [status.added|A fifth]
2131 2131 [status.removed|R a]
2132 2132 [status.removed|R fourth]
2133 2133
2134 2134 $ hg --color=debug log -T status -C -r 10
2135 2135 [log.changeset changeset.secret|changeset: 10:0f9759ec227a]
2136 2136 [log.tag|tag: tip]
2137 2137 [log.user|user: test]
2138 2138 [log.date|date: Thu Jan 01 00:00:00 1970 +0000]
2139 2139 [log.summary|summary: Modify, add, remove, rename]
2140 2140 [ui.note log.files|files:]
2141 2141 [status.modified|M third]
2142 2142 [status.added|A b]
2143 2143 [status.added|A fifth]
2144 2144 [status.copied| fourth]
2145 2145 [status.removed|R a]
2146 2146 [status.removed|R fourth]
2147 2147
2148 2148 $ hg --color=debug log -T status -C -r 10 -v
2149 2149 [log.changeset changeset.secret|changeset: 10:0f9759ec227a]
2150 2150 [log.tag|tag: tip]
2151 2151 [log.user|user: test]
2152 2152 [log.date|date: Thu Jan 01 00:00:00 1970 +0000]
2153 2153 [ui.note log.description|description:]
2154 2154 [ui.note log.description|Modify, add, remove, rename]
2155 2155
2156 2156 [ui.note log.files|files:]
2157 2157 [status.modified|M third]
2158 2158 [status.added|A b]
2159 2159 [status.added|A fifth]
2160 2160 [status.copied| fourth]
2161 2161 [status.removed|R a]
2162 2162 [status.removed|R fourth]
2163 2163
2164 2164 $ hg --color=debug log -T status -C -r 10 --debug
2165 2165 [log.changeset changeset.secret|changeset: 10:0f9759ec227a4859c2014a345cd8a859022b7c6c]
2166 2166 [log.tag|tag: tip]
2167 2167 [log.phase|phase: secret]
2168 2168 [log.parent changeset.secret|parent: 9:bf9dfba36635106d6a73ccc01e28b762da60e066]
2169 2169 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2170 2170 [ui.debug log.manifest|manifest: 8:89dd546f2de0a9d6d664f58d86097eb97baba567]
2171 2171 [log.user|user: test]
2172 2172 [log.date|date: Thu Jan 01 00:00:00 1970 +0000]
2173 2173 [ui.debug log.extra|extra: branch=default]
2174 2174 [ui.note log.description|description:]
2175 2175 [ui.note log.description|Modify, add, remove, rename]
2176 2176
2177 2177 [ui.note log.files|files:]
2178 2178 [status.modified|M third]
2179 2179 [status.added|A b]
2180 2180 [status.added|A fifth]
2181 2181 [status.copied| fourth]
2182 2182 [status.removed|R a]
2183 2183 [status.removed|R fourth]
2184 2184
2185 2185 $ hg --color=debug log -T status -C -r 10 --quiet
2186 2186 [log.node|10:0f9759ec227a]
2187 2187
2188 2188 Check the bisect template
2189 2189
2190 2190 $ hg bisect -g 1
2191 2191 $ hg bisect -b 3 --noupdate
2192 2192 Testing changeset 2:97054abb4ab8 (2 changesets remaining, ~1 tests)
2193 2193 $ hg log -T bisect -r 0:4
2194 2194 changeset: 0:1e4e1b8f71e0
2195 2195 bisect: good (implicit)
2196 2196 user: User Name <user@hostname>
2197 2197 date: Mon Jan 12 13:46:40 1970 +0000
2198 2198 summary: line 1
2199 2199
2200 2200 changeset: 1:b608e9d1a3f0
2201 2201 bisect: good
2202 2202 user: A. N. Other <other@place>
2203 2203 date: Tue Jan 13 17:33:20 1970 +0000
2204 2204 summary: other 1
2205 2205
2206 2206 changeset: 2:97054abb4ab8
2207 2207 bisect: untested
2208 2208 user: other@place
2209 2209 date: Wed Jan 14 21:20:00 1970 +0000
2210 2210 summary: no person
2211 2211
2212 2212 changeset: 3:10e46f2dcbf4
2213 2213 bisect: bad
2214 2214 user: person
2215 2215 date: Fri Jan 16 01:06:40 1970 +0000
2216 2216 summary: no user, no domain
2217 2217
2218 2218 changeset: 4:bbe44766e73d
2219 2219 bisect: bad (implicit)
2220 2220 branch: foo
2221 2221 user: person
2222 2222 date: Sat Jan 17 04:53:20 1970 +0000
2223 2223 summary: new branch
2224 2224
2225 2225 $ hg log --debug -T bisect -r 0:4
2226 2226 changeset: 0:1e4e1b8f71e05681d422154f5421e385fec3454f
2227 2227 bisect: good (implicit)
2228 2228 phase: public
2229 2229 parent: -1:0000000000000000000000000000000000000000
2230 2230 parent: -1:0000000000000000000000000000000000000000
2231 2231 manifest: 0:a0c8bcbbb45c63b90b70ad007bf38961f64f2af0
2232 2232 user: User Name <user@hostname>
2233 2233 date: Mon Jan 12 13:46:40 1970 +0000
2234 2234 files+: a
2235 2235 extra: branch=default
2236 2236 description:
2237 2237 line 1
2238 2238 line 2
2239 2239
2240 2240
2241 2241 changeset: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965
2242 2242 bisect: good
2243 2243 phase: public
2244 2244 parent: 0:1e4e1b8f71e05681d422154f5421e385fec3454f
2245 2245 parent: -1:0000000000000000000000000000000000000000
2246 2246 manifest: 1:4e8d705b1e53e3f9375e0e60dc7b525d8211fe55
2247 2247 user: A. N. Other <other@place>
2248 2248 date: Tue Jan 13 17:33:20 1970 +0000
2249 2249 files+: b
2250 2250 extra: branch=default
2251 2251 description:
2252 2252 other 1
2253 2253 other 2
2254 2254
2255 2255 other 3
2256 2256
2257 2257
2258 2258 changeset: 2:97054abb4ab824450e9164180baf491ae0078465
2259 2259 bisect: untested
2260 2260 phase: public
2261 2261 parent: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965
2262 2262 parent: -1:0000000000000000000000000000000000000000
2263 2263 manifest: 2:6e0e82995c35d0d57a52aca8da4e56139e06b4b1
2264 2264 user: other@place
2265 2265 date: Wed Jan 14 21:20:00 1970 +0000
2266 2266 files+: c
2267 2267 extra: branch=default
2268 2268 description:
2269 2269 no person
2270 2270
2271 2271
2272 2272 changeset: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47
2273 2273 bisect: bad
2274 2274 phase: public
2275 2275 parent: 2:97054abb4ab824450e9164180baf491ae0078465
2276 2276 parent: -1:0000000000000000000000000000000000000000
2277 2277 manifest: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc
2278 2278 user: person
2279 2279 date: Fri Jan 16 01:06:40 1970 +0000
2280 2280 files: c
2281 2281 extra: branch=default
2282 2282 description:
2283 2283 no user, no domain
2284 2284
2285 2285
2286 2286 changeset: 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
2287 2287 bisect: bad (implicit)
2288 2288 branch: foo
2289 2289 phase: draft
2290 2290 parent: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47
2291 2291 parent: -1:0000000000000000000000000000000000000000
2292 2292 manifest: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc
2293 2293 user: person
2294 2294 date: Sat Jan 17 04:53:20 1970 +0000
2295 2295 extra: branch=foo
2296 2296 description:
2297 2297 new branch
2298 2298
2299 2299
2300 2300 $ hg log -v -T bisect -r 0:4
2301 2301 changeset: 0:1e4e1b8f71e0
2302 2302 bisect: good (implicit)
2303 2303 user: User Name <user@hostname>
2304 2304 date: Mon Jan 12 13:46:40 1970 +0000
2305 2305 files: a
2306 2306 description:
2307 2307 line 1
2308 2308 line 2
2309 2309
2310 2310
2311 2311 changeset: 1:b608e9d1a3f0
2312 2312 bisect: good
2313 2313 user: A. N. Other <other@place>
2314 2314 date: Tue Jan 13 17:33:20 1970 +0000
2315 2315 files: b
2316 2316 description:
2317 2317 other 1
2318 2318 other 2
2319 2319
2320 2320 other 3
2321 2321
2322 2322
2323 2323 changeset: 2:97054abb4ab8
2324 2324 bisect: untested
2325 2325 user: other@place
2326 2326 date: Wed Jan 14 21:20:00 1970 +0000
2327 2327 files: c
2328 2328 description:
2329 2329 no person
2330 2330
2331 2331
2332 2332 changeset: 3:10e46f2dcbf4
2333 2333 bisect: bad
2334 2334 user: person
2335 2335 date: Fri Jan 16 01:06:40 1970 +0000
2336 2336 files: c
2337 2337 description:
2338 2338 no user, no domain
2339 2339
2340 2340
2341 2341 changeset: 4:bbe44766e73d
2342 2342 bisect: bad (implicit)
2343 2343 branch: foo
2344 2344 user: person
2345 2345 date: Sat Jan 17 04:53:20 1970 +0000
2346 2346 description:
2347 2347 new branch
2348 2348
2349 2349
2350 2350 $ hg --color=debug log -T bisect -r 0:4
2351 2351 [log.changeset changeset.public|changeset: 0:1e4e1b8f71e0]
2352 2352 [log.bisect bisect.good|bisect: good (implicit)]
2353 2353 [log.user|user: User Name <user@hostname>]
2354 2354 [log.date|date: Mon Jan 12 13:46:40 1970 +0000]
2355 2355 [log.summary|summary: line 1]
2356 2356
2357 2357 [log.changeset changeset.public|changeset: 1:b608e9d1a3f0]
2358 2358 [log.bisect bisect.good|bisect: good]
2359 2359 [log.user|user: A. N. Other <other@place>]
2360 2360 [log.date|date: Tue Jan 13 17:33:20 1970 +0000]
2361 2361 [log.summary|summary: other 1]
2362 2362
2363 2363 [log.changeset changeset.public|changeset: 2:97054abb4ab8]
2364 2364 [log.bisect bisect.untested|bisect: untested]
2365 2365 [log.user|user: other@place]
2366 2366 [log.date|date: Wed Jan 14 21:20:00 1970 +0000]
2367 2367 [log.summary|summary: no person]
2368 2368
2369 2369 [log.changeset changeset.public|changeset: 3:10e46f2dcbf4]
2370 2370 [log.bisect bisect.bad|bisect: bad]
2371 2371 [log.user|user: person]
2372 2372 [log.date|date: Fri Jan 16 01:06:40 1970 +0000]
2373 2373 [log.summary|summary: no user, no domain]
2374 2374
2375 2375 [log.changeset changeset.draft|changeset: 4:bbe44766e73d]
2376 2376 [log.bisect bisect.bad|bisect: bad (implicit)]
2377 2377 [log.branch|branch: foo]
2378 2378 [log.user|user: person]
2379 2379 [log.date|date: Sat Jan 17 04:53:20 1970 +0000]
2380 2380 [log.summary|summary: new branch]
2381 2381
2382 2382 $ hg --color=debug log --debug -T bisect -r 0:4
2383 2383 [log.changeset changeset.public|changeset: 0:1e4e1b8f71e05681d422154f5421e385fec3454f]
2384 2384 [log.bisect bisect.good|bisect: good (implicit)]
2385 2385 [log.phase|phase: public]
2386 2386 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2387 2387 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2388 2388 [ui.debug log.manifest|manifest: 0:a0c8bcbbb45c63b90b70ad007bf38961f64f2af0]
2389 2389 [log.user|user: User Name <user@hostname>]
2390 2390 [log.date|date: Mon Jan 12 13:46:40 1970 +0000]
2391 2391 [ui.debug log.files|files+: a]
2392 2392 [ui.debug log.extra|extra: branch=default]
2393 2393 [ui.note log.description|description:]
2394 2394 [ui.note log.description|line 1
2395 2395 line 2]
2396 2396
2397 2397
2398 2398 [log.changeset changeset.public|changeset: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965]
2399 2399 [log.bisect bisect.good|bisect: good]
2400 2400 [log.phase|phase: public]
2401 2401 [log.parent changeset.public|parent: 0:1e4e1b8f71e05681d422154f5421e385fec3454f]
2402 2402 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2403 2403 [ui.debug log.manifest|manifest: 1:4e8d705b1e53e3f9375e0e60dc7b525d8211fe55]
2404 2404 [log.user|user: A. N. Other <other@place>]
2405 2405 [log.date|date: Tue Jan 13 17:33:20 1970 +0000]
2406 2406 [ui.debug log.files|files+: b]
2407 2407 [ui.debug log.extra|extra: branch=default]
2408 2408 [ui.note log.description|description:]
2409 2409 [ui.note log.description|other 1
2410 2410 other 2
2411 2411
2412 2412 other 3]
2413 2413
2414 2414
2415 2415 [log.changeset changeset.public|changeset: 2:97054abb4ab824450e9164180baf491ae0078465]
2416 2416 [log.bisect bisect.untested|bisect: untested]
2417 2417 [log.phase|phase: public]
2418 2418 [log.parent changeset.public|parent: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965]
2419 2419 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2420 2420 [ui.debug log.manifest|manifest: 2:6e0e82995c35d0d57a52aca8da4e56139e06b4b1]
2421 2421 [log.user|user: other@place]
2422 2422 [log.date|date: Wed Jan 14 21:20:00 1970 +0000]
2423 2423 [ui.debug log.files|files+: c]
2424 2424 [ui.debug log.extra|extra: branch=default]
2425 2425 [ui.note log.description|description:]
2426 2426 [ui.note log.description|no person]
2427 2427
2428 2428
2429 2429 [log.changeset changeset.public|changeset: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47]
2430 2430 [log.bisect bisect.bad|bisect: bad]
2431 2431 [log.phase|phase: public]
2432 2432 [log.parent changeset.public|parent: 2:97054abb4ab824450e9164180baf491ae0078465]
2433 2433 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2434 2434 [ui.debug log.manifest|manifest: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc]
2435 2435 [log.user|user: person]
2436 2436 [log.date|date: Fri Jan 16 01:06:40 1970 +0000]
2437 2437 [ui.debug log.files|files: c]
2438 2438 [ui.debug log.extra|extra: branch=default]
2439 2439 [ui.note log.description|description:]
2440 2440 [ui.note log.description|no user, no domain]
2441 2441
2442 2442
2443 2443 [log.changeset changeset.draft|changeset: 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74]
2444 2444 [log.bisect bisect.bad|bisect: bad (implicit)]
2445 2445 [log.branch|branch: foo]
2446 2446 [log.phase|phase: draft]
2447 2447 [log.parent changeset.public|parent: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47]
2448 2448 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2449 2449 [ui.debug log.manifest|manifest: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc]
2450 2450 [log.user|user: person]
2451 2451 [log.date|date: Sat Jan 17 04:53:20 1970 +0000]
2452 2452 [ui.debug log.extra|extra: branch=foo]
2453 2453 [ui.note log.description|description:]
2454 2454 [ui.note log.description|new branch]
2455 2455
2456 2456
2457 2457 $ hg --color=debug log -v -T bisect -r 0:4
2458 2458 [log.changeset changeset.public|changeset: 0:1e4e1b8f71e0]
2459 2459 [log.bisect bisect.good|bisect: good (implicit)]
2460 2460 [log.user|user: User Name <user@hostname>]
2461 2461 [log.date|date: Mon Jan 12 13:46:40 1970 +0000]
2462 2462 [ui.note log.files|files: a]
2463 2463 [ui.note log.description|description:]
2464 2464 [ui.note log.description|line 1
2465 2465 line 2]
2466 2466
2467 2467
2468 2468 [log.changeset changeset.public|changeset: 1:b608e9d1a3f0]
2469 2469 [log.bisect bisect.good|bisect: good]
2470 2470 [log.user|user: A. N. Other <other@place>]
2471 2471 [log.date|date: Tue Jan 13 17:33:20 1970 +0000]
2472 2472 [ui.note log.files|files: b]
2473 2473 [ui.note log.description|description:]
2474 2474 [ui.note log.description|other 1
2475 2475 other 2
2476 2476
2477 2477 other 3]
2478 2478
2479 2479
2480 2480 [log.changeset changeset.public|changeset: 2:97054abb4ab8]
2481 2481 [log.bisect bisect.untested|bisect: untested]
2482 2482 [log.user|user: other@place]
2483 2483 [log.date|date: Wed Jan 14 21:20:00 1970 +0000]
2484 2484 [ui.note log.files|files: c]
2485 2485 [ui.note log.description|description:]
2486 2486 [ui.note log.description|no person]
2487 2487
2488 2488
2489 2489 [log.changeset changeset.public|changeset: 3:10e46f2dcbf4]
2490 2490 [log.bisect bisect.bad|bisect: bad]
2491 2491 [log.user|user: person]
2492 2492 [log.date|date: Fri Jan 16 01:06:40 1970 +0000]
2493 2493 [ui.note log.files|files: c]
2494 2494 [ui.note log.description|description:]
2495 2495 [ui.note log.description|no user, no domain]
2496 2496
2497 2497
2498 2498 [log.changeset changeset.draft|changeset: 4:bbe44766e73d]
2499 2499 [log.bisect bisect.bad|bisect: bad (implicit)]
2500 2500 [log.branch|branch: foo]
2501 2501 [log.user|user: person]
2502 2502 [log.date|date: Sat Jan 17 04:53:20 1970 +0000]
2503 2503 [ui.note log.description|description:]
2504 2504 [ui.note log.description|new branch]
2505 2505
2506 2506
2507 2507 $ hg bisect --reset
2508 2508
2509 2509 Error on syntax:
2510 2510
2511 2511 $ echo 'x = "f' >> t
2512 2512 $ hg log
2513 2513 hg: parse error at t:3: unmatched quotes
2514 2514 [255]
2515 2515
2516 2516 $ hg log -T '{date'
2517 2517 hg: parse error at 1: unterminated template expansion
2518 2518 [255]
2519 2519
2520 2520 Behind the scenes, this will throw TypeError
2521 2521
2522 2522 $ hg log -l 3 --template '{date|obfuscate}\n'
2523 2523 abort: template filter 'obfuscate' is not compatible with keyword 'date'
2524 2524 [255]
2525 2525
2526 2526 Behind the scenes, this will throw a ValueError
2527 2527
2528 2528 $ hg log -l 3 --template 'line: {desc|shortdate}\n'
2529 2529 abort: template filter 'shortdate' is not compatible with keyword 'desc'
2530 2530 [255]
2531 2531
2532 2532 Behind the scenes, this will throw AttributeError
2533 2533
2534 2534 $ hg log -l 3 --template 'line: {date|escape}\n'
2535 2535 abort: template filter 'escape' is not compatible with keyword 'date'
2536 2536 [255]
2537 2537
2538 2538 $ hg log -l 3 --template 'line: {extras|localdate}\n'
2539 2539 hg: parse error: localdate expects a date information
2540 2540 [255]
2541 2541
2542 2542 Behind the scenes, this will throw ValueError
2543 2543
2544 2544 $ hg tip --template '{author|email|date}\n'
2545 2545 hg: parse error: date expects a date information
2546 2546 [255]
2547 2547
2548 2548 Error in nested template:
2549 2549
2550 2550 $ hg log -T '{"date'
2551 2551 hg: parse error at 2: unterminated string
2552 2552 [255]
2553 2553
2554 2554 $ hg log -T '{"foo{date|=}"}'
2555 2555 hg: parse error at 11: syntax error
2556 2556 [255]
2557 2557
2558 2558 Thrown an error if a template function doesn't exist
2559 2559
2560 2560 $ hg tip --template '{foo()}\n'
2561 2561 hg: parse error: unknown function 'foo'
2562 2562 [255]
2563 2563
2564 2564 Pass generator object created by template function to filter
2565 2565
2566 2566 $ hg log -l 1 --template '{if(author, author)|user}\n'
2567 2567 test
2568 2568
2569 2569 Test diff function:
2570 2570
2571 2571 $ hg diff -c 8
2572 2572 diff -r 29114dbae42b -r 95c24699272e fourth
2573 2573 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2574 2574 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
2575 2575 @@ -0,0 +1,1 @@
2576 2576 +second
2577 2577 diff -r 29114dbae42b -r 95c24699272e second
2578 2578 --- a/second Mon Jan 12 13:46:40 1970 +0000
2579 2579 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
2580 2580 @@ -1,1 +0,0 @@
2581 2581 -second
2582 2582 diff -r 29114dbae42b -r 95c24699272e third
2583 2583 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2584 2584 +++ b/third Wed Jan 01 10:01:00 2020 +0000
2585 2585 @@ -0,0 +1,1 @@
2586 2586 +third
2587 2587
2588 2588 $ hg log -r 8 -T "{diff()}"
2589 2589 diff -r 29114dbae42b -r 95c24699272e fourth
2590 2590 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2591 2591 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
2592 2592 @@ -0,0 +1,1 @@
2593 2593 +second
2594 2594 diff -r 29114dbae42b -r 95c24699272e second
2595 2595 --- a/second Mon Jan 12 13:46:40 1970 +0000
2596 2596 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
2597 2597 @@ -1,1 +0,0 @@
2598 2598 -second
2599 2599 diff -r 29114dbae42b -r 95c24699272e third
2600 2600 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2601 2601 +++ b/third Wed Jan 01 10:01:00 2020 +0000
2602 2602 @@ -0,0 +1,1 @@
2603 2603 +third
2604 2604
2605 2605 $ hg log -r 8 -T "{diff('glob:f*')}"
2606 2606 diff -r 29114dbae42b -r 95c24699272e fourth
2607 2607 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2608 2608 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
2609 2609 @@ -0,0 +1,1 @@
2610 2610 +second
2611 2611
2612 2612 $ hg log -r 8 -T "{diff('', 'glob:f*')}"
2613 2613 diff -r 29114dbae42b -r 95c24699272e second
2614 2614 --- a/second Mon Jan 12 13:46:40 1970 +0000
2615 2615 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
2616 2616 @@ -1,1 +0,0 @@
2617 2617 -second
2618 2618 diff -r 29114dbae42b -r 95c24699272e third
2619 2619 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2620 2620 +++ b/third Wed Jan 01 10:01:00 2020 +0000
2621 2621 @@ -0,0 +1,1 @@
2622 2622 +third
2623 2623
2624 2624 $ hg log -r 8 -T "{diff('FOURTH'|lower)}"
2625 2625 diff -r 29114dbae42b -r 95c24699272e fourth
2626 2626 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2627 2627 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
2628 2628 @@ -0,0 +1,1 @@
2629 2629 +second
2630 2630
2631 2631 $ cd ..
2632 2632
2633 2633
2634 2634 latesttag:
2635 2635
2636 2636 $ hg init latesttag
2637 2637 $ cd latesttag
2638 2638
2639 2639 $ echo a > file
2640 2640 $ hg ci -Am a -d '0 0'
2641 2641 adding file
2642 2642
2643 2643 $ echo b >> file
2644 2644 $ hg ci -m b -d '1 0'
2645 2645
2646 2646 $ echo c >> head1
2647 2647 $ hg ci -Am h1c -d '2 0'
2648 2648 adding head1
2649 2649
2650 2650 $ hg update -q 1
2651 2651 $ echo d >> head2
2652 2652 $ hg ci -Am h2d -d '3 0'
2653 2653 adding head2
2654 2654 created new head
2655 2655
2656 2656 $ echo e >> head2
2657 2657 $ hg ci -m h2e -d '4 0'
2658 2658
2659 2659 $ hg merge -q
2660 2660 $ hg ci -m merge -d '5 -3600'
2661 2661
2662 2662 No tag set:
2663 2663
2664 2664 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
2665 2665 5: null+5
2666 2666 4: null+4
2667 2667 3: null+3
2668 2668 2: null+3
2669 2669 1: null+2
2670 2670 0: null+1
2671 2671
2672 2672 One common tag: longest path wins:
2673 2673
2674 2674 $ hg tag -r 1 -m t1 -d '6 0' t1
2675 2675 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
2676 2676 6: t1+4
2677 2677 5: t1+3
2678 2678 4: t1+2
2679 2679 3: t1+1
2680 2680 2: t1+1
2681 2681 1: t1+0
2682 2682 0: null+1
2683 2683
2684 2684 One ancestor tag: more recent wins:
2685 2685
2686 2686 $ hg tag -r 2 -m t2 -d '7 0' t2
2687 2687 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
2688 2688 7: t2+3
2689 2689 6: t2+2
2690 2690 5: t2+1
2691 2691 4: t1+2
2692 2692 3: t1+1
2693 2693 2: t2+0
2694 2694 1: t1+0
2695 2695 0: null+1
2696 2696
2697 2697 Two branch tags: more recent wins:
2698 2698
2699 2699 $ hg tag -r 3 -m t3 -d '8 0' t3
2700 2700 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
2701 2701 8: t3+5
2702 2702 7: t3+4
2703 2703 6: t3+3
2704 2704 5: t3+2
2705 2705 4: t3+1
2706 2706 3: t3+0
2707 2707 2: t2+0
2708 2708 1: t1+0
2709 2709 0: null+1
2710 2710
2711 2711 Merged tag overrides:
2712 2712
2713 2713 $ hg tag -r 5 -m t5 -d '9 0' t5
2714 2714 $ hg tag -r 3 -m at3 -d '10 0' at3
2715 2715 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
2716 2716 10: t5+5
2717 2717 9: t5+4
2718 2718 8: t5+3
2719 2719 7: t5+2
2720 2720 6: t5+1
2721 2721 5: t5+0
2722 2722 4: at3:t3+1
2723 2723 3: at3:t3+0
2724 2724 2: t2+0
2725 2725 1: t1+0
2726 2726 0: null+1
2727 2727
2728 2728 $ hg log --template "{rev}: {latesttag % '{tag}+{distance},{changes} '}\n"
2729 2729 10: t5+5,5
2730 2730 9: t5+4,4
2731 2731 8: t5+3,3
2732 2732 7: t5+2,2
2733 2733 6: t5+1,1
2734 2734 5: t5+0,0
2735 2735 4: at3+1,1 t3+1,1
2736 2736 3: at3+0,0 t3+0,0
2737 2737 2: t2+0,0
2738 2738 1: t1+0,0
2739 2739 0: null+1,1
2740 2740
2741 2741 $ hg log --template "{rev}: {latesttag('re:^t[13]$') % '{tag}, C: {changes}, D: {distance}'}\n"
2742 2742 10: t3, C: 8, D: 7
2743 2743 9: t3, C: 7, D: 6
2744 2744 8: t3, C: 6, D: 5
2745 2745 7: t3, C: 5, D: 4
2746 2746 6: t3, C: 4, D: 3
2747 2747 5: t3, C: 3, D: 2
2748 2748 4: t3, C: 1, D: 1
2749 2749 3: t3, C: 0, D: 0
2750 2750 2: t1, C: 1, D: 1
2751 2751 1: t1, C: 0, D: 0
2752 2752 0: null, C: 1, D: 1
2753 2753
2754 2754 $ cd ..
2755 2755
2756 2756
2757 2757 Style path expansion: issue1948 - ui.style option doesn't work on OSX
2758 2758 if it is a relative path
2759 2759
2760 2760 $ mkdir -p home/styles
2761 2761
2762 2762 $ cat > home/styles/teststyle <<EOF
2763 2763 > changeset = 'test {rev}:{node|short}\n'
2764 2764 > EOF
2765 2765
2766 2766 $ HOME=`pwd`/home; export HOME
2767 2767
2768 2768 $ cat > latesttag/.hg/hgrc <<EOF
2769 2769 > [ui]
2770 2770 > style = ~/styles/teststyle
2771 2771 > EOF
2772 2772
2773 2773 $ hg -R latesttag tip
2774 2774 test 10:9b4a630e5f5f
2775 2775
2776 2776 Test recursive showlist template (issue1989):
2777 2777
2778 2778 $ cat > style1989 <<EOF
2779 2779 > changeset = '{file_mods}{manifest}{extras}'
2780 2780 > file_mod = 'M|{author|person}\n'
2781 2781 > manifest = '{rev},{author}\n'
2782 2782 > extra = '{key}: {author}\n'
2783 2783 > EOF
2784 2784
2785 2785 $ hg -R latesttag log -r tip --style=style1989
2786 2786 M|test
2787 2787 10,test
2788 2788 branch: test
2789 2789
2790 2790 Test new-style inline templating:
2791 2791
2792 2792 $ hg log -R latesttag -r tip --template 'modified files: {file_mods % " {file}\n"}\n'
2793 2793 modified files: .hgtags
2794 2794
2795 2795
2796 2796 $ hg log -R latesttag -r tip -T '{rev % "a"}\n'
2797 2797 hg: parse error: keyword 'rev' is not iterable
2798 2798 [255]
2799 2799 $ hg log -R latesttag -r tip -T '{get(extras, "unknown") % "a"}\n'
2800 2800 hg: parse error: None is not iterable
2801 2801 [255]
2802 2802
2803 2803 Test the sub function of templating for expansion:
2804 2804
2805 2805 $ hg log -R latesttag -r 10 --template '{sub("[0-9]", "x", "{rev}")}\n'
2806 2806 xx
2807 2807
2808 2808 $ hg log -R latesttag -r 10 -T '{sub("[", "x", rev)}\n'
2809 2809 hg: parse error: sub got an invalid pattern: [
2810 2810 [255]
2811 2811 $ hg log -R latesttag -r 10 -T '{sub("[0-9]", r"\1", rev)}\n'
2812 2812 hg: parse error: sub got an invalid replacement: \1
2813 2813 [255]
2814 2814
2815 2815 Test the strip function with chars specified:
2816 2816
2817 2817 $ hg log -R latesttag --template '{desc}\n'
2818 2818 at3
2819 2819 t5
2820 2820 t3
2821 2821 t2
2822 2822 t1
2823 2823 merge
2824 2824 h2e
2825 2825 h2d
2826 2826 h1c
2827 2827 b
2828 2828 a
2829 2829
2830 2830 $ hg log -R latesttag --template '{strip(desc, "te")}\n'
2831 2831 at3
2832 2832 5
2833 2833 3
2834 2834 2
2835 2835 1
2836 2836 merg
2837 2837 h2
2838 2838 h2d
2839 2839 h1c
2840 2840 b
2841 2841 a
2842 2842
2843 2843 Test date format:
2844 2844
2845 2845 $ hg log -R latesttag --template 'date: {date(date, "%y %m %d %S %z")}\n'
2846 2846 date: 70 01 01 10 +0000
2847 2847 date: 70 01 01 09 +0000
2848 2848 date: 70 01 01 08 +0000
2849 2849 date: 70 01 01 07 +0000
2850 2850 date: 70 01 01 06 +0000
2851 2851 date: 70 01 01 05 +0100
2852 2852 date: 70 01 01 04 +0000
2853 2853 date: 70 01 01 03 +0000
2854 2854 date: 70 01 01 02 +0000
2855 2855 date: 70 01 01 01 +0000
2856 2856 date: 70 01 01 00 +0000
2857 2857
2858 2858 Test invalid date:
2859 2859
2860 2860 $ hg log -R latesttag -T '{date(rev)}\n'
2861 2861 hg: parse error: date expects a date information
2862 2862 [255]
2863 2863
2864 2864 Test integer literal:
2865 2865
2866 2866 $ hg debugtemplate -v '{(0)}\n'
2867 2867 (template
2868 2868 (group
2869 2869 ('integer', '0'))
2870 2870 ('string', '\n'))
2871 2871 0
2872 2872 $ hg debugtemplate -v '{(123)}\n'
2873 2873 (template
2874 2874 (group
2875 2875 ('integer', '123'))
2876 2876 ('string', '\n'))
2877 2877 123
2878 2878 $ hg debugtemplate -v '{(-4)}\n'
2879 2879 (template
2880 2880 (group
2881 2881 ('integer', '-4'))
2882 2882 ('string', '\n'))
2883 2883 -4
2884 2884 $ hg debugtemplate '{(-)}\n'
2885 2885 hg: parse error at 2: integer literal without digits
2886 2886 [255]
2887 2887 $ hg debugtemplate '{(-a)}\n'
2888 2888 hg: parse error at 2: integer literal without digits
2889 2889 [255]
2890 2890
2891 2891 top-level integer literal is interpreted as symbol (i.e. variable name):
2892 2892
2893 2893 $ hg debugtemplate -D 1=one -v '{1}\n'
2894 2894 (template
2895 2895 ('integer', '1')
2896 2896 ('string', '\n'))
2897 2897 one
2898 2898 $ hg debugtemplate -D 1=one -v '{if("t", "{1}")}\n'
2899 2899 (template
2900 2900 (func
2901 2901 ('symbol', 'if')
2902 2902 (list
2903 2903 ('string', 't')
2904 2904 (template
2905 2905 ('integer', '1'))))
2906 2906 ('string', '\n'))
2907 2907 one
2908 2908 $ hg debugtemplate -D 1=one -v '{1|stringify}\n'
2909 2909 (template
2910 2910 (|
2911 2911 ('integer', '1')
2912 2912 ('symbol', 'stringify'))
2913 2913 ('string', '\n'))
2914 2914 one
2915 2915
2916 2916 unless explicit symbol is expected:
2917 2917
2918 2918 $ hg log -Ra -r0 -T '{desc|1}\n'
2919 2919 hg: parse error: expected a symbol, got 'integer'
2920 2920 [255]
2921 2921 $ hg log -Ra -r0 -T '{1()}\n'
2922 2922 hg: parse error: expected a symbol, got 'integer'
2923 2923 [255]
2924 2924
2925 2925 Test string literal:
2926 2926
2927 2927 $ hg debugtemplate -Ra -r0 -v '{"string with no template fragment"}\n'
2928 2928 (template
2929 2929 ('string', 'string with no template fragment')
2930 2930 ('string', '\n'))
2931 2931 string with no template fragment
2932 2932 $ hg debugtemplate -Ra -r0 -v '{"template: {rev}"}\n'
2933 2933 (template
2934 2934 (template
2935 2935 ('string', 'template: ')
2936 2936 ('symbol', 'rev'))
2937 2937 ('string', '\n'))
2938 2938 template: 0
2939 2939 $ hg debugtemplate -Ra -r0 -v '{r"rawstring: {rev}"}\n'
2940 2940 (template
2941 2941 ('string', 'rawstring: {rev}')
2942 2942 ('string', '\n'))
2943 2943 rawstring: {rev}
2944 2944 $ hg debugtemplate -Ra -r0 -v '{files % r"rawstring: {file}"}\n'
2945 2945 (template
2946 2946 (%
2947 2947 ('symbol', 'files')
2948 2948 ('string', 'rawstring: {file}'))
2949 2949 ('string', '\n'))
2950 2950 rawstring: {file}
2951 2951
2952 2952 Test string escaping:
2953 2953
2954 2954 $ hg log -R latesttag -r 0 --template '>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
2955 2955 >
2956 2956 <>\n<[>
2957 2957 <>\n<]>
2958 2958 <>\n<
2959 2959
2960 2960 $ hg log -R latesttag -r 0 \
2961 2961 > --config ui.logtemplate='>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
2962 2962 >
2963 2963 <>\n<[>
2964 2964 <>\n<]>
2965 2965 <>\n<
2966 2966
2967 2967 $ hg log -R latesttag -r 0 -T esc \
2968 2968 > --config templates.esc='>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
2969 2969 >
2970 2970 <>\n<[>
2971 2971 <>\n<]>
2972 2972 <>\n<
2973 2973
2974 2974 $ cat <<'EOF' > esctmpl
2975 2975 > changeset = '>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
2976 2976 > EOF
2977 2977 $ hg log -R latesttag -r 0 --style ./esctmpl
2978 2978 >
2979 2979 <>\n<[>
2980 2980 <>\n<]>
2981 2981 <>\n<
2982 2982
2983 2983 Test string escaping of quotes:
2984 2984
2985 2985 $ hg log -Ra -r0 -T '{"\""}\n'
2986 2986 "
2987 2987 $ hg log -Ra -r0 -T '{"\\\""}\n'
2988 2988 \"
2989 2989 $ hg log -Ra -r0 -T '{r"\""}\n'
2990 2990 \"
2991 2991 $ hg log -Ra -r0 -T '{r"\\\""}\n'
2992 2992 \\\"
2993 2993
2994 2994
2995 2995 $ hg log -Ra -r0 -T '{"\""}\n'
2996 2996 "
2997 2997 $ hg log -Ra -r0 -T '{"\\\""}\n'
2998 2998 \"
2999 2999 $ hg log -Ra -r0 -T '{r"\""}\n'
3000 3000 \"
3001 3001 $ hg log -Ra -r0 -T '{r"\\\""}\n'
3002 3002 \\\"
3003 3003
3004 3004 Test exception in quoted template. single backslash before quotation mark is
3005 3005 stripped before parsing:
3006 3006
3007 3007 $ cat <<'EOF' > escquotetmpl
3008 3008 > changeset = "\" \\" \\\" \\\\" {files % \"{file}\"}\n"
3009 3009 > EOF
3010 3010 $ cd latesttag
3011 3011 $ hg log -r 2 --style ../escquotetmpl
3012 3012 " \" \" \\" head1
3013 3013
3014 3014 $ hg log -r 2 -T esc --config templates.esc='"{\"valid\"}\n"'
3015 3015 valid
3016 3016 $ hg log -r 2 -T esc --config templates.esc="'"'{\'"'"'valid\'"'"'}\n'"'"
3017 3017 valid
3018 3018
3019 3019 Test compatibility with 2.9.2-3.4 of escaped quoted strings in nested
3020 3020 _evalifliteral() templates (issue4733):
3021 3021
3022 3022 $ hg log -r 2 -T '{if(rev, "\"{rev}")}\n'
3023 3023 "2
3024 3024 $ hg log -r 2 -T '{if(rev, "{if(rev, \"\\\"{rev}\")}")}\n'
3025 3025 "2
3026 3026 $ hg log -r 2 -T '{if(rev, "{if(rev, \"{if(rev, \\\"\\\\\\\"{rev}\\\")}\")}")}\n'
3027 3027 "2
3028 3028
3029 3029 $ hg log -r 2 -T '{if(rev, "\\\"")}\n'
3030 3030 \"
3031 3031 $ hg log -r 2 -T '{if(rev, "{if(rev, \"\\\\\\\"\")}")}\n'
3032 3032 \"
3033 3033 $ hg log -r 2 -T '{if(rev, "{if(rev, \"{if(rev, \\\"\\\\\\\\\\\\\\\"\\\")}\")}")}\n'
3034 3034 \"
3035 3035
3036 3036 $ hg log -r 2 -T '{if(rev, r"\\\"")}\n'
3037 3037 \\\"
3038 3038 $ hg log -r 2 -T '{if(rev, "{if(rev, r\"\\\\\\\"\")}")}\n'
3039 3039 \\\"
3040 3040 $ hg log -r 2 -T '{if(rev, "{if(rev, \"{if(rev, r\\\"\\\\\\\\\\\\\\\"\\\")}\")}")}\n'
3041 3041 \\\"
3042 3042
3043 3043 escaped single quotes and errors:
3044 3044
3045 3045 $ hg log -r 2 -T "{if(rev, '{if(rev, \'foo\')}')}"'\n'
3046 3046 foo
3047 3047 $ hg log -r 2 -T "{if(rev, '{if(rev, r\'foo\')}')}"'\n'
3048 3048 foo
3049 3049 $ hg log -r 2 -T '{if(rev, "{if(rev, \")}")}\n'
3050 3050 hg: parse error at 21: unterminated string
3051 3051 [255]
3052 3052 $ hg log -r 2 -T '{if(rev, \"\\"")}\n'
3053 3053 hg: parse error: trailing \ in string
3054 3054 [255]
3055 3055 $ hg log -r 2 -T '{if(rev, r\"\\"")}\n'
3056 3056 hg: parse error: trailing \ in string
3057 3057 [255]
3058 3058
3059 3059 $ cd ..
3060 3060
3061 3061 Test leading backslashes:
3062 3062
3063 3063 $ cd latesttag
3064 3064 $ hg log -r 2 -T '\{rev} {files % "\{file}"}\n'
3065 3065 {rev} {file}
3066 3066 $ hg log -r 2 -T '\\{rev} {files % "\\{file}"}\n'
3067 3067 \2 \head1
3068 3068 $ hg log -r 2 -T '\\\{rev} {files % "\\\{file}"}\n'
3069 3069 \{rev} \{file}
3070 3070 $ cd ..
3071 3071
3072 3072 Test leading backslashes in "if" expression (issue4714):
3073 3073
3074 3074 $ cd latesttag
3075 3075 $ hg log -r 2 -T '{if("1", "\{rev}")} {if("1", r"\{rev}")}\n'
3076 3076 {rev} \{rev}
3077 3077 $ hg log -r 2 -T '{if("1", "\\{rev}")} {if("1", r"\\{rev}")}\n'
3078 3078 \2 \\{rev}
3079 3079 $ hg log -r 2 -T '{if("1", "\\\{rev}")} {if("1", r"\\\{rev}")}\n'
3080 3080 \{rev} \\\{rev}
3081 3081 $ cd ..
3082 3082
3083 3083 "string-escape"-ed "\x5c\x786e" becomes r"\x6e" (once) or r"n" (twice)
3084 3084
3085 3085 $ hg log -R a -r 0 --template '{if("1", "\x5c\x786e", "NG")}\n'
3086 3086 \x6e
3087 3087 $ hg log -R a -r 0 --template '{if("1", r"\x5c\x786e", "NG")}\n'
3088 3088 \x5c\x786e
3089 3089 $ hg log -R a -r 0 --template '{if("", "NG", "\x5c\x786e")}\n'
3090 3090 \x6e
3091 3091 $ hg log -R a -r 0 --template '{if("", "NG", r"\x5c\x786e")}\n'
3092 3092 \x5c\x786e
3093 3093
3094 3094 $ hg log -R a -r 2 --template '{ifeq("no perso\x6e", desc, "\x5c\x786e", "NG")}\n'
3095 3095 \x6e
3096 3096 $ hg log -R a -r 2 --template '{ifeq(r"no perso\x6e", desc, "NG", r"\x5c\x786e")}\n'
3097 3097 \x5c\x786e
3098 3098 $ hg log -R a -r 2 --template '{ifeq(desc, "no perso\x6e", "\x5c\x786e", "NG")}\n'
3099 3099 \x6e
3100 3100 $ hg log -R a -r 2 --template '{ifeq(desc, r"no perso\x6e", "NG", r"\x5c\x786e")}\n'
3101 3101 \x5c\x786e
3102 3102
3103 3103 $ hg log -R a -r 8 --template '{join(files, "\n")}\n'
3104 3104 fourth
3105 3105 second
3106 3106 third
3107 3107 $ hg log -R a -r 8 --template '{join(files, r"\n")}\n'
3108 3108 fourth\nsecond\nthird
3109 3109
3110 3110 $ hg log -R a -r 2 --template '{rstdoc("1st\n\n2nd", "htm\x6c")}'
3111 3111 <p>
3112 3112 1st
3113 3113 </p>
3114 3114 <p>
3115 3115 2nd
3116 3116 </p>
3117 3117 $ hg log -R a -r 2 --template '{rstdoc(r"1st\n\n2nd", "html")}'
3118 3118 <p>
3119 3119 1st\n\n2nd
3120 3120 </p>
3121 3121 $ hg log -R a -r 2 --template '{rstdoc("1st\n\n2nd", r"htm\x6c")}'
3122 3122 1st
3123 3123
3124 3124 2nd
3125 3125
3126 3126 $ hg log -R a -r 2 --template '{strip(desc, "\x6e")}\n'
3127 3127 o perso
3128 3128 $ hg log -R a -r 2 --template '{strip(desc, r"\x6e")}\n'
3129 3129 no person
3130 3130 $ hg log -R a -r 2 --template '{strip("no perso\x6e", "\x6e")}\n'
3131 3131 o perso
3132 3132 $ hg log -R a -r 2 --template '{strip(r"no perso\x6e", r"\x6e")}\n'
3133 3133 no perso
3134 3134
3135 3135 $ hg log -R a -r 2 --template '{sub("\\x6e", "\x2d", desc)}\n'
3136 3136 -o perso-
3137 3137 $ hg log -R a -r 2 --template '{sub(r"\\x6e", "-", desc)}\n'
3138 3138 no person
3139 3139 $ hg log -R a -r 2 --template '{sub("n", r"\x2d", desc)}\n'
3140 3140 \x2do perso\x2d
3141 3141 $ hg log -R a -r 2 --template '{sub("n", "\x2d", "no perso\x6e")}\n'
3142 3142 -o perso-
3143 3143 $ hg log -R a -r 2 --template '{sub("n", r"\x2d", r"no perso\x6e")}\n'
3144 3144 \x2do perso\x6e
3145 3145
3146 3146 $ hg log -R a -r 8 --template '{files % "{file}\n"}'
3147 3147 fourth
3148 3148 second
3149 3149 third
3150 3150
3151 3151 Test string escaping in nested expression:
3152 3152
3153 3153 $ hg log -R a -r 8 --template '{ifeq(r"\x6e", if("1", "\x5c\x786e"), join(files, "\x5c\x786e"))}\n'
3154 3154 fourth\x6esecond\x6ethird
3155 3155 $ hg log -R a -r 8 --template '{ifeq(if("1", r"\x6e"), "\x5c\x786e", join(files, "\x5c\x786e"))}\n'
3156 3156 fourth\x6esecond\x6ethird
3157 3157
3158 3158 $ hg log -R a -r 8 --template '{join(files, ifeq(branch, "default", "\x5c\x786e"))}\n'
3159 3159 fourth\x6esecond\x6ethird
3160 3160 $ hg log -R a -r 8 --template '{join(files, ifeq(branch, "default", r"\x5c\x786e"))}\n'
3161 3161 fourth\x5c\x786esecond\x5c\x786ethird
3162 3162
3163 3163 $ hg log -R a -r 3:4 --template '{rev}:{sub(if("1", "\x6e"), ifeq(branch, "foo", r"\x5c\x786e", "\x5c\x786e"), desc)}\n'
3164 3164 3:\x6eo user, \x6eo domai\x6e
3165 3165 4:\x5c\x786eew bra\x5c\x786ech
3166 3166
3167 3167 Test quotes in nested expression are evaluated just like a $(command)
3168 3168 substitution in POSIX shells:
3169 3169
3170 3170 $ hg log -R a -r 8 -T '{"{"{rev}:{node|short}"}"}\n'
3171 3171 8:95c24699272e
3172 3172 $ hg log -R a -r 8 -T '{"{"\{{rev}} \"{node|short}\""}"}\n'
3173 3173 {8} "95c24699272e"
3174 3174
3175 3175 Test recursive evaluation:
3176 3176
3177 3177 $ hg init r
3178 3178 $ cd r
3179 3179 $ echo a > a
3180 3180 $ hg ci -Am '{rev}'
3181 3181 adding a
3182 3182 $ hg log -r 0 --template '{if(rev, desc)}\n'
3183 3183 {rev}
3184 3184 $ hg log -r 0 --template '{if(rev, "{author} {rev}")}\n'
3185 3185 test 0
3186 3186
3187 3187 $ hg branch -q 'text.{rev}'
3188 3188 $ echo aa >> aa
3189 3189 $ hg ci -u '{node|short}' -m 'desc to be wrapped desc to be wrapped'
3190 3190
3191 3191 $ hg log -l1 --template '{fill(desc, "20", author, branch)}'
3192 3192 {node|short}desc to
3193 3193 text.{rev}be wrapped
3194 3194 text.{rev}desc to be
3195 3195 text.{rev}wrapped (no-eol)
3196 3196 $ hg log -l1 --template '{fill(desc, "20", "{node|short}:", "text.{rev}:")}'
3197 3197 bcc7ff960b8e:desc to
3198 3198 text.1:be wrapped
3199 3199 text.1:desc to be
3200 3200 text.1:wrapped (no-eol)
3201 3201 $ hg log -l1 -T '{fill(desc, date, "", "")}\n'
3202 3202 hg: parse error: fill expects an integer width
3203 3203 [255]
3204 3204
3205 3205 $ hg log -l 1 --template '{sub(r"[0-9]", "-", author)}'
3206 3206 {node|short} (no-eol)
3207 3207 $ hg log -l 1 --template '{sub(r"[0-9]", "-", "{node|short}")}'
3208 3208 bcc-ff---b-e (no-eol)
3209 3209
3210 3210 $ cat >> .hg/hgrc <<EOF
3211 3211 > [extensions]
3212 3212 > color=
3213 3213 > [color]
3214 3214 > mode=ansi
3215 3215 > text.{rev} = red
3216 3216 > text.1 = green
3217 3217 > EOF
3218 3218 $ hg log --color=always -l 1 --template '{label(branch, "text\n")}'
3219 3219 \x1b[0;31mtext\x1b[0m (esc)
3220 3220 $ hg log --color=always -l 1 --template '{label("text.{rev}", "text\n")}'
3221 3221 \x1b[0;32mtext\x1b[0m (esc)
3222 3222
3223 3223 color effect can be specified without quoting:
3224 3224
3225 3225 $ hg log --color=always -l 1 --template '{label(red, "text\n")}'
3226 3226 \x1b[0;31mtext\x1b[0m (esc)
3227 3227
3228 3228 label should be no-op if color is disabled:
3229 3229
3230 3230 $ hg log --color=never -l 1 --template '{label(red, "text\n")}'
3231 3231 text
3232 3232 $ hg log --config extensions.color=! -l 1 --template '{label(red, "text\n")}'
3233 3233 text
3234 3234
3235 3235 Test branches inside if statement:
3236 3236
3237 3237 $ hg log -r 0 --template '{if(branches, "yes", "no")}\n'
3238 3238 no
3239 3239
3240 3240 Test get function:
3241 3241
3242 3242 $ hg log -r 0 --template '{get(extras, "branch")}\n'
3243 3243 default
3244 3244 $ hg log -r 0 --template '{get(extras, "br{"anch"}")}\n'
3245 3245 default
3246 3246 $ hg log -r 0 --template '{get(files, "should_fail")}\n'
3247 3247 hg: parse error: get() expects a dict as first argument
3248 3248 [255]
3249 3249
3250 3250 Test localdate(date, tz) function:
3251 3251
3252 3252 $ TZ=JST-09 hg log -r0 -T '{date|localdate|isodate}\n'
3253 3253 1970-01-01 09:00 +0900
3254 3254 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "UTC")|isodate}\n'
3255 3255 1970-01-01 00:00 +0000
3256 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "blahUTC")|isodate}\n'
3257 hg: parse error: localdate expects a timezone
3258 [255]
3256 3259 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "+0200")|isodate}\n'
3257 3260 1970-01-01 02:00 +0200
3258 3261 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "0")|isodate}\n'
3259 3262 1970-01-01 00:00 +0000
3260 3263 $ TZ=JST-09 hg log -r0 -T '{localdate(date, 0)|isodate}\n'
3261 3264 1970-01-01 00:00 +0000
3262 3265 $ hg log -r0 -T '{localdate(date, "invalid")|isodate}\n'
3263 3266 hg: parse error: localdate expects a timezone
3264 3267 [255]
3265 3268 $ hg log -r0 -T '{localdate(date, date)|isodate}\n'
3266 3269 hg: parse error: localdate expects a timezone
3267 3270 [255]
3268 3271
3269 3272 Test shortest(node) function:
3270 3273
3271 3274 $ echo b > b
3272 3275 $ hg ci -qAm b
3273 3276 $ hg log --template '{shortest(node)}\n'
3274 3277 e777
3275 3278 bcc7
3276 3279 f776
3277 3280 $ hg log --template '{shortest(node, 10)}\n'
3278 3281 e777603221
3279 3282 bcc7ff960b
3280 3283 f7769ec2ab
3281 3284 $ hg log --template '{node|shortest}\n' -l1
3282 3285 e777
3283 3286
3284 3287 $ hg log -r 0 -T '{shortest(node, "1{"0"}")}\n'
3285 3288 f7769ec2ab
3286 3289 $ hg log -r 0 -T '{shortest(node, "not an int")}\n'
3287 3290 hg: parse error: shortest() expects an integer minlength
3288 3291 [255]
3289 3292
3290 3293 Test pad function
3291 3294
3292 3295 $ hg log --template '{pad(rev, 20)} {author|user}\n'
3293 3296 2 test
3294 3297 1 {node|short}
3295 3298 0 test
3296 3299
3297 3300 $ hg log --template '{pad(rev, 20, " ", True)} {author|user}\n'
3298 3301 2 test
3299 3302 1 {node|short}
3300 3303 0 test
3301 3304
3302 3305 $ hg log --template '{pad(rev, 20, "-", False)} {author|user}\n'
3303 3306 2------------------- test
3304 3307 1------------------- {node|short}
3305 3308 0------------------- test
3306 3309
3307 3310 Test template string in pad function
3308 3311
3309 3312 $ hg log -r 0 -T '{pad("\{{rev}}", 10)} {author|user}\n'
3310 3313 {0} test
3311 3314
3312 3315 $ hg log -r 0 -T '{pad(r"\{rev}", 10)} {author|user}\n'
3313 3316 \{rev} test
3314 3317
3315 3318 Test width argument passed to pad function
3316 3319
3317 3320 $ hg log -r 0 -T '{pad(rev, "1{"0"}")} {author|user}\n'
3318 3321 0 test
3319 3322 $ hg log -r 0 -T '{pad(rev, "not an int")}\n'
3320 3323 hg: parse error: pad() expects an integer width
3321 3324 [255]
3322 3325
3323 3326 Test separate function
3324 3327
3325 3328 $ hg log -r 0 -T '{separate("-", "", "a", "b", "", "", "c", "")}\n'
3326 3329 a-b-c
3327 3330 $ hg log -r 0 -T '{separate(" ", "{rev}:{node|short}", author|user, branch)}\n'
3328 3331 0:f7769ec2ab97 test default
3329 3332 $ hg log -r 0 --color=always -T '{separate(" ", "a", label(red, "b"), "c", label(red, ""), "d")}\n'
3330 3333 a \x1b[0;31mb\x1b[0m c d (esc)
3331 3334
3332 3335 Test ifcontains function
3333 3336
3334 3337 $ hg log --template '{rev} {ifcontains(rev, "2 two 0", "is in the string", "is not")}\n'
3335 3338 2 is in the string
3336 3339 1 is not
3337 3340 0 is in the string
3338 3341
3339 3342 $ hg log -T '{rev} {ifcontains(rev, "2 two{" 0"}", "is in the string", "is not")}\n'
3340 3343 2 is in the string
3341 3344 1 is not
3342 3345 0 is in the string
3343 3346
3344 3347 $ hg log --template '{rev} {ifcontains("a", file_adds, "added a", "did not add a")}\n'
3345 3348 2 did not add a
3346 3349 1 did not add a
3347 3350 0 added a
3348 3351
3349 3352 $ hg log --debug -T '{rev}{ifcontains(1, parents, " is parent of 1")}\n'
3350 3353 2 is parent of 1
3351 3354 1
3352 3355 0
3353 3356
3354 3357 Test revset function
3355 3358
3356 3359 $ hg log --template '{rev} {ifcontains(rev, revset("."), "current rev", "not current rev")}\n'
3357 3360 2 current rev
3358 3361 1 not current rev
3359 3362 0 not current rev
3360 3363
3361 3364 $ hg log --template '{rev} {ifcontains(rev, revset(". + .^"), "match rev", "not match rev")}\n'
3362 3365 2 match rev
3363 3366 1 match rev
3364 3367 0 not match rev
3365 3368
3366 3369 $ hg log --template '{rev} Parents: {revset("parents(%s)", rev)}\n'
3367 3370 2 Parents: 1
3368 3371 1 Parents: 0
3369 3372 0 Parents:
3370 3373
3371 3374 $ cat >> .hg/hgrc <<EOF
3372 3375 > [revsetalias]
3373 3376 > myparents(\$1) = parents(\$1)
3374 3377 > EOF
3375 3378 $ hg log --template '{rev} Parents: {revset("myparents(%s)", rev)}\n'
3376 3379 2 Parents: 1
3377 3380 1 Parents: 0
3378 3381 0 Parents:
3379 3382
3380 3383 $ hg log --template 'Rev: {rev}\n{revset("::%s", rev) % "Ancestor: {revision}\n"}\n'
3381 3384 Rev: 2
3382 3385 Ancestor: 0
3383 3386 Ancestor: 1
3384 3387 Ancestor: 2
3385 3388
3386 3389 Rev: 1
3387 3390 Ancestor: 0
3388 3391 Ancestor: 1
3389 3392
3390 3393 Rev: 0
3391 3394 Ancestor: 0
3392 3395
3393 3396 $ hg log --template '{revset("TIP"|lower)}\n' -l1
3394 3397 2
3395 3398
3396 3399 $ hg log -T '{revset("%s", "t{"ip"}")}\n' -l1
3397 3400 2
3398 3401
3399 3402 a list template is evaluated for each item of revset/parents
3400 3403
3401 3404 $ hg log -T '{rev} p: {revset("p1(%s)", rev) % "{rev}:{node|short}"}\n'
3402 3405 2 p: 1:bcc7ff960b8e
3403 3406 1 p: 0:f7769ec2ab97
3404 3407 0 p:
3405 3408
3406 3409 $ hg log --debug -T '{rev} p:{parents % " {rev}:{node|short}"}\n'
3407 3410 2 p: 1:bcc7ff960b8e -1:000000000000
3408 3411 1 p: 0:f7769ec2ab97 -1:000000000000
3409 3412 0 p: -1:000000000000 -1:000000000000
3410 3413
3411 3414 therefore, 'revcache' should be recreated for each rev
3412 3415
3413 3416 $ hg log -T '{rev} {file_adds}\np {revset("p1(%s)", rev) % "{file_adds}"}\n'
3414 3417 2 aa b
3415 3418 p
3416 3419 1
3417 3420 p a
3418 3421 0 a
3419 3422 p
3420 3423
3421 3424 $ hg log --debug -T '{rev} {file_adds}\np {parents % "{file_adds}"}\n'
3422 3425 2 aa b
3423 3426 p
3424 3427 1
3425 3428 p a
3426 3429 0 a
3427 3430 p
3428 3431
3429 3432 a revset item must be evaluated as an integer revision, not an offset from tip
3430 3433
3431 3434 $ hg log -l 1 -T '{revset("null") % "{rev}:{node|short}"}\n'
3432 3435 -1:000000000000
3433 3436 $ hg log -l 1 -T '{revset("%s", "null") % "{rev}:{node|short}"}\n'
3434 3437 -1:000000000000
3435 3438
3436 3439 join() should pick '{rev}' from revset items:
3437 3440
3438 3441 $ hg log -R ../a -T '{join(revset("parents(%d)", rev), ", ")}\n' -r6
3439 3442 4, 5
3440 3443
3441 3444 on the other hand, parents are formatted as '{rev}:{node|formatnode}' by
3442 3445 default. join() should agree with the default formatting:
3443 3446
3444 3447 $ hg log -R ../a -T '{join(parents, ", ")}\n' -r6
3445 3448 5:13207e5a10d9, 4:bbe44766e73d
3446 3449
3447 3450 $ hg log -R ../a -T '{join(parents, ",\n")}\n' -r6 --debug
3448 3451 5:13207e5a10d9fd28ec424934298e176197f2c67f,
3449 3452 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
3450 3453
3451 3454 Test active bookmark templating
3452 3455
3453 3456 $ hg book foo
3454 3457 $ hg book bar
3455 3458 $ hg log --template "{rev} {bookmarks % '{bookmark}{ifeq(bookmark, active, \"*\")} '}\n"
3456 3459 2 bar* foo
3457 3460 1
3458 3461 0
3459 3462 $ hg log --template "{rev} {activebookmark}\n"
3460 3463 2 bar
3461 3464 1
3462 3465 0
3463 3466 $ hg bookmarks --inactive bar
3464 3467 $ hg log --template "{rev} {activebookmark}\n"
3465 3468 2
3466 3469 1
3467 3470 0
3468 3471 $ hg book -r1 baz
3469 3472 $ hg log --template "{rev} {join(bookmarks, ' ')}\n"
3470 3473 2 bar foo
3471 3474 1 baz
3472 3475 0
3473 3476 $ hg log --template "{rev} {ifcontains('foo', bookmarks, 't', 'f')}\n"
3474 3477 2 t
3475 3478 1 f
3476 3479 0 f
3477 3480
3478 3481 Test namespaces dict
3479 3482
3480 3483 $ hg log -T '{rev}{namespaces % " {namespace}={join(names, ",")}"}\n'
3481 3484 2 bookmarks=bar,foo tags=tip branches=text.{rev}
3482 3485 1 bookmarks=baz tags= branches=text.{rev}
3483 3486 0 bookmarks= tags= branches=default
3484 3487 $ hg log -r2 -T '{namespaces % "{namespace}: {names}\n"}'
3485 3488 bookmarks: bar foo
3486 3489 tags: tip
3487 3490 branches: text.{rev}
3488 3491 $ hg log -r2 -T '{namespaces % "{namespace}:\n{names % " {name}\n"}"}'
3489 3492 bookmarks:
3490 3493 bar
3491 3494 foo
3492 3495 tags:
3493 3496 tip
3494 3497 branches:
3495 3498 text.{rev}
3496 3499 $ hg log -r2 -T '{get(namespaces, "bookmarks") % "{name}\n"}'
3497 3500 bar
3498 3501 foo
3499 3502
3500 3503 Test stringify on sub expressions
3501 3504
3502 3505 $ cd ..
3503 3506 $ hg log -R a -r 8 --template '{join(files, if("1", if("1", ", ")))}\n'
3504 3507 fourth, second, third
3505 3508 $ hg log -R a -r 8 --template '{strip(if("1", if("1", "-abc-")), if("1", if("1", "-")))}\n'
3506 3509 abc
3507 3510
3508 3511 Test splitlines
3509 3512
3510 3513 $ hg log -Gv -R a --template "{splitlines(desc) % 'foo {line}\n'}"
3511 3514 @ foo Modify, add, remove, rename
3512 3515 |
3513 3516 o foo future
3514 3517 |
3515 3518 o foo third
3516 3519 |
3517 3520 o foo second
3518 3521
3519 3522 o foo merge
3520 3523 |\
3521 3524 | o foo new head
3522 3525 | |
3523 3526 o | foo new branch
3524 3527 |/
3525 3528 o foo no user, no domain
3526 3529 |
3527 3530 o foo no person
3528 3531 |
3529 3532 o foo other 1
3530 3533 | foo other 2
3531 3534 | foo
3532 3535 | foo other 3
3533 3536 o foo line 1
3534 3537 foo line 2
3535 3538
3536 3539 Test startswith
3537 3540 $ hg log -Gv -R a --template "{startswith(desc)}"
3538 3541 hg: parse error: startswith expects two arguments
3539 3542 [255]
3540 3543
3541 3544 $ hg log -Gv -R a --template "{startswith('line', desc)}"
3542 3545 @
3543 3546 |
3544 3547 o
3545 3548 |
3546 3549 o
3547 3550 |
3548 3551 o
3549 3552
3550 3553 o
3551 3554 |\
3552 3555 | o
3553 3556 | |
3554 3557 o |
3555 3558 |/
3556 3559 o
3557 3560 |
3558 3561 o
3559 3562 |
3560 3563 o
3561 3564 |
3562 3565 o line 1
3563 3566 line 2
3564 3567
3565 3568 Test bad template with better error message
3566 3569
3567 3570 $ hg log -Gv -R a --template '{desc|user()}'
3568 3571 hg: parse error: expected a symbol, got 'func'
3569 3572 [255]
3570 3573
3571 3574 Test word function (including index out of bounds graceful failure)
3572 3575
3573 3576 $ hg log -Gv -R a --template "{word('1', desc)}"
3574 3577 @ add,
3575 3578 |
3576 3579 o
3577 3580 |
3578 3581 o
3579 3582 |
3580 3583 o
3581 3584
3582 3585 o
3583 3586 |\
3584 3587 | o head
3585 3588 | |
3586 3589 o | branch
3587 3590 |/
3588 3591 o user,
3589 3592 |
3590 3593 o person
3591 3594 |
3592 3595 o 1
3593 3596 |
3594 3597 o 1
3595 3598
3596 3599
3597 3600 Test word third parameter used as splitter
3598 3601
3599 3602 $ hg log -Gv -R a --template "{word('0', desc, 'o')}"
3600 3603 @ M
3601 3604 |
3602 3605 o future
3603 3606 |
3604 3607 o third
3605 3608 |
3606 3609 o sec
3607 3610
3608 3611 o merge
3609 3612 |\
3610 3613 | o new head
3611 3614 | |
3612 3615 o | new branch
3613 3616 |/
3614 3617 o n
3615 3618 |
3616 3619 o n
3617 3620 |
3618 3621 o
3619 3622 |
3620 3623 o line 1
3621 3624 line 2
3622 3625
3623 3626 Test word error messages for not enough and too many arguments
3624 3627
3625 3628 $ hg log -Gv -R a --template "{word('0')}"
3626 3629 hg: parse error: word expects two or three arguments, got 1
3627 3630 [255]
3628 3631
3629 3632 $ hg log -Gv -R a --template "{word('0', desc, 'o', 'h', 'b', 'o', 'y')}"
3630 3633 hg: parse error: word expects two or three arguments, got 7
3631 3634 [255]
3632 3635
3633 3636 Test word for integer literal
3634 3637
3635 3638 $ hg log -R a --template "{word(2, desc)}\n" -r0
3636 3639 line
3637 3640
3638 3641 Test word for invalid numbers
3639 3642
3640 3643 $ hg log -Gv -R a --template "{word('a', desc)}"
3641 3644 hg: parse error: word expects an integer index
3642 3645 [255]
3643 3646
3644 3647 Test word for out of range
3645 3648
3646 3649 $ hg log -R a --template "{word(10000, desc)}"
3647 3650 $ hg log -R a --template "{word(-10000, desc)}"
3648 3651
3649 3652 Test indent and not adding to empty lines
3650 3653
3651 3654 $ hg log -T "-----\n{indent(desc, '>> ', ' > ')}\n" -r 0:1 -R a
3652 3655 -----
3653 3656 > line 1
3654 3657 >> line 2
3655 3658 -----
3656 3659 > other 1
3657 3660 >> other 2
3658 3661
3659 3662 >> other 3
3660 3663
3661 3664 Test with non-strings like dates
3662 3665
3663 3666 $ hg log -T "{indent(date, ' ')}\n" -r 2:3 -R a
3664 3667 1200000.00
3665 3668 1300000.00
3666 3669
3667 3670 Test broken string escapes:
3668 3671
3669 3672 $ hg log -T "bogus\\" -R a
3670 3673 hg: parse error: trailing \ in string
3671 3674 [255]
3672 3675 $ hg log -T "\\xy" -R a
3673 3676 hg: parse error: invalid \x escape
3674 3677 [255]
3675 3678
3676 3679 json filter should escape HTML tags so that the output can be embedded in hgweb:
3677 3680
3678 3681 $ hg log -T "{'<foo@example.org>'|json}\n" -R a -l1
3679 3682 "\u003cfoo@example.org\u003e"
3680 3683
3681 3684 Templater supports aliases of symbol and func() styles:
3682 3685
3683 3686 $ hg clone -q a aliases
3684 3687 $ cd aliases
3685 3688 $ cat <<EOF >> .hg/hgrc
3686 3689 > [templatealias]
3687 3690 > r = rev
3688 3691 > rn = "{r}:{node|short}"
3689 3692 > status(c, files) = files % "{c} {file}\n"
3690 3693 > utcdate(d) = localdate(d, "UTC")
3691 3694 > EOF
3692 3695
3693 3696 $ hg debugtemplate -vr0 '{rn} {utcdate(date)|isodate}\n'
3694 3697 (template
3695 3698 ('symbol', 'rn')
3696 3699 ('string', ' ')
3697 3700 (|
3698 3701 (func
3699 3702 ('symbol', 'utcdate')
3700 3703 ('symbol', 'date'))
3701 3704 ('symbol', 'isodate'))
3702 3705 ('string', '\n'))
3703 3706 * expanded:
3704 3707 (template
3705 3708 (template
3706 3709 ('symbol', 'rev')
3707 3710 ('string', ':')
3708 3711 (|
3709 3712 ('symbol', 'node')
3710 3713 ('symbol', 'short')))
3711 3714 ('string', ' ')
3712 3715 (|
3713 3716 (func
3714 3717 ('symbol', 'localdate')
3715 3718 (list
3716 3719 ('symbol', 'date')
3717 3720 ('string', 'UTC')))
3718 3721 ('symbol', 'isodate'))
3719 3722 ('string', '\n'))
3720 3723 0:1e4e1b8f71e0 1970-01-12 13:46 +0000
3721 3724
3722 3725 $ hg debugtemplate -vr0 '{status("A", file_adds)}'
3723 3726 (template
3724 3727 (func
3725 3728 ('symbol', 'status')
3726 3729 (list
3727 3730 ('string', 'A')
3728 3731 ('symbol', 'file_adds'))))
3729 3732 * expanded:
3730 3733 (template
3731 3734 (%
3732 3735 ('symbol', 'file_adds')
3733 3736 (template
3734 3737 ('string', 'A')
3735 3738 ('string', ' ')
3736 3739 ('symbol', 'file')
3737 3740 ('string', '\n'))))
3738 3741 A a
3739 3742
3740 3743 A unary function alias can be called as a filter:
3741 3744
3742 3745 $ hg debugtemplate -vr0 '{date|utcdate|isodate}\n'
3743 3746 (template
3744 3747 (|
3745 3748 (|
3746 3749 ('symbol', 'date')
3747 3750 ('symbol', 'utcdate'))
3748 3751 ('symbol', 'isodate'))
3749 3752 ('string', '\n'))
3750 3753 * expanded:
3751 3754 (template
3752 3755 (|
3753 3756 (func
3754 3757 ('symbol', 'localdate')
3755 3758 (list
3756 3759 ('symbol', 'date')
3757 3760 ('string', 'UTC')))
3758 3761 ('symbol', 'isodate'))
3759 3762 ('string', '\n'))
3760 3763 1970-01-12 13:46 +0000
3761 3764
3762 3765 Aliases should be applied only to command arguments and templates in hgrc.
3763 3766 Otherwise, our stock styles and web templates could be corrupted:
3764 3767
3765 3768 $ hg log -r0 -T '{rn} {utcdate(date)|isodate}\n'
3766 3769 0:1e4e1b8f71e0 1970-01-12 13:46 +0000
3767 3770
3768 3771 $ hg log -r0 --config ui.logtemplate='"{rn} {utcdate(date)|isodate}\n"'
3769 3772 0:1e4e1b8f71e0 1970-01-12 13:46 +0000
3770 3773
3771 3774 $ cat <<EOF > tmpl
3772 3775 > changeset = 'nothing expanded:{rn}\n'
3773 3776 > EOF
3774 3777 $ hg log -r0 --style ./tmpl
3775 3778 nothing expanded:
3776 3779
3777 3780 Aliases in formatter:
3778 3781
3779 3782 $ hg branches -T '{pad(branch, 7)} {rn}\n'
3780 3783 default 6:d41e714fe50d
3781 3784 foo 4:bbe44766e73d
3782 3785
3783 3786 Aliases should honor HGPLAIN:
3784 3787
3785 3788 $ HGPLAIN= hg log -r0 -T 'nothing expanded:{rn}\n'
3786 3789 nothing expanded:
3787 3790 $ HGPLAINEXCEPT=templatealias hg log -r0 -T '{rn}\n'
3788 3791 0:1e4e1b8f71e0
3789 3792
3790 3793 Unparsable alias:
3791 3794
3792 3795 $ hg debugtemplate --config templatealias.bad='x(' -v '{bad}'
3793 3796 (template
3794 3797 ('symbol', 'bad'))
3795 3798 abort: bad definition of template alias "bad": at 2: not a prefix: end
3796 3799 [255]
3797 3800 $ hg log --config templatealias.bad='x(' -T '{bad}'
3798 3801 abort: bad definition of template alias "bad": at 2: not a prefix: end
3799 3802 [255]
3800 3803
3801 3804 $ cd ..
3802 3805
3803 3806 Set up repository for non-ascii encoding tests:
3804 3807
3805 3808 $ hg init nonascii
3806 3809 $ cd nonascii
3807 3810 $ python <<EOF
3808 3811 > open('latin1', 'w').write('\xe9')
3809 3812 > open('utf-8', 'w').write('\xc3\xa9')
3810 3813 > EOF
3811 3814 $ HGENCODING=utf-8 hg branch -q `cat utf-8`
3812 3815 $ HGENCODING=utf-8 hg ci -qAm "non-ascii branch: `cat utf-8`" utf-8
3813 3816
3814 3817 json filter should try round-trip conversion to utf-8:
3815 3818
3816 3819 $ HGENCODING=ascii hg log -T "{branch|json}\n" -r0
3817 3820 "\u00e9"
3818 3821 $ HGENCODING=ascii hg log -T "{desc|json}\n" -r0
3819 3822 "non-ascii branch: \u00e9"
3820 3823
3821 3824 json filter takes input as utf-8b:
3822 3825
3823 3826 $ HGENCODING=ascii hg log -T "{'`cat utf-8`'|json}\n" -l1
3824 3827 "\u00e9"
3825 3828 $ HGENCODING=ascii hg log -T "{'`cat latin1`'|json}\n" -l1
3826 3829 "\udce9"
3827 3830
3828 3831 utf8 filter:
3829 3832
3830 3833 $ HGENCODING=ascii hg log -T "round-trip: {branch|utf8|hex}\n" -r0
3831 3834 round-trip: c3a9
3832 3835 $ HGENCODING=latin1 hg log -T "decoded: {'`cat latin1`'|utf8|hex}\n" -l1
3833 3836 decoded: c3a9
3834 3837 $ HGENCODING=ascii hg log -T "replaced: {'`cat latin1`'|utf8|hex}\n" -l1
3835 3838 abort: decoding near * (glob)
3836 3839 [255]
3837 3840 $ hg log -T "invalid type: {rev|utf8}\n" -r0
3838 3841 abort: template filter 'utf8' is not compatible with keyword 'rev'
3839 3842 [255]
3840 3843
3841 3844 $ cd ..
3842 3845
3843 3846 Test that template function in extension is registered as expected
3844 3847
3845 3848 $ cd a
3846 3849
3847 3850 $ cat <<EOF > $TESTTMP/customfunc.py
3848 3851 > from mercurial import registrar
3849 3852 >
3850 3853 > templatefunc = registrar.templatefunc()
3851 3854 >
3852 3855 > @templatefunc('custom()')
3853 3856 > def custom(context, mapping, args):
3854 3857 > return 'custom'
3855 3858 > EOF
3856 3859 $ cat <<EOF > .hg/hgrc
3857 3860 > [extensions]
3858 3861 > customfunc = $TESTTMP/customfunc.py
3859 3862 > EOF
3860 3863
3861 3864 $ hg log -r . -T "{custom()}\n" --config customfunc.enabled=true
3862 3865 custom
3863 3866
3864 3867 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now