##// END OF EJS Templates
templater: provide loop counter as "index" keyword...
Yuya Nishihara -
r31807:e6eb86b1 default
parent child Browse files
Show More
@@ -1,3471 +1,3474
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 import itertools
11 12 import os
12 13 import re
13 14 import tempfile
14 15
15 16 from .i18n import _
16 17 from .node import (
17 18 bin,
18 19 hex,
19 20 nullid,
20 21 nullrev,
21 22 short,
22 23 )
23 24
24 25 from . import (
25 26 bookmarks,
26 27 changelog,
27 28 copies,
28 29 crecord as crecordmod,
29 30 encoding,
30 31 error,
31 32 formatter,
32 33 graphmod,
33 34 lock as lockmod,
34 35 match as matchmod,
35 36 obsolete,
36 37 patch,
37 38 pathutil,
38 39 phases,
39 40 pycompat,
40 41 repair,
41 42 revlog,
42 43 revset,
43 44 scmutil,
44 45 smartset,
45 46 templatekw,
46 47 templater,
47 48 util,
48 49 vfs as vfsmod,
49 50 )
50 51 stringio = util.stringio
51 52
52 53 # special string such that everything below this line will be ingored in the
53 54 # editor text
54 55 _linebelow = "^HG: ------------------------ >8 ------------------------$"
55 56
56 57 def ishunk(x):
57 58 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
58 59 return isinstance(x, hunkclasses)
59 60
60 61 def newandmodified(chunks, originalchunks):
61 62 newlyaddedandmodifiedfiles = set()
62 63 for chunk in chunks:
63 64 if ishunk(chunk) and chunk.header.isnewfile() and chunk not in \
64 65 originalchunks:
65 66 newlyaddedandmodifiedfiles.add(chunk.header.filename())
66 67 return newlyaddedandmodifiedfiles
67 68
68 69 def parsealiases(cmd):
69 70 return cmd.lstrip("^").split("|")
70 71
71 72 def setupwrapcolorwrite(ui):
72 73 # wrap ui.write so diff output can be labeled/colorized
73 74 def wrapwrite(orig, *args, **kw):
74 75 label = kw.pop('label', '')
75 76 for chunk, l in patch.difflabel(lambda: args):
76 77 orig(chunk, label=label + l)
77 78
78 79 oldwrite = ui.write
79 80 def wrap(*args, **kwargs):
80 81 return wrapwrite(oldwrite, *args, **kwargs)
81 82 setattr(ui, 'write', wrap)
82 83 return oldwrite
83 84
84 85 def filterchunks(ui, originalhunks, usecurses, testfile, operation=None):
85 86 if usecurses:
86 87 if testfile:
87 88 recordfn = crecordmod.testdecorator(testfile,
88 89 crecordmod.testchunkselector)
89 90 else:
90 91 recordfn = crecordmod.chunkselector
91 92
92 93 return crecordmod.filterpatch(ui, originalhunks, recordfn, operation)
93 94
94 95 else:
95 96 return patch.filterpatch(ui, originalhunks, operation)
96 97
97 98 def recordfilter(ui, originalhunks, operation=None):
98 99 """ Prompts the user to filter the originalhunks and return a list of
99 100 selected hunks.
100 101 *operation* is used for to build ui messages to indicate the user what
101 102 kind of filtering they are doing: reverting, committing, shelving, etc.
102 103 (see patch.filterpatch).
103 104 """
104 105 usecurses = crecordmod.checkcurses(ui)
105 106 testfile = ui.config('experimental', 'crecordtest', None)
106 107 oldwrite = setupwrapcolorwrite(ui)
107 108 try:
108 109 newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
109 110 testfile, operation)
110 111 finally:
111 112 ui.write = oldwrite
112 113 return newchunks, newopts
113 114
114 115 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
115 116 filterfn, *pats, **opts):
116 117 from . import merge as mergemod
117 118 if not ui.interactive():
118 119 if cmdsuggest:
119 120 msg = _('running non-interactively, use %s instead') % cmdsuggest
120 121 else:
121 122 msg = _('running non-interactively')
122 123 raise error.Abort(msg)
123 124
124 125 # make sure username is set before going interactive
125 126 if not opts.get('user'):
126 127 ui.username() # raise exception, username not provided
127 128
128 129 def recordfunc(ui, repo, message, match, opts):
129 130 """This is generic record driver.
130 131
131 132 Its job is to interactively filter local changes, and
132 133 accordingly prepare working directory into a state in which the
133 134 job can be delegated to a non-interactive commit command such as
134 135 'commit' or 'qrefresh'.
135 136
136 137 After the actual job is done by non-interactive command, the
137 138 working directory is restored to its original state.
138 139
139 140 In the end we'll record interesting changes, and everything else
140 141 will be left in place, so the user can continue working.
141 142 """
142 143
143 144 checkunfinished(repo, commit=True)
144 145 wctx = repo[None]
145 146 merge = len(wctx.parents()) > 1
146 147 if merge:
147 148 raise error.Abort(_('cannot partially commit a merge '
148 149 '(use "hg commit" instead)'))
149 150
150 151 def fail(f, msg):
151 152 raise error.Abort('%s: %s' % (f, msg))
152 153
153 154 force = opts.get('force')
154 155 if not force:
155 156 vdirs = []
156 157 match.explicitdir = vdirs.append
157 158 match.bad = fail
158 159
159 160 status = repo.status(match=match)
160 161 if not force:
161 162 repo.checkcommitpatterns(wctx, vdirs, match, status, fail)
162 163 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
163 164 diffopts.nodates = True
164 165 diffopts.git = True
165 166 diffopts.showfunc = True
166 167 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
167 168 originalchunks = patch.parsepatch(originaldiff)
168 169
169 170 # 1. filter patch, since we are intending to apply subset of it
170 171 try:
171 172 chunks, newopts = filterfn(ui, originalchunks)
172 173 except patch.PatchError as err:
173 174 raise error.Abort(_('error parsing patch: %s') % err)
174 175 opts.update(newopts)
175 176
176 177 # We need to keep a backup of files that have been newly added and
177 178 # modified during the recording process because there is a previous
178 179 # version without the edit in the workdir
179 180 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
180 181 contenders = set()
181 182 for h in chunks:
182 183 try:
183 184 contenders.update(set(h.files()))
184 185 except AttributeError:
185 186 pass
186 187
187 188 changed = status.modified + status.added + status.removed
188 189 newfiles = [f for f in changed if f in contenders]
189 190 if not newfiles:
190 191 ui.status(_('no changes to record\n'))
191 192 return 0
192 193
193 194 modified = set(status.modified)
194 195
195 196 # 2. backup changed files, so we can restore them in the end
196 197
197 198 if backupall:
198 199 tobackup = changed
199 200 else:
200 201 tobackup = [f for f in newfiles if f in modified or f in \
201 202 newlyaddedandmodifiedfiles]
202 203 backups = {}
203 204 if tobackup:
204 205 backupdir = repo.vfs.join('record-backups')
205 206 try:
206 207 os.mkdir(backupdir)
207 208 except OSError as err:
208 209 if err.errno != errno.EEXIST:
209 210 raise
210 211 try:
211 212 # backup continues
212 213 for f in tobackup:
213 214 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
214 215 dir=backupdir)
215 216 os.close(fd)
216 217 ui.debug('backup %r as %r\n' % (f, tmpname))
217 218 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
218 219 backups[f] = tmpname
219 220
220 221 fp = stringio()
221 222 for c in chunks:
222 223 fname = c.filename()
223 224 if fname in backups:
224 225 c.write(fp)
225 226 dopatch = fp.tell()
226 227 fp.seek(0)
227 228
228 229 # 2.5 optionally review / modify patch in text editor
229 230 if opts.get('review', False):
230 231 patchtext = (crecordmod.diffhelptext
231 232 + crecordmod.patchhelptext
232 233 + fp.read())
233 234 reviewedpatch = ui.edit(patchtext, "",
234 235 extra={"suffix": ".diff"},
235 236 repopath=repo.path)
236 237 fp.truncate(0)
237 238 fp.write(reviewedpatch)
238 239 fp.seek(0)
239 240
240 241 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
241 242 # 3a. apply filtered patch to clean repo (clean)
242 243 if backups:
243 244 # Equivalent to hg.revert
244 245 m = scmutil.matchfiles(repo, backups.keys())
245 246 mergemod.update(repo, repo.dirstate.p1(),
246 247 False, True, matcher=m)
247 248
248 249 # 3b. (apply)
249 250 if dopatch:
250 251 try:
251 252 ui.debug('applying patch\n')
252 253 ui.debug(fp.getvalue())
253 254 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
254 255 except patch.PatchError as err:
255 256 raise error.Abort(str(err))
256 257 del fp
257 258
258 259 # 4. We prepared working directory according to filtered
259 260 # patch. Now is the time to delegate the job to
260 261 # commit/qrefresh or the like!
261 262
262 263 # Make all of the pathnames absolute.
263 264 newfiles = [repo.wjoin(nf) for nf in newfiles]
264 265 return commitfunc(ui, repo, *newfiles, **opts)
265 266 finally:
266 267 # 5. finally restore backed-up files
267 268 try:
268 269 dirstate = repo.dirstate
269 270 for realname, tmpname in backups.iteritems():
270 271 ui.debug('restoring %r to %r\n' % (tmpname, realname))
271 272
272 273 if dirstate[realname] == 'n':
273 274 # without normallookup, restoring timestamp
274 275 # may cause partially committed files
275 276 # to be treated as unmodified
276 277 dirstate.normallookup(realname)
277 278
278 279 # copystat=True here and above are a hack to trick any
279 280 # editors that have f open that we haven't modified them.
280 281 #
281 282 # Also note that this racy as an editor could notice the
282 283 # file's mtime before we've finished writing it.
283 284 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
284 285 os.unlink(tmpname)
285 286 if tobackup:
286 287 os.rmdir(backupdir)
287 288 except OSError:
288 289 pass
289 290
290 291 def recordinwlock(ui, repo, message, match, opts):
291 292 with repo.wlock():
292 293 return recordfunc(ui, repo, message, match, opts)
293 294
294 295 return commit(ui, repo, recordinwlock, pats, opts)
295 296
296 297 def findpossible(cmd, table, strict=False):
297 298 """
298 299 Return cmd -> (aliases, command table entry)
299 300 for each matching command.
300 301 Return debug commands (or their aliases) only if no normal command matches.
301 302 """
302 303 choice = {}
303 304 debugchoice = {}
304 305
305 306 if cmd in table:
306 307 # short-circuit exact matches, "log" alias beats "^log|history"
307 308 keys = [cmd]
308 309 else:
309 310 keys = table.keys()
310 311
311 312 allcmds = []
312 313 for e in keys:
313 314 aliases = parsealiases(e)
314 315 allcmds.extend(aliases)
315 316 found = None
316 317 if cmd in aliases:
317 318 found = cmd
318 319 elif not strict:
319 320 for a in aliases:
320 321 if a.startswith(cmd):
321 322 found = a
322 323 break
323 324 if found is not None:
324 325 if aliases[0].startswith("debug") or found.startswith("debug"):
325 326 debugchoice[found] = (aliases, table[e])
326 327 else:
327 328 choice[found] = (aliases, table[e])
328 329
329 330 if not choice and debugchoice:
330 331 choice = debugchoice
331 332
332 333 return choice, allcmds
333 334
334 335 def findcmd(cmd, table, strict=True):
335 336 """Return (aliases, command table entry) for command string."""
336 337 choice, allcmds = findpossible(cmd, table, strict)
337 338
338 339 if cmd in choice:
339 340 return choice[cmd]
340 341
341 342 if len(choice) > 1:
342 343 clist = choice.keys()
343 344 clist.sort()
344 345 raise error.AmbiguousCommand(cmd, clist)
345 346
346 347 if choice:
347 348 return choice.values()[0]
348 349
349 350 raise error.UnknownCommand(cmd, allcmds)
350 351
351 352 def findrepo(p):
352 353 while not os.path.isdir(os.path.join(p, ".hg")):
353 354 oldp, p = p, os.path.dirname(p)
354 355 if p == oldp:
355 356 return None
356 357
357 358 return p
358 359
359 360 def bailifchanged(repo, merge=True, hint=None):
360 361 """ enforce the precondition that working directory must be clean.
361 362
362 363 'merge' can be set to false if a pending uncommitted merge should be
363 364 ignored (such as when 'update --check' runs).
364 365
365 366 'hint' is the usual hint given to Abort exception.
366 367 """
367 368
368 369 if merge and repo.dirstate.p2() != nullid:
369 370 raise error.Abort(_('outstanding uncommitted merge'), hint=hint)
370 371 modified, added, removed, deleted = repo.status()[:4]
371 372 if modified or added or removed or deleted:
372 373 raise error.Abort(_('uncommitted changes'), hint=hint)
373 374 ctx = repo[None]
374 375 for s in sorted(ctx.substate):
375 376 ctx.sub(s).bailifchanged(hint=hint)
376 377
377 378 def logmessage(ui, opts):
378 379 """ get the log message according to -m and -l option """
379 380 message = opts.get('message')
380 381 logfile = opts.get('logfile')
381 382
382 383 if message and logfile:
383 384 raise error.Abort(_('options --message and --logfile are mutually '
384 385 'exclusive'))
385 386 if not message and logfile:
386 387 try:
387 388 if logfile == '-':
388 389 message = ui.fin.read()
389 390 else:
390 391 message = '\n'.join(util.readfile(logfile).splitlines())
391 392 except IOError as inst:
392 393 raise error.Abort(_("can't read commit message '%s': %s") %
393 394 (logfile, inst.strerror))
394 395 return message
395 396
396 397 def mergeeditform(ctxorbool, baseformname):
397 398 """return appropriate editform name (referencing a committemplate)
398 399
399 400 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
400 401 merging is committed.
401 402
402 403 This returns baseformname with '.merge' appended if it is a merge,
403 404 otherwise '.normal' is appended.
404 405 """
405 406 if isinstance(ctxorbool, bool):
406 407 if ctxorbool:
407 408 return baseformname + ".merge"
408 409 elif 1 < len(ctxorbool.parents()):
409 410 return baseformname + ".merge"
410 411
411 412 return baseformname + ".normal"
412 413
413 414 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
414 415 editform='', **opts):
415 416 """get appropriate commit message editor according to '--edit' option
416 417
417 418 'finishdesc' is a function to be called with edited commit message
418 419 (= 'description' of the new changeset) just after editing, but
419 420 before checking empty-ness. It should return actual text to be
420 421 stored into history. This allows to change description before
421 422 storing.
422 423
423 424 'extramsg' is a extra message to be shown in the editor instead of
424 425 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
425 426 is automatically added.
426 427
427 428 'editform' is a dot-separated list of names, to distinguish
428 429 the purpose of commit text editing.
429 430
430 431 'getcommiteditor' returns 'commitforceeditor' regardless of
431 432 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
432 433 they are specific for usage in MQ.
433 434 """
434 435 if edit or finishdesc or extramsg:
435 436 return lambda r, c, s: commitforceeditor(r, c, s,
436 437 finishdesc=finishdesc,
437 438 extramsg=extramsg,
438 439 editform=editform)
439 440 elif editform:
440 441 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
441 442 else:
442 443 return commiteditor
443 444
444 445 def loglimit(opts):
445 446 """get the log limit according to option -l/--limit"""
446 447 limit = opts.get('limit')
447 448 if limit:
448 449 try:
449 450 limit = int(limit)
450 451 except ValueError:
451 452 raise error.Abort(_('limit must be a positive integer'))
452 453 if limit <= 0:
453 454 raise error.Abort(_('limit must be positive'))
454 455 else:
455 456 limit = None
456 457 return limit
457 458
458 459 def makefilename(repo, pat, node, desc=None,
459 460 total=None, seqno=None, revwidth=None, pathname=None):
460 461 node_expander = {
461 462 'H': lambda: hex(node),
462 463 'R': lambda: str(repo.changelog.rev(node)),
463 464 'h': lambda: short(node),
464 465 'm': lambda: re.sub('[^\w]', '_', str(desc))
465 466 }
466 467 expander = {
467 468 '%': lambda: '%',
468 469 'b': lambda: os.path.basename(repo.root),
469 470 }
470 471
471 472 try:
472 473 if node:
473 474 expander.update(node_expander)
474 475 if node:
475 476 expander['r'] = (lambda:
476 477 str(repo.changelog.rev(node)).zfill(revwidth or 0))
477 478 if total is not None:
478 479 expander['N'] = lambda: str(total)
479 480 if seqno is not None:
480 481 expander['n'] = lambda: str(seqno)
481 482 if total is not None and seqno is not None:
482 483 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
483 484 if pathname is not None:
484 485 expander['s'] = lambda: os.path.basename(pathname)
485 486 expander['d'] = lambda: os.path.dirname(pathname) or '.'
486 487 expander['p'] = lambda: pathname
487 488
488 489 newname = []
489 490 patlen = len(pat)
490 491 i = 0
491 492 while i < patlen:
492 493 c = pat[i]
493 494 if c == '%':
494 495 i += 1
495 496 c = pat[i]
496 497 c = expander[c]()
497 498 newname.append(c)
498 499 i += 1
499 500 return ''.join(newname)
500 501 except KeyError as inst:
501 502 raise error.Abort(_("invalid format spec '%%%s' in output filename") %
502 503 inst.args[0])
503 504
504 505 class _unclosablefile(object):
505 506 def __init__(self, fp):
506 507 self._fp = fp
507 508
508 509 def close(self):
509 510 pass
510 511
511 512 def __iter__(self):
512 513 return iter(self._fp)
513 514
514 515 def __getattr__(self, attr):
515 516 return getattr(self._fp, attr)
516 517
517 518 def __enter__(self):
518 519 return self
519 520
520 521 def __exit__(self, exc_type, exc_value, exc_tb):
521 522 pass
522 523
523 524 def makefileobj(repo, pat, node=None, desc=None, total=None,
524 525 seqno=None, revwidth=None, mode='wb', modemap=None,
525 526 pathname=None):
526 527
527 528 writable = mode not in ('r', 'rb')
528 529
529 530 if not pat or pat == '-':
530 531 if writable:
531 532 fp = repo.ui.fout
532 533 else:
533 534 fp = repo.ui.fin
534 535 return _unclosablefile(fp)
535 536 if util.safehasattr(pat, 'write') and writable:
536 537 return pat
537 538 if util.safehasattr(pat, 'read') and 'r' in mode:
538 539 return pat
539 540 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
540 541 if modemap is not None:
541 542 mode = modemap.get(fn, mode)
542 543 if mode == 'wb':
543 544 modemap[fn] = 'ab'
544 545 return open(fn, mode)
545 546
546 547 def openrevlog(repo, cmd, file_, opts):
547 548 """opens the changelog, manifest, a filelog or a given revlog"""
548 549 cl = opts['changelog']
549 550 mf = opts['manifest']
550 551 dir = opts['dir']
551 552 msg = None
552 553 if cl and mf:
553 554 msg = _('cannot specify --changelog and --manifest at the same time')
554 555 elif cl and dir:
555 556 msg = _('cannot specify --changelog and --dir at the same time')
556 557 elif cl or mf or dir:
557 558 if file_:
558 559 msg = _('cannot specify filename with --changelog or --manifest')
559 560 elif not repo:
560 561 msg = _('cannot specify --changelog or --manifest or --dir '
561 562 'without a repository')
562 563 if msg:
563 564 raise error.Abort(msg)
564 565
565 566 r = None
566 567 if repo:
567 568 if cl:
568 569 r = repo.unfiltered().changelog
569 570 elif dir:
570 571 if 'treemanifest' not in repo.requirements:
571 572 raise error.Abort(_("--dir can only be used on repos with "
572 573 "treemanifest enabled"))
573 574 dirlog = repo.manifestlog._revlog.dirlog(dir)
574 575 if len(dirlog):
575 576 r = dirlog
576 577 elif mf:
577 578 r = repo.manifestlog._revlog
578 579 elif file_:
579 580 filelog = repo.file(file_)
580 581 if len(filelog):
581 582 r = filelog
582 583 if not r:
583 584 if not file_:
584 585 raise error.CommandError(cmd, _('invalid arguments'))
585 586 if not os.path.isfile(file_):
586 587 raise error.Abort(_("revlog '%s' not found") % file_)
587 588 r = revlog.revlog(vfsmod.vfs(pycompat.getcwd(), audit=False),
588 589 file_[:-2] + ".i")
589 590 return r
590 591
591 592 def copy(ui, repo, pats, opts, rename=False):
592 593 # called with the repo lock held
593 594 #
594 595 # hgsep => pathname that uses "/" to separate directories
595 596 # ossep => pathname that uses os.sep to separate directories
596 597 cwd = repo.getcwd()
597 598 targets = {}
598 599 after = opts.get("after")
599 600 dryrun = opts.get("dry_run")
600 601 wctx = repo[None]
601 602
602 603 def walkpat(pat):
603 604 srcs = []
604 605 if after:
605 606 badstates = '?'
606 607 else:
607 608 badstates = '?r'
608 609 m = scmutil.match(repo[None], [pat], opts, globbed=True)
609 610 for abs in repo.walk(m):
610 611 state = repo.dirstate[abs]
611 612 rel = m.rel(abs)
612 613 exact = m.exact(abs)
613 614 if state in badstates:
614 615 if exact and state == '?':
615 616 ui.warn(_('%s: not copying - file is not managed\n') % rel)
616 617 if exact and state == 'r':
617 618 ui.warn(_('%s: not copying - file has been marked for'
618 619 ' remove\n') % rel)
619 620 continue
620 621 # abs: hgsep
621 622 # rel: ossep
622 623 srcs.append((abs, rel, exact))
623 624 return srcs
624 625
625 626 # abssrc: hgsep
626 627 # relsrc: ossep
627 628 # otarget: ossep
628 629 def copyfile(abssrc, relsrc, otarget, exact):
629 630 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
630 631 if '/' in abstarget:
631 632 # We cannot normalize abstarget itself, this would prevent
632 633 # case only renames, like a => A.
633 634 abspath, absname = abstarget.rsplit('/', 1)
634 635 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
635 636 reltarget = repo.pathto(abstarget, cwd)
636 637 target = repo.wjoin(abstarget)
637 638 src = repo.wjoin(abssrc)
638 639 state = repo.dirstate[abstarget]
639 640
640 641 scmutil.checkportable(ui, abstarget)
641 642
642 643 # check for collisions
643 644 prevsrc = targets.get(abstarget)
644 645 if prevsrc is not None:
645 646 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
646 647 (reltarget, repo.pathto(abssrc, cwd),
647 648 repo.pathto(prevsrc, cwd)))
648 649 return
649 650
650 651 # check for overwrites
651 652 exists = os.path.lexists(target)
652 653 samefile = False
653 654 if exists and abssrc != abstarget:
654 655 if (repo.dirstate.normalize(abssrc) ==
655 656 repo.dirstate.normalize(abstarget)):
656 657 if not rename:
657 658 ui.warn(_("%s: can't copy - same file\n") % reltarget)
658 659 return
659 660 exists = False
660 661 samefile = True
661 662
662 663 if not after and exists or after and state in 'mn':
663 664 if not opts['force']:
664 665 if state in 'mn':
665 666 msg = _('%s: not overwriting - file already committed\n')
666 667 if after:
667 668 flags = '--after --force'
668 669 else:
669 670 flags = '--force'
670 671 if rename:
671 672 hint = _('(hg rename %s to replace the file by '
672 673 'recording a rename)\n') % flags
673 674 else:
674 675 hint = _('(hg copy %s to replace the file by '
675 676 'recording a copy)\n') % flags
676 677 else:
677 678 msg = _('%s: not overwriting - file exists\n')
678 679 if rename:
679 680 hint = _('(hg rename --after to record the rename)\n')
680 681 else:
681 682 hint = _('(hg copy --after to record the copy)\n')
682 683 ui.warn(msg % reltarget)
683 684 ui.warn(hint)
684 685 return
685 686
686 687 if after:
687 688 if not exists:
688 689 if rename:
689 690 ui.warn(_('%s: not recording move - %s does not exist\n') %
690 691 (relsrc, reltarget))
691 692 else:
692 693 ui.warn(_('%s: not recording copy - %s does not exist\n') %
693 694 (relsrc, reltarget))
694 695 return
695 696 elif not dryrun:
696 697 try:
697 698 if exists:
698 699 os.unlink(target)
699 700 targetdir = os.path.dirname(target) or '.'
700 701 if not os.path.isdir(targetdir):
701 702 os.makedirs(targetdir)
702 703 if samefile:
703 704 tmp = target + "~hgrename"
704 705 os.rename(src, tmp)
705 706 os.rename(tmp, target)
706 707 else:
707 708 util.copyfile(src, target)
708 709 srcexists = True
709 710 except IOError as inst:
710 711 if inst.errno == errno.ENOENT:
711 712 ui.warn(_('%s: deleted in working directory\n') % relsrc)
712 713 srcexists = False
713 714 else:
714 715 ui.warn(_('%s: cannot copy - %s\n') %
715 716 (relsrc, inst.strerror))
716 717 return True # report a failure
717 718
718 719 if ui.verbose or not exact:
719 720 if rename:
720 721 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
721 722 else:
722 723 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
723 724
724 725 targets[abstarget] = abssrc
725 726
726 727 # fix up dirstate
727 728 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
728 729 dryrun=dryrun, cwd=cwd)
729 730 if rename and not dryrun:
730 731 if not after and srcexists and not samefile:
731 732 repo.wvfs.unlinkpath(abssrc)
732 733 wctx.forget([abssrc])
733 734
734 735 # pat: ossep
735 736 # dest ossep
736 737 # srcs: list of (hgsep, hgsep, ossep, bool)
737 738 # return: function that takes hgsep and returns ossep
738 739 def targetpathfn(pat, dest, srcs):
739 740 if os.path.isdir(pat):
740 741 abspfx = pathutil.canonpath(repo.root, cwd, pat)
741 742 abspfx = util.localpath(abspfx)
742 743 if destdirexists:
743 744 striplen = len(os.path.split(abspfx)[0])
744 745 else:
745 746 striplen = len(abspfx)
746 747 if striplen:
747 748 striplen += len(pycompat.ossep)
748 749 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
749 750 elif destdirexists:
750 751 res = lambda p: os.path.join(dest,
751 752 os.path.basename(util.localpath(p)))
752 753 else:
753 754 res = lambda p: dest
754 755 return res
755 756
756 757 # pat: ossep
757 758 # dest ossep
758 759 # srcs: list of (hgsep, hgsep, ossep, bool)
759 760 # return: function that takes hgsep and returns ossep
760 761 def targetpathafterfn(pat, dest, srcs):
761 762 if matchmod.patkind(pat):
762 763 # a mercurial pattern
763 764 res = lambda p: os.path.join(dest,
764 765 os.path.basename(util.localpath(p)))
765 766 else:
766 767 abspfx = pathutil.canonpath(repo.root, cwd, pat)
767 768 if len(abspfx) < len(srcs[0][0]):
768 769 # A directory. Either the target path contains the last
769 770 # component of the source path or it does not.
770 771 def evalpath(striplen):
771 772 score = 0
772 773 for s in srcs:
773 774 t = os.path.join(dest, util.localpath(s[0])[striplen:])
774 775 if os.path.lexists(t):
775 776 score += 1
776 777 return score
777 778
778 779 abspfx = util.localpath(abspfx)
779 780 striplen = len(abspfx)
780 781 if striplen:
781 782 striplen += len(pycompat.ossep)
782 783 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
783 784 score = evalpath(striplen)
784 785 striplen1 = len(os.path.split(abspfx)[0])
785 786 if striplen1:
786 787 striplen1 += len(pycompat.ossep)
787 788 if evalpath(striplen1) > score:
788 789 striplen = striplen1
789 790 res = lambda p: os.path.join(dest,
790 791 util.localpath(p)[striplen:])
791 792 else:
792 793 # a file
793 794 if destdirexists:
794 795 res = lambda p: os.path.join(dest,
795 796 os.path.basename(util.localpath(p)))
796 797 else:
797 798 res = lambda p: dest
798 799 return res
799 800
800 801 pats = scmutil.expandpats(pats)
801 802 if not pats:
802 803 raise error.Abort(_('no source or destination specified'))
803 804 if len(pats) == 1:
804 805 raise error.Abort(_('no destination specified'))
805 806 dest = pats.pop()
806 807 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
807 808 if not destdirexists:
808 809 if len(pats) > 1 or matchmod.patkind(pats[0]):
809 810 raise error.Abort(_('with multiple sources, destination must be an '
810 811 'existing directory'))
811 812 if util.endswithsep(dest):
812 813 raise error.Abort(_('destination %s is not a directory') % dest)
813 814
814 815 tfn = targetpathfn
815 816 if after:
816 817 tfn = targetpathafterfn
817 818 copylist = []
818 819 for pat in pats:
819 820 srcs = walkpat(pat)
820 821 if not srcs:
821 822 continue
822 823 copylist.append((tfn(pat, dest, srcs), srcs))
823 824 if not copylist:
824 825 raise error.Abort(_('no files to copy'))
825 826
826 827 errors = 0
827 828 for targetpath, srcs in copylist:
828 829 for abssrc, relsrc, exact in srcs:
829 830 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
830 831 errors += 1
831 832
832 833 if errors:
833 834 ui.warn(_('(consider using --after)\n'))
834 835
835 836 return errors != 0
836 837
837 838 ## facility to let extension process additional data into an import patch
838 839 # list of identifier to be executed in order
839 840 extrapreimport = [] # run before commit
840 841 extrapostimport = [] # run after commit
841 842 # mapping from identifier to actual import function
842 843 #
843 844 # 'preimport' are run before the commit is made and are provided the following
844 845 # arguments:
845 846 # - repo: the localrepository instance,
846 847 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
847 848 # - extra: the future extra dictionary of the changeset, please mutate it,
848 849 # - opts: the import options.
849 850 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
850 851 # mutation of in memory commit and more. Feel free to rework the code to get
851 852 # there.
852 853 extrapreimportmap = {}
853 854 # 'postimport' are run after the commit is made and are provided the following
854 855 # argument:
855 856 # - ctx: the changectx created by import.
856 857 extrapostimportmap = {}
857 858
858 859 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
859 860 """Utility function used by commands.import to import a single patch
860 861
861 862 This function is explicitly defined here to help the evolve extension to
862 863 wrap this part of the import logic.
863 864
864 865 The API is currently a bit ugly because it a simple code translation from
865 866 the import command. Feel free to make it better.
866 867
867 868 :hunk: a patch (as a binary string)
868 869 :parents: nodes that will be parent of the created commit
869 870 :opts: the full dict of option passed to the import command
870 871 :msgs: list to save commit message to.
871 872 (used in case we need to save it when failing)
872 873 :updatefunc: a function that update a repo to a given node
873 874 updatefunc(<repo>, <node>)
874 875 """
875 876 # avoid cycle context -> subrepo -> cmdutil
876 877 from . import context
877 878 extractdata = patch.extract(ui, hunk)
878 879 tmpname = extractdata.get('filename')
879 880 message = extractdata.get('message')
880 881 user = opts.get('user') or extractdata.get('user')
881 882 date = opts.get('date') or extractdata.get('date')
882 883 branch = extractdata.get('branch')
883 884 nodeid = extractdata.get('nodeid')
884 885 p1 = extractdata.get('p1')
885 886 p2 = extractdata.get('p2')
886 887
887 888 nocommit = opts.get('no_commit')
888 889 importbranch = opts.get('import_branch')
889 890 update = not opts.get('bypass')
890 891 strip = opts["strip"]
891 892 prefix = opts["prefix"]
892 893 sim = float(opts.get('similarity') or 0)
893 894 if not tmpname:
894 895 return (None, None, False)
895 896
896 897 rejects = False
897 898
898 899 try:
899 900 cmdline_message = logmessage(ui, opts)
900 901 if cmdline_message:
901 902 # pickup the cmdline msg
902 903 message = cmdline_message
903 904 elif message:
904 905 # pickup the patch msg
905 906 message = message.strip()
906 907 else:
907 908 # launch the editor
908 909 message = None
909 910 ui.debug('message:\n%s\n' % message)
910 911
911 912 if len(parents) == 1:
912 913 parents.append(repo[nullid])
913 914 if opts.get('exact'):
914 915 if not nodeid or not p1:
915 916 raise error.Abort(_('not a Mercurial patch'))
916 917 p1 = repo[p1]
917 918 p2 = repo[p2 or nullid]
918 919 elif p2:
919 920 try:
920 921 p1 = repo[p1]
921 922 p2 = repo[p2]
922 923 # Without any options, consider p2 only if the
923 924 # patch is being applied on top of the recorded
924 925 # first parent.
925 926 if p1 != parents[0]:
926 927 p1 = parents[0]
927 928 p2 = repo[nullid]
928 929 except error.RepoError:
929 930 p1, p2 = parents
930 931 if p2.node() == nullid:
931 932 ui.warn(_("warning: import the patch as a normal revision\n"
932 933 "(use --exact to import the patch as a merge)\n"))
933 934 else:
934 935 p1, p2 = parents
935 936
936 937 n = None
937 938 if update:
938 939 if p1 != parents[0]:
939 940 updatefunc(repo, p1.node())
940 941 if p2 != parents[1]:
941 942 repo.setparents(p1.node(), p2.node())
942 943
943 944 if opts.get('exact') or importbranch:
944 945 repo.dirstate.setbranch(branch or 'default')
945 946
946 947 partial = opts.get('partial', False)
947 948 files = set()
948 949 try:
949 950 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
950 951 files=files, eolmode=None, similarity=sim / 100.0)
951 952 except patch.PatchError as e:
952 953 if not partial:
953 954 raise error.Abort(str(e))
954 955 if partial:
955 956 rejects = True
956 957
957 958 files = list(files)
958 959 if nocommit:
959 960 if message:
960 961 msgs.append(message)
961 962 else:
962 963 if opts.get('exact') or p2:
963 964 # If you got here, you either use --force and know what
964 965 # you are doing or used --exact or a merge patch while
965 966 # being updated to its first parent.
966 967 m = None
967 968 else:
968 969 m = scmutil.matchfiles(repo, files or [])
969 970 editform = mergeeditform(repo[None], 'import.normal')
970 971 if opts.get('exact'):
971 972 editor = None
972 973 else:
973 974 editor = getcommiteditor(editform=editform, **opts)
974 975 extra = {}
975 976 for idfunc in extrapreimport:
976 977 extrapreimportmap[idfunc](repo, extractdata, extra, opts)
977 978 overrides = {}
978 979 if partial:
979 980 overrides[('ui', 'allowemptycommit')] = True
980 981 with repo.ui.configoverride(overrides, 'import'):
981 982 n = repo.commit(message, user,
982 983 date, match=m,
983 984 editor=editor, extra=extra)
984 985 for idfunc in extrapostimport:
985 986 extrapostimportmap[idfunc](repo[n])
986 987 else:
987 988 if opts.get('exact') or importbranch:
988 989 branch = branch or 'default'
989 990 else:
990 991 branch = p1.branch()
991 992 store = patch.filestore()
992 993 try:
993 994 files = set()
994 995 try:
995 996 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
996 997 files, eolmode=None)
997 998 except patch.PatchError as e:
998 999 raise error.Abort(str(e))
999 1000 if opts.get('exact'):
1000 1001 editor = None
1001 1002 else:
1002 1003 editor = getcommiteditor(editform='import.bypass')
1003 1004 memctx = context.makememctx(repo, (p1.node(), p2.node()),
1004 1005 message,
1005 1006 user,
1006 1007 date,
1007 1008 branch, files, store,
1008 1009 editor=editor)
1009 1010 n = memctx.commit()
1010 1011 finally:
1011 1012 store.close()
1012 1013 if opts.get('exact') and nocommit:
1013 1014 # --exact with --no-commit is still useful in that it does merge
1014 1015 # and branch bits
1015 1016 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1016 1017 elif opts.get('exact') and hex(n) != nodeid:
1017 1018 raise error.Abort(_('patch is damaged or loses information'))
1018 1019 msg = _('applied to working directory')
1019 1020 if n:
1020 1021 # i18n: refers to a short changeset id
1021 1022 msg = _('created %s') % short(n)
1022 1023 return (msg, n, rejects)
1023 1024 finally:
1024 1025 os.unlink(tmpname)
1025 1026
1026 1027 # facility to let extensions include additional data in an exported patch
1027 1028 # list of identifiers to be executed in order
1028 1029 extraexport = []
1029 1030 # mapping from identifier to actual export function
1030 1031 # function as to return a string to be added to the header or None
1031 1032 # it is given two arguments (sequencenumber, changectx)
1032 1033 extraexportmap = {}
1033 1034
1034 1035 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1035 1036 opts=None, match=None):
1036 1037 '''export changesets as hg patches.'''
1037 1038
1038 1039 total = len(revs)
1039 1040 revwidth = max([len(str(rev)) for rev in revs])
1040 1041 filemode = {}
1041 1042
1042 1043 def single(rev, seqno, fp):
1043 1044 ctx = repo[rev]
1044 1045 node = ctx.node()
1045 1046 parents = [p.node() for p in ctx.parents() if p]
1046 1047 branch = ctx.branch()
1047 1048 if switch_parent:
1048 1049 parents.reverse()
1049 1050
1050 1051 if parents:
1051 1052 prev = parents[0]
1052 1053 else:
1053 1054 prev = nullid
1054 1055
1055 1056 shouldclose = False
1056 1057 if not fp and len(template) > 0:
1057 1058 desc_lines = ctx.description().rstrip().split('\n')
1058 1059 desc = desc_lines[0] #Commit always has a first line.
1059 1060 fp = makefileobj(repo, template, node, desc=desc, total=total,
1060 1061 seqno=seqno, revwidth=revwidth, mode='wb',
1061 1062 modemap=filemode)
1062 1063 shouldclose = True
1063 1064 if fp and not getattr(fp, 'name', '<unnamed>').startswith('<'):
1064 1065 repo.ui.note("%s\n" % fp.name)
1065 1066
1066 1067 if not fp:
1067 1068 write = repo.ui.write
1068 1069 else:
1069 1070 def write(s, **kw):
1070 1071 fp.write(s)
1071 1072
1072 1073 write("# HG changeset patch\n")
1073 1074 write("# User %s\n" % ctx.user())
1074 1075 write("# Date %d %d\n" % ctx.date())
1075 1076 write("# %s\n" % util.datestr(ctx.date()))
1076 1077 if branch and branch != 'default':
1077 1078 write("# Branch %s\n" % branch)
1078 1079 write("# Node ID %s\n" % hex(node))
1079 1080 write("# Parent %s\n" % hex(prev))
1080 1081 if len(parents) > 1:
1081 1082 write("# Parent %s\n" % hex(parents[1]))
1082 1083
1083 1084 for headerid in extraexport:
1084 1085 header = extraexportmap[headerid](seqno, ctx)
1085 1086 if header is not None:
1086 1087 write('# %s\n' % header)
1087 1088 write(ctx.description().rstrip())
1088 1089 write("\n\n")
1089 1090
1090 1091 for chunk, label in patch.diffui(repo, prev, node, match, opts=opts):
1091 1092 write(chunk, label=label)
1092 1093
1093 1094 if shouldclose:
1094 1095 fp.close()
1095 1096
1096 1097 for seqno, rev in enumerate(revs):
1097 1098 single(rev, seqno + 1, fp)
1098 1099
1099 1100 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
1100 1101 changes=None, stat=False, fp=None, prefix='',
1101 1102 root='', listsubrepos=False):
1102 1103 '''show diff or diffstat.'''
1103 1104 if fp is None:
1104 1105 write = ui.write
1105 1106 else:
1106 1107 def write(s, **kw):
1107 1108 fp.write(s)
1108 1109
1109 1110 if root:
1110 1111 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
1111 1112 else:
1112 1113 relroot = ''
1113 1114 if relroot != '':
1114 1115 # XXX relative roots currently don't work if the root is within a
1115 1116 # subrepo
1116 1117 uirelroot = match.uipath(relroot)
1117 1118 relroot += '/'
1118 1119 for matchroot in match.files():
1119 1120 if not matchroot.startswith(relroot):
1120 1121 ui.warn(_('warning: %s not inside relative root %s\n') % (
1121 1122 match.uipath(matchroot), uirelroot))
1122 1123
1123 1124 if stat:
1124 1125 diffopts = diffopts.copy(context=0)
1125 1126 width = 80
1126 1127 if not ui.plain():
1127 1128 width = ui.termwidth()
1128 1129 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
1129 1130 prefix=prefix, relroot=relroot)
1130 1131 for chunk, label in patch.diffstatui(util.iterlines(chunks),
1131 1132 width=width):
1132 1133 write(chunk, label=label)
1133 1134 else:
1134 1135 for chunk, label in patch.diffui(repo, node1, node2, match,
1135 1136 changes, diffopts, prefix=prefix,
1136 1137 relroot=relroot):
1137 1138 write(chunk, label=label)
1138 1139
1139 1140 if listsubrepos:
1140 1141 ctx1 = repo[node1]
1141 1142 ctx2 = repo[node2]
1142 1143 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
1143 1144 tempnode2 = node2
1144 1145 try:
1145 1146 if node2 is not None:
1146 1147 tempnode2 = ctx2.substate[subpath][1]
1147 1148 except KeyError:
1148 1149 # A subrepo that existed in node1 was deleted between node1 and
1149 1150 # node2 (inclusive). Thus, ctx2's substate won't contain that
1150 1151 # subpath. The best we can do is to ignore it.
1151 1152 tempnode2 = None
1152 1153 submatch = matchmod.subdirmatcher(subpath, match)
1153 1154 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
1154 1155 stat=stat, fp=fp, prefix=prefix)
1155 1156
1156 1157 def _changesetlabels(ctx):
1157 1158 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
1158 1159 if ctx.obsolete():
1159 1160 labels.append('changeset.obsolete')
1160 1161 if ctx.troubled():
1161 1162 labels.append('changeset.troubled')
1162 1163 for trouble in ctx.troubles():
1163 1164 labels.append('trouble.%s' % trouble)
1164 1165 return ' '.join(labels)
1165 1166
1166 1167 class changeset_printer(object):
1167 1168 '''show changeset information when templating not requested.'''
1168 1169
1169 1170 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1170 1171 self.ui = ui
1171 1172 self.repo = repo
1172 1173 self.buffered = buffered
1173 1174 self.matchfn = matchfn
1174 1175 self.diffopts = diffopts
1175 1176 self.header = {}
1176 1177 self.hunk = {}
1177 1178 self.lastheader = None
1178 1179 self.footer = None
1179 1180
1180 1181 def flush(self, ctx):
1181 1182 rev = ctx.rev()
1182 1183 if rev in self.header:
1183 1184 h = self.header[rev]
1184 1185 if h != self.lastheader:
1185 1186 self.lastheader = h
1186 1187 self.ui.write(h)
1187 1188 del self.header[rev]
1188 1189 if rev in self.hunk:
1189 1190 self.ui.write(self.hunk[rev])
1190 1191 del self.hunk[rev]
1191 1192 return 1
1192 1193 return 0
1193 1194
1194 1195 def close(self):
1195 1196 if self.footer:
1196 1197 self.ui.write(self.footer)
1197 1198
1198 1199 def show(self, ctx, copies=None, matchfn=None, **props):
1199 1200 if self.buffered:
1200 1201 self.ui.pushbuffer(labeled=True)
1201 1202 self._show(ctx, copies, matchfn, props)
1202 1203 self.hunk[ctx.rev()] = self.ui.popbuffer()
1203 1204 else:
1204 1205 self._show(ctx, copies, matchfn, props)
1205 1206
1206 1207 def _show(self, ctx, copies, matchfn, props):
1207 1208 '''show a single changeset or file revision'''
1208 1209 changenode = ctx.node()
1209 1210 rev = ctx.rev()
1210 1211 if self.ui.debugflag:
1211 1212 hexfunc = hex
1212 1213 else:
1213 1214 hexfunc = short
1214 1215 # as of now, wctx.node() and wctx.rev() return None, but we want to
1215 1216 # show the same values as {node} and {rev} templatekw
1216 1217 revnode = (scmutil.intrev(rev), hexfunc(bin(ctx.hex())))
1217 1218
1218 1219 if self.ui.quiet:
1219 1220 self.ui.write("%d:%s\n" % revnode, label='log.node')
1220 1221 return
1221 1222
1222 1223 date = util.datestr(ctx.date())
1223 1224
1224 1225 # i18n: column positioning for "hg log"
1225 1226 self.ui.write(_("changeset: %d:%s\n") % revnode,
1226 1227 label=_changesetlabels(ctx))
1227 1228
1228 1229 # branches are shown first before any other names due to backwards
1229 1230 # compatibility
1230 1231 branch = ctx.branch()
1231 1232 # don't show the default branch name
1232 1233 if branch != 'default':
1233 1234 # i18n: column positioning for "hg log"
1234 1235 self.ui.write(_("branch: %s\n") % branch,
1235 1236 label='log.branch')
1236 1237
1237 1238 for nsname, ns in self.repo.names.iteritems():
1238 1239 # branches has special logic already handled above, so here we just
1239 1240 # skip it
1240 1241 if nsname == 'branches':
1241 1242 continue
1242 1243 # we will use the templatename as the color name since those two
1243 1244 # should be the same
1244 1245 for name in ns.names(self.repo, changenode):
1245 1246 self.ui.write(ns.logfmt % name,
1246 1247 label='log.%s' % ns.colorname)
1247 1248 if self.ui.debugflag:
1248 1249 # i18n: column positioning for "hg log"
1249 1250 self.ui.write(_("phase: %s\n") % ctx.phasestr(),
1250 1251 label='log.phase')
1251 1252 for pctx in scmutil.meaningfulparents(self.repo, ctx):
1252 1253 label = 'log.parent changeset.%s' % pctx.phasestr()
1253 1254 # i18n: column positioning for "hg log"
1254 1255 self.ui.write(_("parent: %d:%s\n")
1255 1256 % (pctx.rev(), hexfunc(pctx.node())),
1256 1257 label=label)
1257 1258
1258 1259 if self.ui.debugflag and rev is not None:
1259 1260 mnode = ctx.manifestnode()
1260 1261 # i18n: column positioning for "hg log"
1261 1262 self.ui.write(_("manifest: %d:%s\n") %
1262 1263 (self.repo.manifestlog._revlog.rev(mnode),
1263 1264 hex(mnode)),
1264 1265 label='ui.debug log.manifest')
1265 1266 # i18n: column positioning for "hg log"
1266 1267 self.ui.write(_("user: %s\n") % ctx.user(),
1267 1268 label='log.user')
1268 1269 # i18n: column positioning for "hg log"
1269 1270 self.ui.write(_("date: %s\n") % date,
1270 1271 label='log.date')
1271 1272
1272 1273 if ctx.troubled():
1273 1274 # i18n: column positioning for "hg log"
1274 1275 self.ui.write(_("trouble: %s\n") % ', '.join(ctx.troubles()),
1275 1276 label='log.trouble')
1276 1277
1277 1278 if self.ui.debugflag:
1278 1279 files = ctx.p1().status(ctx)[:3]
1279 1280 for key, value in zip([# i18n: column positioning for "hg log"
1280 1281 _("files:"),
1281 1282 # i18n: column positioning for "hg log"
1282 1283 _("files+:"),
1283 1284 # i18n: column positioning for "hg log"
1284 1285 _("files-:")], files):
1285 1286 if value:
1286 1287 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
1287 1288 label='ui.debug log.files')
1288 1289 elif ctx.files() and self.ui.verbose:
1289 1290 # i18n: column positioning for "hg log"
1290 1291 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
1291 1292 label='ui.note log.files')
1292 1293 if copies and self.ui.verbose:
1293 1294 copies = ['%s (%s)' % c for c in copies]
1294 1295 # i18n: column positioning for "hg log"
1295 1296 self.ui.write(_("copies: %s\n") % ' '.join(copies),
1296 1297 label='ui.note log.copies')
1297 1298
1298 1299 extra = ctx.extra()
1299 1300 if extra and self.ui.debugflag:
1300 1301 for key, value in sorted(extra.items()):
1301 1302 # i18n: column positioning for "hg log"
1302 1303 self.ui.write(_("extra: %s=%s\n")
1303 1304 % (key, util.escapestr(value)),
1304 1305 label='ui.debug log.extra')
1305 1306
1306 1307 description = ctx.description().strip()
1307 1308 if description:
1308 1309 if self.ui.verbose:
1309 1310 self.ui.write(_("description:\n"),
1310 1311 label='ui.note log.description')
1311 1312 self.ui.write(description,
1312 1313 label='ui.note log.description')
1313 1314 self.ui.write("\n\n")
1314 1315 else:
1315 1316 # i18n: column positioning for "hg log"
1316 1317 self.ui.write(_("summary: %s\n") %
1317 1318 description.splitlines()[0],
1318 1319 label='log.summary')
1319 1320 self.ui.write("\n")
1320 1321
1321 1322 self.showpatch(ctx, matchfn)
1322 1323
1323 1324 def showpatch(self, ctx, matchfn):
1324 1325 if not matchfn:
1325 1326 matchfn = self.matchfn
1326 1327 if matchfn:
1327 1328 stat = self.diffopts.get('stat')
1328 1329 diff = self.diffopts.get('patch')
1329 1330 diffopts = patch.diffallopts(self.ui, self.diffopts)
1330 1331 node = ctx.node()
1331 1332 prev = ctx.p1().node()
1332 1333 if stat:
1333 1334 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1334 1335 match=matchfn, stat=True)
1335 1336 if diff:
1336 1337 if stat:
1337 1338 self.ui.write("\n")
1338 1339 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1339 1340 match=matchfn, stat=False)
1340 1341 self.ui.write("\n")
1341 1342
1342 1343 class jsonchangeset(changeset_printer):
1343 1344 '''format changeset information.'''
1344 1345
1345 1346 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1346 1347 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1347 1348 self.cache = {}
1348 1349 self._first = True
1349 1350
1350 1351 def close(self):
1351 1352 if not self._first:
1352 1353 self.ui.write("\n]\n")
1353 1354 else:
1354 1355 self.ui.write("[]\n")
1355 1356
1356 1357 def _show(self, ctx, copies, matchfn, props):
1357 1358 '''show a single changeset or file revision'''
1358 1359 rev = ctx.rev()
1359 1360 if rev is None:
1360 1361 jrev = jnode = 'null'
1361 1362 else:
1362 1363 jrev = str(rev)
1363 1364 jnode = '"%s"' % hex(ctx.node())
1364 1365 j = encoding.jsonescape
1365 1366
1366 1367 if self._first:
1367 1368 self.ui.write("[\n {")
1368 1369 self._first = False
1369 1370 else:
1370 1371 self.ui.write(",\n {")
1371 1372
1372 1373 if self.ui.quiet:
1373 1374 self.ui.write(('\n "rev": %s') % jrev)
1374 1375 self.ui.write((',\n "node": %s') % jnode)
1375 1376 self.ui.write('\n }')
1376 1377 return
1377 1378
1378 1379 self.ui.write(('\n "rev": %s') % jrev)
1379 1380 self.ui.write((',\n "node": %s') % jnode)
1380 1381 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
1381 1382 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
1382 1383 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
1383 1384 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
1384 1385 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
1385 1386
1386 1387 self.ui.write((',\n "bookmarks": [%s]') %
1387 1388 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1388 1389 self.ui.write((',\n "tags": [%s]') %
1389 1390 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1390 1391 self.ui.write((',\n "parents": [%s]') %
1391 1392 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1392 1393
1393 1394 if self.ui.debugflag:
1394 1395 if rev is None:
1395 1396 jmanifestnode = 'null'
1396 1397 else:
1397 1398 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
1398 1399 self.ui.write((',\n "manifest": %s') % jmanifestnode)
1399 1400
1400 1401 self.ui.write((',\n "extra": {%s}') %
1401 1402 ", ".join('"%s": "%s"' % (j(k), j(v))
1402 1403 for k, v in ctx.extra().items()))
1403 1404
1404 1405 files = ctx.p1().status(ctx)
1405 1406 self.ui.write((',\n "modified": [%s]') %
1406 1407 ", ".join('"%s"' % j(f) for f in files[0]))
1407 1408 self.ui.write((',\n "added": [%s]') %
1408 1409 ", ".join('"%s"' % j(f) for f in files[1]))
1409 1410 self.ui.write((',\n "removed": [%s]') %
1410 1411 ", ".join('"%s"' % j(f) for f in files[2]))
1411 1412
1412 1413 elif self.ui.verbose:
1413 1414 self.ui.write((',\n "files": [%s]') %
1414 1415 ", ".join('"%s"' % j(f) for f in ctx.files()))
1415 1416
1416 1417 if copies:
1417 1418 self.ui.write((',\n "copies": {%s}') %
1418 1419 ", ".join('"%s": "%s"' % (j(k), j(v))
1419 1420 for k, v in copies))
1420 1421
1421 1422 matchfn = self.matchfn
1422 1423 if matchfn:
1423 1424 stat = self.diffopts.get('stat')
1424 1425 diff = self.diffopts.get('patch')
1425 1426 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1426 1427 node, prev = ctx.node(), ctx.p1().node()
1427 1428 if stat:
1428 1429 self.ui.pushbuffer()
1429 1430 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1430 1431 match=matchfn, stat=True)
1431 1432 self.ui.write((',\n "diffstat": "%s"')
1432 1433 % j(self.ui.popbuffer()))
1433 1434 if diff:
1434 1435 self.ui.pushbuffer()
1435 1436 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1436 1437 match=matchfn, stat=False)
1437 1438 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
1438 1439
1439 1440 self.ui.write("\n }")
1440 1441
1441 1442 class changeset_templater(changeset_printer):
1442 1443 '''format changeset information.'''
1443 1444
1444 1445 def __init__(self, ui, repo, matchfn, diffopts, tmpl, mapfile, buffered):
1445 1446 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1446 1447 assert not (tmpl and mapfile)
1447 1448 defaulttempl = templatekw.defaulttempl
1448 1449 if mapfile:
1449 1450 self.t = templater.templater.frommapfile(mapfile,
1450 1451 cache=defaulttempl)
1451 1452 else:
1452 1453 self.t = formatter.maketemplater(ui, 'changeset', tmpl,
1453 1454 cache=defaulttempl)
1454 1455
1456 self._counter = itertools.count()
1455 1457 self.cache = {}
1456 1458
1457 1459 # find correct templates for current mode
1458 1460 tmplmodes = [
1459 1461 (True, None),
1460 1462 (self.ui.verbose, 'verbose'),
1461 1463 (self.ui.quiet, 'quiet'),
1462 1464 (self.ui.debugflag, 'debug'),
1463 1465 ]
1464 1466
1465 1467 self._parts = {'header': '', 'footer': '', 'changeset': 'changeset',
1466 1468 'docheader': '', 'docfooter': ''}
1467 1469 for mode, postfix in tmplmodes:
1468 1470 for t in self._parts:
1469 1471 cur = t
1470 1472 if postfix:
1471 1473 cur += "_" + postfix
1472 1474 if mode and cur in self.t:
1473 1475 self._parts[t] = cur
1474 1476
1475 1477 if self._parts['docheader']:
1476 1478 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
1477 1479
1478 1480 def close(self):
1479 1481 if self._parts['docfooter']:
1480 1482 if not self.footer:
1481 1483 self.footer = ""
1482 1484 self.footer += templater.stringify(self.t(self._parts['docfooter']))
1483 1485 return super(changeset_templater, self).close()
1484 1486
1485 1487 def _show(self, ctx, copies, matchfn, props):
1486 1488 '''show a single changeset or file revision'''
1487 1489 props = props.copy()
1488 1490 props.update(templatekw.keywords)
1489 1491 props['templ'] = self.t
1490 1492 props['ctx'] = ctx
1491 1493 props['repo'] = self.repo
1492 1494 props['ui'] = self.repo.ui
1495 props['index'] = next(self._counter)
1493 1496 props['revcache'] = {'copies': copies}
1494 1497 props['cache'] = self.cache
1495 1498
1496 1499 # write header
1497 1500 if self._parts['header']:
1498 1501 h = templater.stringify(self.t(self._parts['header'], **props))
1499 1502 if self.buffered:
1500 1503 self.header[ctx.rev()] = h
1501 1504 else:
1502 1505 if self.lastheader != h:
1503 1506 self.lastheader = h
1504 1507 self.ui.write(h)
1505 1508
1506 1509 # write changeset metadata, then patch if requested
1507 1510 key = self._parts['changeset']
1508 1511 self.ui.write(templater.stringify(self.t(key, **props)))
1509 1512 self.showpatch(ctx, matchfn)
1510 1513
1511 1514 if self._parts['footer']:
1512 1515 if not self.footer:
1513 1516 self.footer = templater.stringify(
1514 1517 self.t(self._parts['footer'], **props))
1515 1518
1516 1519 def gettemplate(ui, tmpl, style):
1517 1520 """
1518 1521 Find the template matching the given template spec or style.
1519 1522 """
1520 1523
1521 1524 # ui settings
1522 1525 if not tmpl and not style: # template are stronger than style
1523 1526 tmpl = ui.config('ui', 'logtemplate')
1524 1527 if tmpl:
1525 1528 return templater.unquotestring(tmpl), None
1526 1529 else:
1527 1530 style = util.expandpath(ui.config('ui', 'style', ''))
1528 1531
1529 1532 if not tmpl and style:
1530 1533 mapfile = style
1531 1534 if not os.path.split(mapfile)[0]:
1532 1535 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1533 1536 or templater.templatepath(mapfile))
1534 1537 if mapname:
1535 1538 mapfile = mapname
1536 1539 return None, mapfile
1537 1540
1538 1541 if not tmpl:
1539 1542 return None, None
1540 1543
1541 1544 return formatter.lookuptemplate(ui, 'changeset', tmpl)
1542 1545
1543 1546 def show_changeset(ui, repo, opts, buffered=False):
1544 1547 """show one changeset using template or regular display.
1545 1548
1546 1549 Display format will be the first non-empty hit of:
1547 1550 1. option 'template'
1548 1551 2. option 'style'
1549 1552 3. [ui] setting 'logtemplate'
1550 1553 4. [ui] setting 'style'
1551 1554 If all of these values are either the unset or the empty string,
1552 1555 regular display via changeset_printer() is done.
1553 1556 """
1554 1557 # options
1555 1558 matchfn = None
1556 1559 if opts.get('patch') or opts.get('stat'):
1557 1560 matchfn = scmutil.matchall(repo)
1558 1561
1559 1562 if opts.get('template') == 'json':
1560 1563 return jsonchangeset(ui, repo, matchfn, opts, buffered)
1561 1564
1562 1565 tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))
1563 1566
1564 1567 if not tmpl and not mapfile:
1565 1568 return changeset_printer(ui, repo, matchfn, opts, buffered)
1566 1569
1567 1570 return changeset_templater(ui, repo, matchfn, opts, tmpl, mapfile, buffered)
1568 1571
1569 1572 def showmarker(fm, marker, index=None):
1570 1573 """utility function to display obsolescence marker in a readable way
1571 1574
1572 1575 To be used by debug function."""
1573 1576 if index is not None:
1574 1577 fm.write('index', '%i ', index)
1575 1578 fm.write('precnode', '%s ', hex(marker.precnode()))
1576 1579 succs = marker.succnodes()
1577 1580 fm.condwrite(succs, 'succnodes', '%s ',
1578 1581 fm.formatlist(map(hex, succs), name='node'))
1579 1582 fm.write('flag', '%X ', marker.flags())
1580 1583 parents = marker.parentnodes()
1581 1584 if parents is not None:
1582 1585 fm.write('parentnodes', '{%s} ',
1583 1586 fm.formatlist(map(hex, parents), name='node', sep=', '))
1584 1587 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
1585 1588 meta = marker.metadata().copy()
1586 1589 meta.pop('date', None)
1587 1590 fm.write('metadata', '{%s}', fm.formatdict(meta, fmt='%r: %r', sep=', '))
1588 1591 fm.plain('\n')
1589 1592
1590 1593 def finddate(ui, repo, date):
1591 1594 """Find the tipmost changeset that matches the given date spec"""
1592 1595
1593 1596 df = util.matchdate(date)
1594 1597 m = scmutil.matchall(repo)
1595 1598 results = {}
1596 1599
1597 1600 def prep(ctx, fns):
1598 1601 d = ctx.date()
1599 1602 if df(d[0]):
1600 1603 results[ctx.rev()] = d
1601 1604
1602 1605 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1603 1606 rev = ctx.rev()
1604 1607 if rev in results:
1605 1608 ui.status(_("found revision %s from %s\n") %
1606 1609 (rev, util.datestr(results[rev])))
1607 1610 return str(rev)
1608 1611
1609 1612 raise error.Abort(_("revision matching date not found"))
1610 1613
1611 1614 def increasingwindows(windowsize=8, sizelimit=512):
1612 1615 while True:
1613 1616 yield windowsize
1614 1617 if windowsize < sizelimit:
1615 1618 windowsize *= 2
1616 1619
1617 1620 class FileWalkError(Exception):
1618 1621 pass
1619 1622
1620 1623 def walkfilerevs(repo, match, follow, revs, fncache):
1621 1624 '''Walks the file history for the matched files.
1622 1625
1623 1626 Returns the changeset revs that are involved in the file history.
1624 1627
1625 1628 Throws FileWalkError if the file history can't be walked using
1626 1629 filelogs alone.
1627 1630 '''
1628 1631 wanted = set()
1629 1632 copies = []
1630 1633 minrev, maxrev = min(revs), max(revs)
1631 1634 def filerevgen(filelog, last):
1632 1635 """
1633 1636 Only files, no patterns. Check the history of each file.
1634 1637
1635 1638 Examines filelog entries within minrev, maxrev linkrev range
1636 1639 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1637 1640 tuples in backwards order
1638 1641 """
1639 1642 cl_count = len(repo)
1640 1643 revs = []
1641 1644 for j in xrange(0, last + 1):
1642 1645 linkrev = filelog.linkrev(j)
1643 1646 if linkrev < minrev:
1644 1647 continue
1645 1648 # only yield rev for which we have the changelog, it can
1646 1649 # happen while doing "hg log" during a pull or commit
1647 1650 if linkrev >= cl_count:
1648 1651 break
1649 1652
1650 1653 parentlinkrevs = []
1651 1654 for p in filelog.parentrevs(j):
1652 1655 if p != nullrev:
1653 1656 parentlinkrevs.append(filelog.linkrev(p))
1654 1657 n = filelog.node(j)
1655 1658 revs.append((linkrev, parentlinkrevs,
1656 1659 follow and filelog.renamed(n)))
1657 1660
1658 1661 return reversed(revs)
1659 1662 def iterfiles():
1660 1663 pctx = repo['.']
1661 1664 for filename in match.files():
1662 1665 if follow:
1663 1666 if filename not in pctx:
1664 1667 raise error.Abort(_('cannot follow file not in parent '
1665 1668 'revision: "%s"') % filename)
1666 1669 yield filename, pctx[filename].filenode()
1667 1670 else:
1668 1671 yield filename, None
1669 1672 for filename_node in copies:
1670 1673 yield filename_node
1671 1674
1672 1675 for file_, node in iterfiles():
1673 1676 filelog = repo.file(file_)
1674 1677 if not len(filelog):
1675 1678 if node is None:
1676 1679 # A zero count may be a directory or deleted file, so
1677 1680 # try to find matching entries on the slow path.
1678 1681 if follow:
1679 1682 raise error.Abort(
1680 1683 _('cannot follow nonexistent file: "%s"') % file_)
1681 1684 raise FileWalkError("Cannot walk via filelog")
1682 1685 else:
1683 1686 continue
1684 1687
1685 1688 if node is None:
1686 1689 last = len(filelog) - 1
1687 1690 else:
1688 1691 last = filelog.rev(node)
1689 1692
1690 1693 # keep track of all ancestors of the file
1691 1694 ancestors = set([filelog.linkrev(last)])
1692 1695
1693 1696 # iterate from latest to oldest revision
1694 1697 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1695 1698 if not follow:
1696 1699 if rev > maxrev:
1697 1700 continue
1698 1701 else:
1699 1702 # Note that last might not be the first interesting
1700 1703 # rev to us:
1701 1704 # if the file has been changed after maxrev, we'll
1702 1705 # have linkrev(last) > maxrev, and we still need
1703 1706 # to explore the file graph
1704 1707 if rev not in ancestors:
1705 1708 continue
1706 1709 # XXX insert 1327 fix here
1707 1710 if flparentlinkrevs:
1708 1711 ancestors.update(flparentlinkrevs)
1709 1712
1710 1713 fncache.setdefault(rev, []).append(file_)
1711 1714 wanted.add(rev)
1712 1715 if copied:
1713 1716 copies.append(copied)
1714 1717
1715 1718 return wanted
1716 1719
1717 1720 class _followfilter(object):
1718 1721 def __init__(self, repo, onlyfirst=False):
1719 1722 self.repo = repo
1720 1723 self.startrev = nullrev
1721 1724 self.roots = set()
1722 1725 self.onlyfirst = onlyfirst
1723 1726
1724 1727 def match(self, rev):
1725 1728 def realparents(rev):
1726 1729 if self.onlyfirst:
1727 1730 return self.repo.changelog.parentrevs(rev)[0:1]
1728 1731 else:
1729 1732 return filter(lambda x: x != nullrev,
1730 1733 self.repo.changelog.parentrevs(rev))
1731 1734
1732 1735 if self.startrev == nullrev:
1733 1736 self.startrev = rev
1734 1737 return True
1735 1738
1736 1739 if rev > self.startrev:
1737 1740 # forward: all descendants
1738 1741 if not self.roots:
1739 1742 self.roots.add(self.startrev)
1740 1743 for parent in realparents(rev):
1741 1744 if parent in self.roots:
1742 1745 self.roots.add(rev)
1743 1746 return True
1744 1747 else:
1745 1748 # backwards: all parents
1746 1749 if not self.roots:
1747 1750 self.roots.update(realparents(self.startrev))
1748 1751 if rev in self.roots:
1749 1752 self.roots.remove(rev)
1750 1753 self.roots.update(realparents(rev))
1751 1754 return True
1752 1755
1753 1756 return False
1754 1757
1755 1758 def walkchangerevs(repo, match, opts, prepare):
1756 1759 '''Iterate over files and the revs in which they changed.
1757 1760
1758 1761 Callers most commonly need to iterate backwards over the history
1759 1762 in which they are interested. Doing so has awful (quadratic-looking)
1760 1763 performance, so we use iterators in a "windowed" way.
1761 1764
1762 1765 We walk a window of revisions in the desired order. Within the
1763 1766 window, we first walk forwards to gather data, then in the desired
1764 1767 order (usually backwards) to display it.
1765 1768
1766 1769 This function returns an iterator yielding contexts. Before
1767 1770 yielding each context, the iterator will first call the prepare
1768 1771 function on each context in the window in forward order.'''
1769 1772
1770 1773 follow = opts.get('follow') or opts.get('follow_first')
1771 1774 revs = _logrevs(repo, opts)
1772 1775 if not revs:
1773 1776 return []
1774 1777 wanted = set()
1775 1778 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1776 1779 opts.get('removed'))
1777 1780 fncache = {}
1778 1781 change = repo.changectx
1779 1782
1780 1783 # First step is to fill wanted, the set of revisions that we want to yield.
1781 1784 # When it does not induce extra cost, we also fill fncache for revisions in
1782 1785 # wanted: a cache of filenames that were changed (ctx.files()) and that
1783 1786 # match the file filtering conditions.
1784 1787
1785 1788 if match.always():
1786 1789 # No files, no patterns. Display all revs.
1787 1790 wanted = revs
1788 1791 elif not slowpath:
1789 1792 # We only have to read through the filelog to find wanted revisions
1790 1793
1791 1794 try:
1792 1795 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1793 1796 except FileWalkError:
1794 1797 slowpath = True
1795 1798
1796 1799 # We decided to fall back to the slowpath because at least one
1797 1800 # of the paths was not a file. Check to see if at least one of them
1798 1801 # existed in history, otherwise simply return
1799 1802 for path in match.files():
1800 1803 if path == '.' or path in repo.store:
1801 1804 break
1802 1805 else:
1803 1806 return []
1804 1807
1805 1808 if slowpath:
1806 1809 # We have to read the changelog to match filenames against
1807 1810 # changed files
1808 1811
1809 1812 if follow:
1810 1813 raise error.Abort(_('can only follow copies/renames for explicit '
1811 1814 'filenames'))
1812 1815
1813 1816 # The slow path checks files modified in every changeset.
1814 1817 # This is really slow on large repos, so compute the set lazily.
1815 1818 class lazywantedset(object):
1816 1819 def __init__(self):
1817 1820 self.set = set()
1818 1821 self.revs = set(revs)
1819 1822
1820 1823 # No need to worry about locality here because it will be accessed
1821 1824 # in the same order as the increasing window below.
1822 1825 def __contains__(self, value):
1823 1826 if value in self.set:
1824 1827 return True
1825 1828 elif not value in self.revs:
1826 1829 return False
1827 1830 else:
1828 1831 self.revs.discard(value)
1829 1832 ctx = change(value)
1830 1833 matches = filter(match, ctx.files())
1831 1834 if matches:
1832 1835 fncache[value] = matches
1833 1836 self.set.add(value)
1834 1837 return True
1835 1838 return False
1836 1839
1837 1840 def discard(self, value):
1838 1841 self.revs.discard(value)
1839 1842 self.set.discard(value)
1840 1843
1841 1844 wanted = lazywantedset()
1842 1845
1843 1846 # it might be worthwhile to do this in the iterator if the rev range
1844 1847 # is descending and the prune args are all within that range
1845 1848 for rev in opts.get('prune', ()):
1846 1849 rev = repo[rev].rev()
1847 1850 ff = _followfilter(repo)
1848 1851 stop = min(revs[0], revs[-1])
1849 1852 for x in xrange(rev, stop - 1, -1):
1850 1853 if ff.match(x):
1851 1854 wanted = wanted - [x]
1852 1855
1853 1856 # Now that wanted is correctly initialized, we can iterate over the
1854 1857 # revision range, yielding only revisions in wanted.
1855 1858 def iterate():
1856 1859 if follow and match.always():
1857 1860 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1858 1861 def want(rev):
1859 1862 return ff.match(rev) and rev in wanted
1860 1863 else:
1861 1864 def want(rev):
1862 1865 return rev in wanted
1863 1866
1864 1867 it = iter(revs)
1865 1868 stopiteration = False
1866 1869 for windowsize in increasingwindows():
1867 1870 nrevs = []
1868 1871 for i in xrange(windowsize):
1869 1872 rev = next(it, None)
1870 1873 if rev is None:
1871 1874 stopiteration = True
1872 1875 break
1873 1876 elif want(rev):
1874 1877 nrevs.append(rev)
1875 1878 for rev in sorted(nrevs):
1876 1879 fns = fncache.get(rev)
1877 1880 ctx = change(rev)
1878 1881 if not fns:
1879 1882 def fns_generator():
1880 1883 for f in ctx.files():
1881 1884 if match(f):
1882 1885 yield f
1883 1886 fns = fns_generator()
1884 1887 prepare(ctx, fns)
1885 1888 for rev in nrevs:
1886 1889 yield change(rev)
1887 1890
1888 1891 if stopiteration:
1889 1892 break
1890 1893
1891 1894 return iterate()
1892 1895
1893 1896 def _makefollowlogfilematcher(repo, files, followfirst):
1894 1897 # When displaying a revision with --patch --follow FILE, we have
1895 1898 # to know which file of the revision must be diffed. With
1896 1899 # --follow, we want the names of the ancestors of FILE in the
1897 1900 # revision, stored in "fcache". "fcache" is populated by
1898 1901 # reproducing the graph traversal already done by --follow revset
1899 1902 # and relating revs to file names (which is not "correct" but
1900 1903 # good enough).
1901 1904 fcache = {}
1902 1905 fcacheready = [False]
1903 1906 pctx = repo['.']
1904 1907
1905 1908 def populate():
1906 1909 for fn in files:
1907 1910 fctx = pctx[fn]
1908 1911 fcache.setdefault(fctx.introrev(), set()).add(fctx.path())
1909 1912 for c in fctx.ancestors(followfirst=followfirst):
1910 1913 fcache.setdefault(c.rev(), set()).add(c.path())
1911 1914
1912 1915 def filematcher(rev):
1913 1916 if not fcacheready[0]:
1914 1917 # Lazy initialization
1915 1918 fcacheready[0] = True
1916 1919 populate()
1917 1920 return scmutil.matchfiles(repo, fcache.get(rev, []))
1918 1921
1919 1922 return filematcher
1920 1923
1921 1924 def _makenofollowlogfilematcher(repo, pats, opts):
1922 1925 '''hook for extensions to override the filematcher for non-follow cases'''
1923 1926 return None
1924 1927
1925 1928 def _makelogrevset(repo, pats, opts, revs):
1926 1929 """Return (expr, filematcher) where expr is a revset string built
1927 1930 from log options and file patterns or None. If --stat or --patch
1928 1931 are not passed filematcher is None. Otherwise it is a callable
1929 1932 taking a revision number and returning a match objects filtering
1930 1933 the files to be detailed when displaying the revision.
1931 1934 """
1932 1935 opt2revset = {
1933 1936 'no_merges': ('not merge()', None),
1934 1937 'only_merges': ('merge()', None),
1935 1938 '_ancestors': ('ancestors(%(val)s)', None),
1936 1939 '_fancestors': ('_firstancestors(%(val)s)', None),
1937 1940 '_descendants': ('descendants(%(val)s)', None),
1938 1941 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1939 1942 '_matchfiles': ('_matchfiles(%(val)s)', None),
1940 1943 'date': ('date(%(val)r)', None),
1941 1944 'branch': ('branch(%(val)r)', ' or '),
1942 1945 '_patslog': ('filelog(%(val)r)', ' or '),
1943 1946 '_patsfollow': ('follow(%(val)r)', ' or '),
1944 1947 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1945 1948 'keyword': ('keyword(%(val)r)', ' or '),
1946 1949 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1947 1950 'user': ('user(%(val)r)', ' or '),
1948 1951 }
1949 1952
1950 1953 opts = dict(opts)
1951 1954 # follow or not follow?
1952 1955 follow = opts.get('follow') or opts.get('follow_first')
1953 1956 if opts.get('follow_first'):
1954 1957 followfirst = 1
1955 1958 else:
1956 1959 followfirst = 0
1957 1960 # --follow with FILE behavior depends on revs...
1958 1961 it = iter(revs)
1959 1962 startrev = next(it)
1960 1963 followdescendants = startrev < next(it, startrev)
1961 1964
1962 1965 # branch and only_branch are really aliases and must be handled at
1963 1966 # the same time
1964 1967 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1965 1968 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1966 1969 # pats/include/exclude are passed to match.match() directly in
1967 1970 # _matchfiles() revset but walkchangerevs() builds its matcher with
1968 1971 # scmutil.match(). The difference is input pats are globbed on
1969 1972 # platforms without shell expansion (windows).
1970 1973 wctx = repo[None]
1971 1974 match, pats = scmutil.matchandpats(wctx, pats, opts)
1972 1975 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1973 1976 opts.get('removed'))
1974 1977 if not slowpath:
1975 1978 for f in match.files():
1976 1979 if follow and f not in wctx:
1977 1980 # If the file exists, it may be a directory, so let it
1978 1981 # take the slow path.
1979 1982 if os.path.exists(repo.wjoin(f)):
1980 1983 slowpath = True
1981 1984 continue
1982 1985 else:
1983 1986 raise error.Abort(_('cannot follow file not in parent '
1984 1987 'revision: "%s"') % f)
1985 1988 filelog = repo.file(f)
1986 1989 if not filelog:
1987 1990 # A zero count may be a directory or deleted file, so
1988 1991 # try to find matching entries on the slow path.
1989 1992 if follow:
1990 1993 raise error.Abort(
1991 1994 _('cannot follow nonexistent file: "%s"') % f)
1992 1995 slowpath = True
1993 1996
1994 1997 # We decided to fall back to the slowpath because at least one
1995 1998 # of the paths was not a file. Check to see if at least one of them
1996 1999 # existed in history - in that case, we'll continue down the
1997 2000 # slowpath; otherwise, we can turn off the slowpath
1998 2001 if slowpath:
1999 2002 for path in match.files():
2000 2003 if path == '.' or path in repo.store:
2001 2004 break
2002 2005 else:
2003 2006 slowpath = False
2004 2007
2005 2008 fpats = ('_patsfollow', '_patsfollowfirst')
2006 2009 fnopats = (('_ancestors', '_fancestors'),
2007 2010 ('_descendants', '_fdescendants'))
2008 2011 if slowpath:
2009 2012 # See walkchangerevs() slow path.
2010 2013 #
2011 2014 # pats/include/exclude cannot be represented as separate
2012 2015 # revset expressions as their filtering logic applies at file
2013 2016 # level. For instance "-I a -X a" matches a revision touching
2014 2017 # "a" and "b" while "file(a) and not file(b)" does
2015 2018 # not. Besides, filesets are evaluated against the working
2016 2019 # directory.
2017 2020 matchargs = ['r:', 'd:relpath']
2018 2021 for p in pats:
2019 2022 matchargs.append('p:' + p)
2020 2023 for p in opts.get('include', []):
2021 2024 matchargs.append('i:' + p)
2022 2025 for p in opts.get('exclude', []):
2023 2026 matchargs.append('x:' + p)
2024 2027 matchargs = ','.join(('%r' % p) for p in matchargs)
2025 2028 opts['_matchfiles'] = matchargs
2026 2029 if follow:
2027 2030 opts[fnopats[0][followfirst]] = '.'
2028 2031 else:
2029 2032 if follow:
2030 2033 if pats:
2031 2034 # follow() revset interprets its file argument as a
2032 2035 # manifest entry, so use match.files(), not pats.
2033 2036 opts[fpats[followfirst]] = list(match.files())
2034 2037 else:
2035 2038 op = fnopats[followdescendants][followfirst]
2036 2039 opts[op] = 'rev(%d)' % startrev
2037 2040 else:
2038 2041 opts['_patslog'] = list(pats)
2039 2042
2040 2043 filematcher = None
2041 2044 if opts.get('patch') or opts.get('stat'):
2042 2045 # When following files, track renames via a special matcher.
2043 2046 # If we're forced to take the slowpath it means we're following
2044 2047 # at least one pattern/directory, so don't bother with rename tracking.
2045 2048 if follow and not match.always() and not slowpath:
2046 2049 # _makefollowlogfilematcher expects its files argument to be
2047 2050 # relative to the repo root, so use match.files(), not pats.
2048 2051 filematcher = _makefollowlogfilematcher(repo, match.files(),
2049 2052 followfirst)
2050 2053 else:
2051 2054 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
2052 2055 if filematcher is None:
2053 2056 filematcher = lambda rev: match
2054 2057
2055 2058 expr = []
2056 2059 for op, val in sorted(opts.iteritems()):
2057 2060 if not val:
2058 2061 continue
2059 2062 if op not in opt2revset:
2060 2063 continue
2061 2064 revop, andor = opt2revset[op]
2062 2065 if '%(val)' not in revop:
2063 2066 expr.append(revop)
2064 2067 else:
2065 2068 if not isinstance(val, list):
2066 2069 e = revop % {'val': val}
2067 2070 else:
2068 2071 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
2069 2072 expr.append(e)
2070 2073
2071 2074 if expr:
2072 2075 expr = '(' + ' and '.join(expr) + ')'
2073 2076 else:
2074 2077 expr = None
2075 2078 return expr, filematcher
2076 2079
2077 2080 def _logrevs(repo, opts):
2078 2081 # Default --rev value depends on --follow but --follow behavior
2079 2082 # depends on revisions resolved from --rev...
2080 2083 follow = opts.get('follow') or opts.get('follow_first')
2081 2084 if opts.get('rev'):
2082 2085 revs = scmutil.revrange(repo, opts['rev'])
2083 2086 elif follow and repo.dirstate.p1() == nullid:
2084 2087 revs = smartset.baseset()
2085 2088 elif follow:
2086 2089 revs = repo.revs('reverse(:.)')
2087 2090 else:
2088 2091 revs = smartset.spanset(repo)
2089 2092 revs.reverse()
2090 2093 return revs
2091 2094
2092 2095 def getgraphlogrevs(repo, pats, opts):
2093 2096 """Return (revs, expr, filematcher) where revs is an iterable of
2094 2097 revision numbers, expr is a revset string built from log options
2095 2098 and file patterns or None, and used to filter 'revs'. If --stat or
2096 2099 --patch are not passed filematcher is None. Otherwise it is a
2097 2100 callable taking a revision number and returning a match objects
2098 2101 filtering the files to be detailed when displaying the revision.
2099 2102 """
2100 2103 limit = loglimit(opts)
2101 2104 revs = _logrevs(repo, opts)
2102 2105 if not revs:
2103 2106 return smartset.baseset(), None, None
2104 2107 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2105 2108 if opts.get('rev'):
2106 2109 # User-specified revs might be unsorted, but don't sort before
2107 2110 # _makelogrevset because it might depend on the order of revs
2108 2111 if not (revs.isdescending() or revs.istopo()):
2109 2112 revs.sort(reverse=True)
2110 2113 if expr:
2111 2114 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2112 2115 revs = matcher(repo, revs)
2113 2116 if limit is not None:
2114 2117 limitedrevs = []
2115 2118 for idx, rev in enumerate(revs):
2116 2119 if idx >= limit:
2117 2120 break
2118 2121 limitedrevs.append(rev)
2119 2122 revs = smartset.baseset(limitedrevs)
2120 2123
2121 2124 return revs, expr, filematcher
2122 2125
2123 2126 def getlogrevs(repo, pats, opts):
2124 2127 """Return (revs, expr, filematcher) where revs is an iterable of
2125 2128 revision numbers, expr is a revset string built from log options
2126 2129 and file patterns or None, and used to filter 'revs'. If --stat or
2127 2130 --patch are not passed filematcher is None. Otherwise it is a
2128 2131 callable taking a revision number and returning a match objects
2129 2132 filtering the files to be detailed when displaying the revision.
2130 2133 """
2131 2134 limit = loglimit(opts)
2132 2135 revs = _logrevs(repo, opts)
2133 2136 if not revs:
2134 2137 return smartset.baseset([]), None, None
2135 2138 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2136 2139 if expr:
2137 2140 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2138 2141 revs = matcher(repo, revs)
2139 2142 if limit is not None:
2140 2143 limitedrevs = []
2141 2144 for idx, r in enumerate(revs):
2142 2145 if limit <= idx:
2143 2146 break
2144 2147 limitedrevs.append(r)
2145 2148 revs = smartset.baseset(limitedrevs)
2146 2149
2147 2150 return revs, expr, filematcher
2148 2151
2149 2152 def _graphnodeformatter(ui, displayer):
2150 2153 spec = ui.config('ui', 'graphnodetemplate')
2151 2154 if not spec:
2152 2155 return templatekw.showgraphnode # fast path for "{graphnode}"
2153 2156
2154 2157 templ = formatter.gettemplater(ui, 'graphnode', spec)
2155 2158 cache = {}
2156 2159 if isinstance(displayer, changeset_templater):
2157 2160 cache = displayer.cache # reuse cache of slow templates
2158 2161 props = templatekw.keywords.copy()
2159 2162 props['templ'] = templ
2160 2163 props['cache'] = cache
2161 2164 def formatnode(repo, ctx):
2162 2165 props['ctx'] = ctx
2163 2166 props['repo'] = repo
2164 2167 props['ui'] = repo.ui
2165 2168 props['revcache'] = {}
2166 2169 return templater.stringify(templ('graphnode', **props))
2167 2170 return formatnode
2168 2171
2169 2172 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
2170 2173 filematcher=None):
2171 2174 formatnode = _graphnodeformatter(ui, displayer)
2172 2175 state = graphmod.asciistate()
2173 2176 styles = state['styles']
2174 2177
2175 2178 # only set graph styling if HGPLAIN is not set.
2176 2179 if ui.plain('graph'):
2177 2180 # set all edge styles to |, the default pre-3.8 behaviour
2178 2181 styles.update(dict.fromkeys(styles, '|'))
2179 2182 else:
2180 2183 edgetypes = {
2181 2184 'parent': graphmod.PARENT,
2182 2185 'grandparent': graphmod.GRANDPARENT,
2183 2186 'missing': graphmod.MISSINGPARENT
2184 2187 }
2185 2188 for name, key in edgetypes.items():
2186 2189 # experimental config: experimental.graphstyle.*
2187 2190 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
2188 2191 styles[key])
2189 2192 if not styles[key]:
2190 2193 styles[key] = None
2191 2194
2192 2195 # experimental config: experimental.graphshorten
2193 2196 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
2194 2197
2195 2198 for rev, type, ctx, parents in dag:
2196 2199 char = formatnode(repo, ctx)
2197 2200 copies = None
2198 2201 if getrenamed and ctx.rev():
2199 2202 copies = []
2200 2203 for fn in ctx.files():
2201 2204 rename = getrenamed(fn, ctx.rev())
2202 2205 if rename:
2203 2206 copies.append((fn, rename[0]))
2204 2207 revmatchfn = None
2205 2208 if filematcher is not None:
2206 2209 revmatchfn = filematcher(ctx.rev())
2207 2210 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
2208 2211 lines = displayer.hunk.pop(rev).split('\n')
2209 2212 if not lines[-1]:
2210 2213 del lines[-1]
2211 2214 displayer.flush(ctx)
2212 2215 edges = edgefn(type, char, lines, state, rev, parents)
2213 2216 for type, char, lines, coldata in edges:
2214 2217 graphmod.ascii(ui, state, type, char, lines, coldata)
2215 2218 displayer.close()
2216 2219
2217 2220 def graphlog(ui, repo, pats, opts):
2218 2221 # Parameters are identical to log command ones
2219 2222 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
2220 2223 revdag = graphmod.dagwalker(repo, revs)
2221 2224
2222 2225 getrenamed = None
2223 2226 if opts.get('copies'):
2224 2227 endrev = None
2225 2228 if opts.get('rev'):
2226 2229 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
2227 2230 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
2228 2231
2229 2232 ui.pager('log')
2230 2233 displayer = show_changeset(ui, repo, opts, buffered=True)
2231 2234 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
2232 2235 filematcher)
2233 2236
2234 2237 def checkunsupportedgraphflags(pats, opts):
2235 2238 for op in ["newest_first"]:
2236 2239 if op in opts and opts[op]:
2237 2240 raise error.Abort(_("-G/--graph option is incompatible with --%s")
2238 2241 % op.replace("_", "-"))
2239 2242
2240 2243 def graphrevs(repo, nodes, opts):
2241 2244 limit = loglimit(opts)
2242 2245 nodes.reverse()
2243 2246 if limit is not None:
2244 2247 nodes = nodes[:limit]
2245 2248 return graphmod.nodes(repo, nodes)
2246 2249
2247 2250 def add(ui, repo, match, prefix, explicitonly, **opts):
2248 2251 join = lambda f: os.path.join(prefix, f)
2249 2252 bad = []
2250 2253
2251 2254 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2252 2255 names = []
2253 2256 wctx = repo[None]
2254 2257 cca = None
2255 2258 abort, warn = scmutil.checkportabilityalert(ui)
2256 2259 if abort or warn:
2257 2260 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2258 2261
2259 2262 badmatch = matchmod.badmatch(match, badfn)
2260 2263 dirstate = repo.dirstate
2261 2264 # We don't want to just call wctx.walk here, since it would return a lot of
2262 2265 # clean files, which we aren't interested in and takes time.
2263 2266 for f in sorted(dirstate.walk(badmatch, sorted(wctx.substate),
2264 2267 True, False, full=False)):
2265 2268 exact = match.exact(f)
2266 2269 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2267 2270 if cca:
2268 2271 cca(f)
2269 2272 names.append(f)
2270 2273 if ui.verbose or not exact:
2271 2274 ui.status(_('adding %s\n') % match.rel(f))
2272 2275
2273 2276 for subpath in sorted(wctx.substate):
2274 2277 sub = wctx.sub(subpath)
2275 2278 try:
2276 2279 submatch = matchmod.subdirmatcher(subpath, match)
2277 2280 if opts.get('subrepos'):
2278 2281 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
2279 2282 else:
2280 2283 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
2281 2284 except error.LookupError:
2282 2285 ui.status(_("skipping missing subrepository: %s\n")
2283 2286 % join(subpath))
2284 2287
2285 2288 if not opts.get('dry_run'):
2286 2289 rejected = wctx.add(names, prefix)
2287 2290 bad.extend(f for f in rejected if f in match.files())
2288 2291 return bad
2289 2292
2290 2293 def forget(ui, repo, match, prefix, explicitonly):
2291 2294 join = lambda f: os.path.join(prefix, f)
2292 2295 bad = []
2293 2296 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2294 2297 wctx = repo[None]
2295 2298 forgot = []
2296 2299
2297 2300 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2298 2301 forget = sorted(s[0] + s[1] + s[3] + s[6])
2299 2302 if explicitonly:
2300 2303 forget = [f for f in forget if match.exact(f)]
2301 2304
2302 2305 for subpath in sorted(wctx.substate):
2303 2306 sub = wctx.sub(subpath)
2304 2307 try:
2305 2308 submatch = matchmod.subdirmatcher(subpath, match)
2306 2309 subbad, subforgot = sub.forget(submatch, prefix)
2307 2310 bad.extend([subpath + '/' + f for f in subbad])
2308 2311 forgot.extend([subpath + '/' + f for f in subforgot])
2309 2312 except error.LookupError:
2310 2313 ui.status(_("skipping missing subrepository: %s\n")
2311 2314 % join(subpath))
2312 2315
2313 2316 if not explicitonly:
2314 2317 for f in match.files():
2315 2318 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2316 2319 if f not in forgot:
2317 2320 if repo.wvfs.exists(f):
2318 2321 # Don't complain if the exact case match wasn't given.
2319 2322 # But don't do this until after checking 'forgot', so
2320 2323 # that subrepo files aren't normalized, and this op is
2321 2324 # purely from data cached by the status walk above.
2322 2325 if repo.dirstate.normalize(f) in repo.dirstate:
2323 2326 continue
2324 2327 ui.warn(_('not removing %s: '
2325 2328 'file is already untracked\n')
2326 2329 % match.rel(f))
2327 2330 bad.append(f)
2328 2331
2329 2332 for f in forget:
2330 2333 if ui.verbose or not match.exact(f):
2331 2334 ui.status(_('removing %s\n') % match.rel(f))
2332 2335
2333 2336 rejected = wctx.forget(forget, prefix)
2334 2337 bad.extend(f for f in rejected if f in match.files())
2335 2338 forgot.extend(f for f in forget if f not in rejected)
2336 2339 return bad, forgot
2337 2340
2338 2341 def files(ui, ctx, m, fm, fmt, subrepos):
2339 2342 rev = ctx.rev()
2340 2343 ret = 1
2341 2344 ds = ctx.repo().dirstate
2342 2345
2343 2346 for f in ctx.matches(m):
2344 2347 if rev is None and ds[f] == 'r':
2345 2348 continue
2346 2349 fm.startitem()
2347 2350 if ui.verbose:
2348 2351 fc = ctx[f]
2349 2352 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2350 2353 fm.data(abspath=f)
2351 2354 fm.write('path', fmt, m.rel(f))
2352 2355 ret = 0
2353 2356
2354 2357 for subpath in sorted(ctx.substate):
2355 2358 submatch = matchmod.subdirmatcher(subpath, m)
2356 2359 if (subrepos or m.exact(subpath) or any(submatch.files())):
2357 2360 sub = ctx.sub(subpath)
2358 2361 try:
2359 2362 recurse = m.exact(subpath) or subrepos
2360 2363 if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
2361 2364 ret = 0
2362 2365 except error.LookupError:
2363 2366 ui.status(_("skipping missing subrepository: %s\n")
2364 2367 % m.abs(subpath))
2365 2368
2366 2369 return ret
2367 2370
2368 2371 def remove(ui, repo, m, prefix, after, force, subrepos, warnings=None):
2369 2372 join = lambda f: os.path.join(prefix, f)
2370 2373 ret = 0
2371 2374 s = repo.status(match=m, clean=True)
2372 2375 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2373 2376
2374 2377 wctx = repo[None]
2375 2378
2376 2379 if warnings is None:
2377 2380 warnings = []
2378 2381 warn = True
2379 2382 else:
2380 2383 warn = False
2381 2384
2382 2385 subs = sorted(wctx.substate)
2383 2386 total = len(subs)
2384 2387 count = 0
2385 2388 for subpath in subs:
2386 2389 count += 1
2387 2390 submatch = matchmod.subdirmatcher(subpath, m)
2388 2391 if subrepos or m.exact(subpath) or any(submatch.files()):
2389 2392 ui.progress(_('searching'), count, total=total, unit=_('subrepos'))
2390 2393 sub = wctx.sub(subpath)
2391 2394 try:
2392 2395 if sub.removefiles(submatch, prefix, after, force, subrepos,
2393 2396 warnings):
2394 2397 ret = 1
2395 2398 except error.LookupError:
2396 2399 warnings.append(_("skipping missing subrepository: %s\n")
2397 2400 % join(subpath))
2398 2401 ui.progress(_('searching'), None)
2399 2402
2400 2403 # warn about failure to delete explicit files/dirs
2401 2404 deleteddirs = util.dirs(deleted)
2402 2405 files = m.files()
2403 2406 total = len(files)
2404 2407 count = 0
2405 2408 for f in files:
2406 2409 def insubrepo():
2407 2410 for subpath in wctx.substate:
2408 2411 if f.startswith(subpath + '/'):
2409 2412 return True
2410 2413 return False
2411 2414
2412 2415 count += 1
2413 2416 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2414 2417 isdir = f in deleteddirs or wctx.hasdir(f)
2415 2418 if (f in repo.dirstate or isdir or f == '.'
2416 2419 or insubrepo() or f in subs):
2417 2420 continue
2418 2421
2419 2422 if repo.wvfs.exists(f):
2420 2423 if repo.wvfs.isdir(f):
2421 2424 warnings.append(_('not removing %s: no tracked files\n')
2422 2425 % m.rel(f))
2423 2426 else:
2424 2427 warnings.append(_('not removing %s: file is untracked\n')
2425 2428 % m.rel(f))
2426 2429 # missing files will generate a warning elsewhere
2427 2430 ret = 1
2428 2431 ui.progress(_('deleting'), None)
2429 2432
2430 2433 if force:
2431 2434 list = modified + deleted + clean + added
2432 2435 elif after:
2433 2436 list = deleted
2434 2437 remaining = modified + added + clean
2435 2438 total = len(remaining)
2436 2439 count = 0
2437 2440 for f in remaining:
2438 2441 count += 1
2439 2442 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2440 2443 warnings.append(_('not removing %s: file still exists\n')
2441 2444 % m.rel(f))
2442 2445 ret = 1
2443 2446 ui.progress(_('skipping'), None)
2444 2447 else:
2445 2448 list = deleted + clean
2446 2449 total = len(modified) + len(added)
2447 2450 count = 0
2448 2451 for f in modified:
2449 2452 count += 1
2450 2453 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2451 2454 warnings.append(_('not removing %s: file is modified (use -f'
2452 2455 ' to force removal)\n') % m.rel(f))
2453 2456 ret = 1
2454 2457 for f in added:
2455 2458 count += 1
2456 2459 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2457 2460 warnings.append(_("not removing %s: file has been marked for add"
2458 2461 " (use 'hg forget' to undo add)\n") % m.rel(f))
2459 2462 ret = 1
2460 2463 ui.progress(_('skipping'), None)
2461 2464
2462 2465 list = sorted(list)
2463 2466 total = len(list)
2464 2467 count = 0
2465 2468 for f in list:
2466 2469 count += 1
2467 2470 if ui.verbose or not m.exact(f):
2468 2471 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2469 2472 ui.status(_('removing %s\n') % m.rel(f))
2470 2473 ui.progress(_('deleting'), None)
2471 2474
2472 2475 with repo.wlock():
2473 2476 if not after:
2474 2477 for f in list:
2475 2478 if f in added:
2476 2479 continue # we never unlink added files on remove
2477 2480 repo.wvfs.unlinkpath(f, ignoremissing=True)
2478 2481 repo[None].forget(list)
2479 2482
2480 2483 if warn:
2481 2484 for warning in warnings:
2482 2485 ui.warn(warning)
2483 2486
2484 2487 return ret
2485 2488
2486 2489 def cat(ui, repo, ctx, matcher, prefix, **opts):
2487 2490 err = 1
2488 2491
2489 2492 def write(path):
2490 2493 fp = makefileobj(repo, opts.get('output'), ctx.node(),
2491 2494 pathname=os.path.join(prefix, path))
2492 2495 data = ctx[path].data()
2493 2496 if opts.get('decode'):
2494 2497 data = repo.wwritedata(path, data)
2495 2498 fp.write(data)
2496 2499 fp.close()
2497 2500
2498 2501 # Automation often uses hg cat on single files, so special case it
2499 2502 # for performance to avoid the cost of parsing the manifest.
2500 2503 if len(matcher.files()) == 1 and not matcher.anypats():
2501 2504 file = matcher.files()[0]
2502 2505 mfl = repo.manifestlog
2503 2506 mfnode = ctx.manifestnode()
2504 2507 try:
2505 2508 if mfnode and mfl[mfnode].find(file)[0]:
2506 2509 write(file)
2507 2510 return 0
2508 2511 except KeyError:
2509 2512 pass
2510 2513
2511 2514 for abs in ctx.walk(matcher):
2512 2515 write(abs)
2513 2516 err = 0
2514 2517
2515 2518 for subpath in sorted(ctx.substate):
2516 2519 sub = ctx.sub(subpath)
2517 2520 try:
2518 2521 submatch = matchmod.subdirmatcher(subpath, matcher)
2519 2522
2520 2523 if not sub.cat(submatch, os.path.join(prefix, sub._path),
2521 2524 **opts):
2522 2525 err = 0
2523 2526 except error.RepoLookupError:
2524 2527 ui.status(_("skipping missing subrepository: %s\n")
2525 2528 % os.path.join(prefix, subpath))
2526 2529
2527 2530 return err
2528 2531
2529 2532 def commit(ui, repo, commitfunc, pats, opts):
2530 2533 '''commit the specified files or all outstanding changes'''
2531 2534 date = opts.get('date')
2532 2535 if date:
2533 2536 opts['date'] = util.parsedate(date)
2534 2537 message = logmessage(ui, opts)
2535 2538 matcher = scmutil.match(repo[None], pats, opts)
2536 2539
2537 2540 # extract addremove carefully -- this function can be called from a command
2538 2541 # that doesn't support addremove
2539 2542 if opts.get('addremove'):
2540 2543 if scmutil.addremove(repo, matcher, "", opts) != 0:
2541 2544 raise error.Abort(
2542 2545 _("failed to mark all new/missing files as added/removed"))
2543 2546
2544 2547 return commitfunc(ui, repo, message, matcher, opts)
2545 2548
2546 2549 def samefile(f, ctx1, ctx2):
2547 2550 if f in ctx1.manifest():
2548 2551 a = ctx1.filectx(f)
2549 2552 if f in ctx2.manifest():
2550 2553 b = ctx2.filectx(f)
2551 2554 return (not a.cmp(b)
2552 2555 and a.flags() == b.flags())
2553 2556 else:
2554 2557 return False
2555 2558 else:
2556 2559 return f not in ctx2.manifest()
2557 2560
2558 2561 def amend(ui, repo, commitfunc, old, extra, pats, opts):
2559 2562 # avoid cycle context -> subrepo -> cmdutil
2560 2563 from . import context
2561 2564
2562 2565 # amend will reuse the existing user if not specified, but the obsolete
2563 2566 # marker creation requires that the current user's name is specified.
2564 2567 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2565 2568 ui.username() # raise exception if username not set
2566 2569
2567 2570 ui.note(_('amending changeset %s\n') % old)
2568 2571 base = old.p1()
2569 2572 createmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
2570 2573
2571 2574 wlock = lock = newid = None
2572 2575 try:
2573 2576 wlock = repo.wlock()
2574 2577 lock = repo.lock()
2575 2578 with repo.transaction('amend') as tr:
2576 2579 # See if we got a message from -m or -l, if not, open the editor
2577 2580 # with the message of the changeset to amend
2578 2581 message = logmessage(ui, opts)
2579 2582 # ensure logfile does not conflict with later enforcement of the
2580 2583 # message. potential logfile content has been processed by
2581 2584 # `logmessage` anyway.
2582 2585 opts.pop('logfile')
2583 2586 # First, do a regular commit to record all changes in the working
2584 2587 # directory (if there are any)
2585 2588 ui.callhooks = False
2586 2589 activebookmark = repo._bookmarks.active
2587 2590 try:
2588 2591 repo._bookmarks.active = None
2589 2592 opts['message'] = 'temporary amend commit for %s' % old
2590 2593 node = commit(ui, repo, commitfunc, pats, opts)
2591 2594 finally:
2592 2595 repo._bookmarks.active = activebookmark
2593 2596 repo._bookmarks.recordchange(tr)
2594 2597 ui.callhooks = True
2595 2598 ctx = repo[node]
2596 2599
2597 2600 # Participating changesets:
2598 2601 #
2599 2602 # node/ctx o - new (intermediate) commit that contains changes
2600 2603 # | from working dir to go into amending commit
2601 2604 # | (or a workingctx if there were no changes)
2602 2605 # |
2603 2606 # old o - changeset to amend
2604 2607 # |
2605 2608 # base o - parent of amending changeset
2606 2609
2607 2610 # Update extra dict from amended commit (e.g. to preserve graft
2608 2611 # source)
2609 2612 extra.update(old.extra())
2610 2613
2611 2614 # Also update it from the intermediate commit or from the wctx
2612 2615 extra.update(ctx.extra())
2613 2616
2614 2617 if len(old.parents()) > 1:
2615 2618 # ctx.files() isn't reliable for merges, so fall back to the
2616 2619 # slower repo.status() method
2617 2620 files = set([fn for st in repo.status(base, old)[:3]
2618 2621 for fn in st])
2619 2622 else:
2620 2623 files = set(old.files())
2621 2624
2622 2625 # Second, we use either the commit we just did, or if there were no
2623 2626 # changes the parent of the working directory as the version of the
2624 2627 # files in the final amend commit
2625 2628 if node:
2626 2629 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
2627 2630
2628 2631 user = ctx.user()
2629 2632 date = ctx.date()
2630 2633 # Recompute copies (avoid recording a -> b -> a)
2631 2634 copied = copies.pathcopies(base, ctx)
2632 2635 if old.p2:
2633 2636 copied.update(copies.pathcopies(old.p2(), ctx))
2634 2637
2635 2638 # Prune files which were reverted by the updates: if old
2636 2639 # introduced file X and our intermediate commit, node,
2637 2640 # renamed that file, then those two files are the same and
2638 2641 # we can discard X from our list of files. Likewise if X
2639 2642 # was deleted, it's no longer relevant
2640 2643 files.update(ctx.files())
2641 2644 files = [f for f in files if not samefile(f, ctx, base)]
2642 2645
2643 2646 def filectxfn(repo, ctx_, path):
2644 2647 try:
2645 2648 fctx = ctx[path]
2646 2649 flags = fctx.flags()
2647 2650 mctx = context.memfilectx(repo,
2648 2651 fctx.path(), fctx.data(),
2649 2652 islink='l' in flags,
2650 2653 isexec='x' in flags,
2651 2654 copied=copied.get(path))
2652 2655 return mctx
2653 2656 except KeyError:
2654 2657 return None
2655 2658 else:
2656 2659 ui.note(_('copying changeset %s to %s\n') % (old, base))
2657 2660
2658 2661 # Use version of files as in the old cset
2659 2662 def filectxfn(repo, ctx_, path):
2660 2663 try:
2661 2664 return old.filectx(path)
2662 2665 except KeyError:
2663 2666 return None
2664 2667
2665 2668 user = opts.get('user') or old.user()
2666 2669 date = opts.get('date') or old.date()
2667 2670 editform = mergeeditform(old, 'commit.amend')
2668 2671 editor = getcommiteditor(editform=editform, **opts)
2669 2672 if not message:
2670 2673 editor = getcommiteditor(edit=True, editform=editform)
2671 2674 message = old.description()
2672 2675
2673 2676 pureextra = extra.copy()
2674 2677 extra['amend_source'] = old.hex()
2675 2678
2676 2679 new = context.memctx(repo,
2677 2680 parents=[base.node(), old.p2().node()],
2678 2681 text=message,
2679 2682 files=files,
2680 2683 filectxfn=filectxfn,
2681 2684 user=user,
2682 2685 date=date,
2683 2686 extra=extra,
2684 2687 editor=editor)
2685 2688
2686 2689 newdesc = changelog.stripdesc(new.description())
2687 2690 if ((not node)
2688 2691 and newdesc == old.description()
2689 2692 and user == old.user()
2690 2693 and date == old.date()
2691 2694 and pureextra == old.extra()):
2692 2695 # nothing changed. continuing here would create a new node
2693 2696 # anyway because of the amend_source noise.
2694 2697 #
2695 2698 # This not what we expect from amend.
2696 2699 return old.node()
2697 2700
2698 2701 ph = repo.ui.config('phases', 'new-commit', phases.draft)
2699 2702 try:
2700 2703 if opts.get('secret'):
2701 2704 commitphase = 'secret'
2702 2705 else:
2703 2706 commitphase = old.phase()
2704 2707 repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
2705 2708 newid = repo.commitctx(new)
2706 2709 finally:
2707 2710 repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
2708 2711 if newid != old.node():
2709 2712 # Reroute the working copy parent to the new changeset
2710 2713 repo.setparents(newid, nullid)
2711 2714
2712 2715 # Move bookmarks from old parent to amend commit
2713 2716 bms = repo.nodebookmarks(old.node())
2714 2717 if bms:
2715 2718 marks = repo._bookmarks
2716 2719 for bm in bms:
2717 2720 ui.debug('moving bookmarks %r from %s to %s\n' %
2718 2721 (marks, old.hex(), hex(newid)))
2719 2722 marks[bm] = newid
2720 2723 marks.recordchange(tr)
2721 2724 #commit the whole amend process
2722 2725 if createmarkers:
2723 2726 # mark the new changeset as successor of the rewritten one
2724 2727 new = repo[newid]
2725 2728 obs = [(old, (new,))]
2726 2729 if node:
2727 2730 obs.append((ctx, ()))
2728 2731
2729 2732 obsolete.createmarkers(repo, obs)
2730 2733 if not createmarkers and newid != old.node():
2731 2734 # Strip the intermediate commit (if there was one) and the amended
2732 2735 # commit
2733 2736 if node:
2734 2737 ui.note(_('stripping intermediate changeset %s\n') % ctx)
2735 2738 ui.note(_('stripping amended changeset %s\n') % old)
2736 2739 repair.strip(ui, repo, old.node(), topic='amend-backup')
2737 2740 finally:
2738 2741 lockmod.release(lock, wlock)
2739 2742 return newid
2740 2743
2741 2744 def commiteditor(repo, ctx, subs, editform=''):
2742 2745 if ctx.description():
2743 2746 return ctx.description()
2744 2747 return commitforceeditor(repo, ctx, subs, editform=editform,
2745 2748 unchangedmessagedetection=True)
2746 2749
2747 2750 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2748 2751 editform='', unchangedmessagedetection=False):
2749 2752 if not extramsg:
2750 2753 extramsg = _("Leave message empty to abort commit.")
2751 2754
2752 2755 forms = [e for e in editform.split('.') if e]
2753 2756 forms.insert(0, 'changeset')
2754 2757 templatetext = None
2755 2758 while forms:
2756 2759 tmpl = repo.ui.config('committemplate', '.'.join(forms))
2757 2760 if tmpl:
2758 2761 templatetext = committext = buildcommittemplate(
2759 2762 repo, ctx, subs, extramsg, tmpl)
2760 2763 break
2761 2764 forms.pop()
2762 2765 else:
2763 2766 committext = buildcommittext(repo, ctx, subs, extramsg)
2764 2767
2765 2768 # run editor in the repository root
2766 2769 olddir = pycompat.getcwd()
2767 2770 os.chdir(repo.root)
2768 2771
2769 2772 # make in-memory changes visible to external process
2770 2773 tr = repo.currenttransaction()
2771 2774 repo.dirstate.write(tr)
2772 2775 pending = tr and tr.writepending() and repo.root
2773 2776
2774 2777 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2775 2778 editform=editform, pending=pending,
2776 2779 repopath=repo.path)
2777 2780 text = editortext
2778 2781
2779 2782 # strip away anything below this special string (used for editors that want
2780 2783 # to display the diff)
2781 2784 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
2782 2785 if stripbelow:
2783 2786 text = text[:stripbelow.start()]
2784 2787
2785 2788 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2786 2789 os.chdir(olddir)
2787 2790
2788 2791 if finishdesc:
2789 2792 text = finishdesc(text)
2790 2793 if not text.strip():
2791 2794 raise error.Abort(_("empty commit message"))
2792 2795 if unchangedmessagedetection and editortext == templatetext:
2793 2796 raise error.Abort(_("commit message unchanged"))
2794 2797
2795 2798 return text
2796 2799
2797 2800 def buildcommittemplate(repo, ctx, subs, extramsg, tmpl):
2798 2801 ui = repo.ui
2799 2802 tmpl, mapfile = gettemplate(ui, tmpl, None)
2800 2803
2801 2804 t = changeset_templater(ui, repo, None, {}, tmpl, mapfile, False)
2802 2805
2803 2806 for k, v in repo.ui.configitems('committemplate'):
2804 2807 if k != 'changeset':
2805 2808 t.t.cache[k] = v
2806 2809
2807 2810 if not extramsg:
2808 2811 extramsg = '' # ensure that extramsg is string
2809 2812
2810 2813 ui.pushbuffer()
2811 2814 t.show(ctx, extramsg=extramsg)
2812 2815 return ui.popbuffer()
2813 2816
2814 2817 def hgprefix(msg):
2815 2818 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2816 2819
2817 2820 def buildcommittext(repo, ctx, subs, extramsg):
2818 2821 edittext = []
2819 2822 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2820 2823 if ctx.description():
2821 2824 edittext.append(ctx.description())
2822 2825 edittext.append("")
2823 2826 edittext.append("") # Empty line between message and comments.
2824 2827 edittext.append(hgprefix(_("Enter commit message."
2825 2828 " Lines beginning with 'HG:' are removed.")))
2826 2829 edittext.append(hgprefix(extramsg))
2827 2830 edittext.append("HG: --")
2828 2831 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2829 2832 if ctx.p2():
2830 2833 edittext.append(hgprefix(_("branch merge")))
2831 2834 if ctx.branch():
2832 2835 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2833 2836 if bookmarks.isactivewdirparent(repo):
2834 2837 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2835 2838 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2836 2839 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2837 2840 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2838 2841 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2839 2842 if not added and not modified and not removed:
2840 2843 edittext.append(hgprefix(_("no files changed")))
2841 2844 edittext.append("")
2842 2845
2843 2846 return "\n".join(edittext)
2844 2847
2845 2848 def commitstatus(repo, node, branch, bheads=None, opts=None):
2846 2849 if opts is None:
2847 2850 opts = {}
2848 2851 ctx = repo[node]
2849 2852 parents = ctx.parents()
2850 2853
2851 2854 if (not opts.get('amend') and bheads and node not in bheads and not
2852 2855 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2853 2856 repo.ui.status(_('created new head\n'))
2854 2857 # The message is not printed for initial roots. For the other
2855 2858 # changesets, it is printed in the following situations:
2856 2859 #
2857 2860 # Par column: for the 2 parents with ...
2858 2861 # N: null or no parent
2859 2862 # B: parent is on another named branch
2860 2863 # C: parent is a regular non head changeset
2861 2864 # H: parent was a branch head of the current branch
2862 2865 # Msg column: whether we print "created new head" message
2863 2866 # In the following, it is assumed that there already exists some
2864 2867 # initial branch heads of the current branch, otherwise nothing is
2865 2868 # printed anyway.
2866 2869 #
2867 2870 # Par Msg Comment
2868 2871 # N N y additional topo root
2869 2872 #
2870 2873 # B N y additional branch root
2871 2874 # C N y additional topo head
2872 2875 # H N n usual case
2873 2876 #
2874 2877 # B B y weird additional branch root
2875 2878 # C B y branch merge
2876 2879 # H B n merge with named branch
2877 2880 #
2878 2881 # C C y additional head from merge
2879 2882 # C H n merge with a head
2880 2883 #
2881 2884 # H H n head merge: head count decreases
2882 2885
2883 2886 if not opts.get('close_branch'):
2884 2887 for r in parents:
2885 2888 if r.closesbranch() and r.branch() == branch:
2886 2889 repo.ui.status(_('reopening closed branch head %d\n') % r)
2887 2890
2888 2891 if repo.ui.debugflag:
2889 2892 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
2890 2893 elif repo.ui.verbose:
2891 2894 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
2892 2895
2893 2896 def postcommitstatus(repo, pats, opts):
2894 2897 return repo.status(match=scmutil.match(repo[None], pats, opts))
2895 2898
2896 2899 def revert(ui, repo, ctx, parents, *pats, **opts):
2897 2900 parent, p2 = parents
2898 2901 node = ctx.node()
2899 2902
2900 2903 mf = ctx.manifest()
2901 2904 if node == p2:
2902 2905 parent = p2
2903 2906
2904 2907 # need all matching names in dirstate and manifest of target rev,
2905 2908 # so have to walk both. do not print errors if files exist in one
2906 2909 # but not other. in both cases, filesets should be evaluated against
2907 2910 # workingctx to get consistent result (issue4497). this means 'set:**'
2908 2911 # cannot be used to select missing files from target rev.
2909 2912
2910 2913 # `names` is a mapping for all elements in working copy and target revision
2911 2914 # The mapping is in the form:
2912 2915 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2913 2916 names = {}
2914 2917
2915 2918 with repo.wlock():
2916 2919 ## filling of the `names` mapping
2917 2920 # walk dirstate to fill `names`
2918 2921
2919 2922 interactive = opts.get('interactive', False)
2920 2923 wctx = repo[None]
2921 2924 m = scmutil.match(wctx, pats, opts)
2922 2925
2923 2926 # we'll need this later
2924 2927 targetsubs = sorted(s for s in wctx.substate if m(s))
2925 2928
2926 2929 if not m.always():
2927 2930 for abs in repo.walk(matchmod.badmatch(m, lambda x, y: False)):
2928 2931 names[abs] = m.rel(abs), m.exact(abs)
2929 2932
2930 2933 # walk target manifest to fill `names`
2931 2934
2932 2935 def badfn(path, msg):
2933 2936 if path in names:
2934 2937 return
2935 2938 if path in ctx.substate:
2936 2939 return
2937 2940 path_ = path + '/'
2938 2941 for f in names:
2939 2942 if f.startswith(path_):
2940 2943 return
2941 2944 ui.warn("%s: %s\n" % (m.rel(path), msg))
2942 2945
2943 2946 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
2944 2947 if abs not in names:
2945 2948 names[abs] = m.rel(abs), m.exact(abs)
2946 2949
2947 2950 # Find status of all file in `names`.
2948 2951 m = scmutil.matchfiles(repo, names)
2949 2952
2950 2953 changes = repo.status(node1=node, match=m,
2951 2954 unknown=True, ignored=True, clean=True)
2952 2955 else:
2953 2956 changes = repo.status(node1=node, match=m)
2954 2957 for kind in changes:
2955 2958 for abs in kind:
2956 2959 names[abs] = m.rel(abs), m.exact(abs)
2957 2960
2958 2961 m = scmutil.matchfiles(repo, names)
2959 2962
2960 2963 modified = set(changes.modified)
2961 2964 added = set(changes.added)
2962 2965 removed = set(changes.removed)
2963 2966 _deleted = set(changes.deleted)
2964 2967 unknown = set(changes.unknown)
2965 2968 unknown.update(changes.ignored)
2966 2969 clean = set(changes.clean)
2967 2970 modadded = set()
2968 2971
2969 2972 # We need to account for the state of the file in the dirstate,
2970 2973 # even when we revert against something else than parent. This will
2971 2974 # slightly alter the behavior of revert (doing back up or not, delete
2972 2975 # or just forget etc).
2973 2976 if parent == node:
2974 2977 dsmodified = modified
2975 2978 dsadded = added
2976 2979 dsremoved = removed
2977 2980 # store all local modifications, useful later for rename detection
2978 2981 localchanges = dsmodified | dsadded
2979 2982 modified, added, removed = set(), set(), set()
2980 2983 else:
2981 2984 changes = repo.status(node1=parent, match=m)
2982 2985 dsmodified = set(changes.modified)
2983 2986 dsadded = set(changes.added)
2984 2987 dsremoved = set(changes.removed)
2985 2988 # store all local modifications, useful later for rename detection
2986 2989 localchanges = dsmodified | dsadded
2987 2990
2988 2991 # only take into account for removes between wc and target
2989 2992 clean |= dsremoved - removed
2990 2993 dsremoved &= removed
2991 2994 # distinct between dirstate remove and other
2992 2995 removed -= dsremoved
2993 2996
2994 2997 modadded = added & dsmodified
2995 2998 added -= modadded
2996 2999
2997 3000 # tell newly modified apart.
2998 3001 dsmodified &= modified
2999 3002 dsmodified |= modified & dsadded # dirstate added may need backup
3000 3003 modified -= dsmodified
3001 3004
3002 3005 # We need to wait for some post-processing to update this set
3003 3006 # before making the distinction. The dirstate will be used for
3004 3007 # that purpose.
3005 3008 dsadded = added
3006 3009
3007 3010 # in case of merge, files that are actually added can be reported as
3008 3011 # modified, we need to post process the result
3009 3012 if p2 != nullid:
3010 3013 mergeadd = set(dsmodified)
3011 3014 for path in dsmodified:
3012 3015 if path in mf:
3013 3016 mergeadd.remove(path)
3014 3017 dsadded |= mergeadd
3015 3018 dsmodified -= mergeadd
3016 3019
3017 3020 # if f is a rename, update `names` to also revert the source
3018 3021 cwd = repo.getcwd()
3019 3022 for f in localchanges:
3020 3023 src = repo.dirstate.copied(f)
3021 3024 # XXX should we check for rename down to target node?
3022 3025 if src and src not in names and repo.dirstate[src] == 'r':
3023 3026 dsremoved.add(src)
3024 3027 names[src] = (repo.pathto(src, cwd), True)
3025 3028
3026 3029 # determine the exact nature of the deleted changesets
3027 3030 deladded = set(_deleted)
3028 3031 for path in _deleted:
3029 3032 if path in mf:
3030 3033 deladded.remove(path)
3031 3034 deleted = _deleted - deladded
3032 3035
3033 3036 # distinguish between file to forget and the other
3034 3037 added = set()
3035 3038 for abs in dsadded:
3036 3039 if repo.dirstate[abs] != 'a':
3037 3040 added.add(abs)
3038 3041 dsadded -= added
3039 3042
3040 3043 for abs in deladded:
3041 3044 if repo.dirstate[abs] == 'a':
3042 3045 dsadded.add(abs)
3043 3046 deladded -= dsadded
3044 3047
3045 3048 # For files marked as removed, we check if an unknown file is present at
3046 3049 # the same path. If a such file exists it may need to be backed up.
3047 3050 # Making the distinction at this stage helps have simpler backup
3048 3051 # logic.
3049 3052 removunk = set()
3050 3053 for abs in removed:
3051 3054 target = repo.wjoin(abs)
3052 3055 if os.path.lexists(target):
3053 3056 removunk.add(abs)
3054 3057 removed -= removunk
3055 3058
3056 3059 dsremovunk = set()
3057 3060 for abs in dsremoved:
3058 3061 target = repo.wjoin(abs)
3059 3062 if os.path.lexists(target):
3060 3063 dsremovunk.add(abs)
3061 3064 dsremoved -= dsremovunk
3062 3065
3063 3066 # action to be actually performed by revert
3064 3067 # (<list of file>, message>) tuple
3065 3068 actions = {'revert': ([], _('reverting %s\n')),
3066 3069 'add': ([], _('adding %s\n')),
3067 3070 'remove': ([], _('removing %s\n')),
3068 3071 'drop': ([], _('removing %s\n')),
3069 3072 'forget': ([], _('forgetting %s\n')),
3070 3073 'undelete': ([], _('undeleting %s\n')),
3071 3074 'noop': (None, _('no changes needed to %s\n')),
3072 3075 'unknown': (None, _('file not managed: %s\n')),
3073 3076 }
3074 3077
3075 3078 # "constant" that convey the backup strategy.
3076 3079 # All set to `discard` if `no-backup` is set do avoid checking
3077 3080 # no_backup lower in the code.
3078 3081 # These values are ordered for comparison purposes
3079 3082 backupinteractive = 3 # do backup if interactively modified
3080 3083 backup = 2 # unconditionally do backup
3081 3084 check = 1 # check if the existing file differs from target
3082 3085 discard = 0 # never do backup
3083 3086 if opts.get('no_backup'):
3084 3087 backupinteractive = backup = check = discard
3085 3088 if interactive:
3086 3089 dsmodifiedbackup = backupinteractive
3087 3090 else:
3088 3091 dsmodifiedbackup = backup
3089 3092 tobackup = set()
3090 3093
3091 3094 backupanddel = actions['remove']
3092 3095 if not opts.get('no_backup'):
3093 3096 backupanddel = actions['drop']
3094 3097
3095 3098 disptable = (
3096 3099 # dispatch table:
3097 3100 # file state
3098 3101 # action
3099 3102 # make backup
3100 3103
3101 3104 ## Sets that results that will change file on disk
3102 3105 # Modified compared to target, no local change
3103 3106 (modified, actions['revert'], discard),
3104 3107 # Modified compared to target, but local file is deleted
3105 3108 (deleted, actions['revert'], discard),
3106 3109 # Modified compared to target, local change
3107 3110 (dsmodified, actions['revert'], dsmodifiedbackup),
3108 3111 # Added since target
3109 3112 (added, actions['remove'], discard),
3110 3113 # Added in working directory
3111 3114 (dsadded, actions['forget'], discard),
3112 3115 # Added since target, have local modification
3113 3116 (modadded, backupanddel, backup),
3114 3117 # Added since target but file is missing in working directory
3115 3118 (deladded, actions['drop'], discard),
3116 3119 # Removed since target, before working copy parent
3117 3120 (removed, actions['add'], discard),
3118 3121 # Same as `removed` but an unknown file exists at the same path
3119 3122 (removunk, actions['add'], check),
3120 3123 # Removed since targe, marked as such in working copy parent
3121 3124 (dsremoved, actions['undelete'], discard),
3122 3125 # Same as `dsremoved` but an unknown file exists at the same path
3123 3126 (dsremovunk, actions['undelete'], check),
3124 3127 ## the following sets does not result in any file changes
3125 3128 # File with no modification
3126 3129 (clean, actions['noop'], discard),
3127 3130 # Existing file, not tracked anywhere
3128 3131 (unknown, actions['unknown'], discard),
3129 3132 )
3130 3133
3131 3134 for abs, (rel, exact) in sorted(names.items()):
3132 3135 # target file to be touch on disk (relative to cwd)
3133 3136 target = repo.wjoin(abs)
3134 3137 # search the entry in the dispatch table.
3135 3138 # if the file is in any of these sets, it was touched in the working
3136 3139 # directory parent and we are sure it needs to be reverted.
3137 3140 for table, (xlist, msg), dobackup in disptable:
3138 3141 if abs not in table:
3139 3142 continue
3140 3143 if xlist is not None:
3141 3144 xlist.append(abs)
3142 3145 if dobackup:
3143 3146 # If in interactive mode, don't automatically create
3144 3147 # .orig files (issue4793)
3145 3148 if dobackup == backupinteractive:
3146 3149 tobackup.add(abs)
3147 3150 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
3148 3151 bakname = scmutil.origpath(ui, repo, rel)
3149 3152 ui.note(_('saving current version of %s as %s\n') %
3150 3153 (rel, bakname))
3151 3154 if not opts.get('dry_run'):
3152 3155 if interactive:
3153 3156 util.copyfile(target, bakname)
3154 3157 else:
3155 3158 util.rename(target, bakname)
3156 3159 if ui.verbose or not exact:
3157 3160 if not isinstance(msg, basestring):
3158 3161 msg = msg(abs)
3159 3162 ui.status(msg % rel)
3160 3163 elif exact:
3161 3164 ui.warn(msg % rel)
3162 3165 break
3163 3166
3164 3167 if not opts.get('dry_run'):
3165 3168 needdata = ('revert', 'add', 'undelete')
3166 3169 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
3167 3170 _performrevert(repo, parents, ctx, actions, interactive, tobackup)
3168 3171
3169 3172 if targetsubs:
3170 3173 # Revert the subrepos on the revert list
3171 3174 for sub in targetsubs:
3172 3175 try:
3173 3176 wctx.sub(sub).revert(ctx.substate[sub], *pats, **opts)
3174 3177 except KeyError:
3175 3178 raise error.Abort("subrepository '%s' does not exist in %s!"
3176 3179 % (sub, short(ctx.node())))
3177 3180
3178 3181 def _revertprefetch(repo, ctx, *files):
3179 3182 """Let extension changing the storage layer prefetch content"""
3180 3183 pass
3181 3184
3182 3185 def _performrevert(repo, parents, ctx, actions, interactive=False,
3183 3186 tobackup=None):
3184 3187 """function that actually perform all the actions computed for revert
3185 3188
3186 3189 This is an independent function to let extension to plug in and react to
3187 3190 the imminent revert.
3188 3191
3189 3192 Make sure you have the working directory locked when calling this function.
3190 3193 """
3191 3194 parent, p2 = parents
3192 3195 node = ctx.node()
3193 3196 excluded_files = []
3194 3197 matcher_opts = {"exclude": excluded_files}
3195 3198
3196 3199 def checkout(f):
3197 3200 fc = ctx[f]
3198 3201 repo.wwrite(f, fc.data(), fc.flags())
3199 3202
3200 3203 def doremove(f):
3201 3204 try:
3202 3205 repo.wvfs.unlinkpath(f)
3203 3206 except OSError:
3204 3207 pass
3205 3208 repo.dirstate.remove(f)
3206 3209
3207 3210 audit_path = pathutil.pathauditor(repo.root)
3208 3211 for f in actions['forget'][0]:
3209 3212 if interactive:
3210 3213 choice = repo.ui.promptchoice(
3211 3214 _("forget added file %s (Yn)?$$ &Yes $$ &No") % f)
3212 3215 if choice == 0:
3213 3216 repo.dirstate.drop(f)
3214 3217 else:
3215 3218 excluded_files.append(repo.wjoin(f))
3216 3219 else:
3217 3220 repo.dirstate.drop(f)
3218 3221 for f in actions['remove'][0]:
3219 3222 audit_path(f)
3220 3223 if interactive:
3221 3224 choice = repo.ui.promptchoice(
3222 3225 _("remove added file %s (Yn)?$$ &Yes $$ &No") % f)
3223 3226 if choice == 0:
3224 3227 doremove(f)
3225 3228 else:
3226 3229 excluded_files.append(repo.wjoin(f))
3227 3230 else:
3228 3231 doremove(f)
3229 3232 for f in actions['drop'][0]:
3230 3233 audit_path(f)
3231 3234 repo.dirstate.remove(f)
3232 3235
3233 3236 normal = None
3234 3237 if node == parent:
3235 3238 # We're reverting to our parent. If possible, we'd like status
3236 3239 # to report the file as clean. We have to use normallookup for
3237 3240 # merges to avoid losing information about merged/dirty files.
3238 3241 if p2 != nullid:
3239 3242 normal = repo.dirstate.normallookup
3240 3243 else:
3241 3244 normal = repo.dirstate.normal
3242 3245
3243 3246 newlyaddedandmodifiedfiles = set()
3244 3247 if interactive:
3245 3248 # Prompt the user for changes to revert
3246 3249 torevert = [repo.wjoin(f) for f in actions['revert'][0]]
3247 3250 m = scmutil.match(ctx, torevert, matcher_opts)
3248 3251 diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
3249 3252 diffopts.nodates = True
3250 3253 diffopts.git = True
3251 3254 operation = 'discard'
3252 3255 reversehunks = True
3253 3256 if node != parent:
3254 3257 operation = 'revert'
3255 3258 reversehunks = repo.ui.configbool('experimental',
3256 3259 'revertalternateinteractivemode',
3257 3260 True)
3258 3261 if reversehunks:
3259 3262 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3260 3263 else:
3261 3264 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3262 3265 originalchunks = patch.parsepatch(diff)
3263 3266
3264 3267 try:
3265 3268
3266 3269 chunks, opts = recordfilter(repo.ui, originalchunks,
3267 3270 operation=operation)
3268 3271 if reversehunks:
3269 3272 chunks = patch.reversehunks(chunks)
3270 3273
3271 3274 except patch.PatchError as err:
3272 3275 raise error.Abort(_('error parsing patch: %s') % err)
3273 3276
3274 3277 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3275 3278 if tobackup is None:
3276 3279 tobackup = set()
3277 3280 # Apply changes
3278 3281 fp = stringio()
3279 3282 for c in chunks:
3280 3283 # Create a backup file only if this hunk should be backed up
3281 3284 if ishunk(c) and c.header.filename() in tobackup:
3282 3285 abs = c.header.filename()
3283 3286 target = repo.wjoin(abs)
3284 3287 bakname = scmutil.origpath(repo.ui, repo, m.rel(abs))
3285 3288 util.copyfile(target, bakname)
3286 3289 tobackup.remove(abs)
3287 3290 c.write(fp)
3288 3291 dopatch = fp.tell()
3289 3292 fp.seek(0)
3290 3293 if dopatch:
3291 3294 try:
3292 3295 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3293 3296 except patch.PatchError as err:
3294 3297 raise error.Abort(str(err))
3295 3298 del fp
3296 3299 else:
3297 3300 for f in actions['revert'][0]:
3298 3301 checkout(f)
3299 3302 if normal:
3300 3303 normal(f)
3301 3304
3302 3305 for f in actions['add'][0]:
3303 3306 # Don't checkout modified files, they are already created by the diff
3304 3307 if f not in newlyaddedandmodifiedfiles:
3305 3308 checkout(f)
3306 3309 repo.dirstate.add(f)
3307 3310
3308 3311 normal = repo.dirstate.normallookup
3309 3312 if node == parent and p2 == nullid:
3310 3313 normal = repo.dirstate.normal
3311 3314 for f in actions['undelete'][0]:
3312 3315 checkout(f)
3313 3316 normal(f)
3314 3317
3315 3318 copied = copies.pathcopies(repo[parent], ctx)
3316 3319
3317 3320 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3318 3321 if f in copied:
3319 3322 repo.dirstate.copy(copied[f], f)
3320 3323
3321 3324 def command(table):
3322 3325 """Returns a function object to be used as a decorator for making commands.
3323 3326
3324 3327 This function receives a command table as its argument. The table should
3325 3328 be a dict.
3326 3329
3327 3330 The returned function can be used as a decorator for adding commands
3328 3331 to that command table. This function accepts multiple arguments to define
3329 3332 a command.
3330 3333
3331 3334 The first argument is the command name.
3332 3335
3333 3336 The options argument is an iterable of tuples defining command arguments.
3334 3337 See ``mercurial.fancyopts.fancyopts()`` for the format of each tuple.
3335 3338
3336 3339 The synopsis argument defines a short, one line summary of how to use the
3337 3340 command. This shows up in the help output.
3338 3341
3339 3342 The norepo argument defines whether the command does not require a
3340 3343 local repository. Most commands operate against a repository, thus the
3341 3344 default is False.
3342 3345
3343 3346 The optionalrepo argument defines whether the command optionally requires
3344 3347 a local repository.
3345 3348
3346 3349 The inferrepo argument defines whether to try to find a repository from the
3347 3350 command line arguments. If True, arguments will be examined for potential
3348 3351 repository locations. See ``findrepo()``. If a repository is found, it
3349 3352 will be used.
3350 3353 """
3351 3354 def cmd(name, options=(), synopsis=None, norepo=False, optionalrepo=False,
3352 3355 inferrepo=False):
3353 3356 def decorator(func):
3354 3357 func.norepo = norepo
3355 3358 func.optionalrepo = optionalrepo
3356 3359 func.inferrepo = inferrepo
3357 3360 if synopsis:
3358 3361 table[name] = func, list(options), synopsis
3359 3362 else:
3360 3363 table[name] = func, list(options)
3361 3364 return func
3362 3365 return decorator
3363 3366
3364 3367 return cmd
3365 3368
3366 3369 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3367 3370 # commands.outgoing. "missing" is "missing" of the result of
3368 3371 # "findcommonoutgoing()"
3369 3372 outgoinghooks = util.hooks()
3370 3373
3371 3374 # a list of (ui, repo) functions called by commands.summary
3372 3375 summaryhooks = util.hooks()
3373 3376
3374 3377 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3375 3378 #
3376 3379 # functions should return tuple of booleans below, if 'changes' is None:
3377 3380 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3378 3381 #
3379 3382 # otherwise, 'changes' is a tuple of tuples below:
3380 3383 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3381 3384 # - (desturl, destbranch, destpeer, outgoing)
3382 3385 summaryremotehooks = util.hooks()
3383 3386
3384 3387 # A list of state files kept by multistep operations like graft.
3385 3388 # Since graft cannot be aborted, it is considered 'clearable' by update.
3386 3389 # note: bisect is intentionally excluded
3387 3390 # (state file, clearable, allowcommit, error, hint)
3388 3391 unfinishedstates = [
3389 3392 ('graftstate', True, False, _('graft in progress'),
3390 3393 _("use 'hg graft --continue' or 'hg update' to abort")),
3391 3394 ('updatestate', True, False, _('last update was interrupted'),
3392 3395 _("use 'hg update' to get a consistent checkout"))
3393 3396 ]
3394 3397
3395 3398 def checkunfinished(repo, commit=False):
3396 3399 '''Look for an unfinished multistep operation, like graft, and abort
3397 3400 if found. It's probably good to check this right before
3398 3401 bailifchanged().
3399 3402 '''
3400 3403 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3401 3404 if commit and allowcommit:
3402 3405 continue
3403 3406 if repo.vfs.exists(f):
3404 3407 raise error.Abort(msg, hint=hint)
3405 3408
3406 3409 def clearunfinished(repo):
3407 3410 '''Check for unfinished operations (as above), and clear the ones
3408 3411 that are clearable.
3409 3412 '''
3410 3413 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3411 3414 if not clearable and repo.vfs.exists(f):
3412 3415 raise error.Abort(msg, hint=hint)
3413 3416 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3414 3417 if clearable and repo.vfs.exists(f):
3415 3418 util.unlink(repo.vfs.join(f))
3416 3419
3417 3420 afterresolvedstates = [
3418 3421 ('graftstate',
3419 3422 _('hg graft --continue')),
3420 3423 ]
3421 3424
3422 3425 def howtocontinue(repo):
3423 3426 '''Check for an unfinished operation and return the command to finish
3424 3427 it.
3425 3428
3426 3429 afterresolvedstates tuples define a .hg/{file} and the corresponding
3427 3430 command needed to finish it.
3428 3431
3429 3432 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3430 3433 a boolean.
3431 3434 '''
3432 3435 contmsg = _("continue: %s")
3433 3436 for f, msg in afterresolvedstates:
3434 3437 if repo.vfs.exists(f):
3435 3438 return contmsg % msg, True
3436 3439 workingctx = repo[None]
3437 3440 dirty = any(repo.status()) or any(workingctx.sub(s).dirty()
3438 3441 for s in workingctx.substate)
3439 3442 if dirty:
3440 3443 return contmsg % _("hg commit"), False
3441 3444 return None, None
3442 3445
3443 3446 def checkafterresolved(repo):
3444 3447 '''Inform the user about the next action after completing hg resolve
3445 3448
3446 3449 If there's a matching afterresolvedstates, howtocontinue will yield
3447 3450 repo.ui.warn as the reporter.
3448 3451
3449 3452 Otherwise, it will yield repo.ui.note.
3450 3453 '''
3451 3454 msg, warning = howtocontinue(repo)
3452 3455 if msg is not None:
3453 3456 if warning:
3454 3457 repo.ui.warn("%s\n" % msg)
3455 3458 else:
3456 3459 repo.ui.note("%s\n" % msg)
3457 3460
3458 3461 def wrongtooltocontinue(repo, task):
3459 3462 '''Raise an abort suggesting how to properly continue if there is an
3460 3463 active task.
3461 3464
3462 3465 Uses howtocontinue() to find the active task.
3463 3466
3464 3467 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3465 3468 a hint.
3466 3469 '''
3467 3470 after = howtocontinue(repo)
3468 3471 hint = None
3469 3472 if after[1]:
3470 3473 hint = after[0]
3471 3474 raise error.Abort(_('no %s in progress') % task, hint=hint)
@@ -1,426 +1,429
1 1 # formatter.py - generic output formatting for mercurial
2 2 #
3 3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """Generic output formatting for Mercurial
9 9
10 10 The formatter provides API to show data in various ways. The following
11 11 functions should be used in place of ui.write():
12 12
13 13 - fm.write() for unconditional output
14 14 - fm.condwrite() to show some extra data conditionally in plain output
15 15 - fm.context() to provide changectx to template output
16 16 - fm.data() to provide extra data to JSON or template output
17 17 - fm.plain() to show raw text that isn't provided to JSON or template output
18 18
19 19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 20 beforehand so the data is converted to the appropriate data type. Use
21 21 fm.isplain() if you need to convert or format data conditionally which isn't
22 22 supported by the formatter API.
23 23
24 24 To build nested structure (i.e. a list of dicts), use fm.nested().
25 25
26 26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27 27
28 28 fm.condwrite() vs 'if cond:':
29 29
30 30 In most cases, use fm.condwrite() so users can selectively show the data
31 31 in template output. If it's costly to build data, use plain 'if cond:' with
32 32 fm.write().
33 33
34 34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35 35
36 36 fm.nested() should be used to form a tree structure (a list of dicts of
37 37 lists of dicts...) which can be accessed through template keywords, e.g.
38 38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 39 exports a dict-type object to template, which can be accessed by e.g.
40 40 "{get(foo, key)}" function.
41 41
42 42 Doctest helper:
43 43
44 44 >>> def show(fn, verbose=False, **opts):
45 45 ... import sys
46 46 ... from . import ui as uimod
47 47 ... ui = uimod.ui()
48 48 ... ui.fout = sys.stdout # redirect to doctest
49 49 ... ui.verbose = verbose
50 50 ... return fn(ui, ui.formatter(fn.__name__, opts))
51 51
52 52 Basic example:
53 53
54 54 >>> def files(ui, fm):
55 55 ... files = [('foo', 123, (0, 0)), ('bar', 456, (1, 0))]
56 56 ... for f in files:
57 57 ... fm.startitem()
58 58 ... fm.write('path', '%s', f[0])
59 59 ... fm.condwrite(ui.verbose, 'date', ' %s',
60 60 ... fm.formatdate(f[2], '%Y-%m-%d %H:%M:%S'))
61 61 ... fm.data(size=f[1])
62 62 ... fm.plain('\\n')
63 63 ... fm.end()
64 64 >>> show(files)
65 65 foo
66 66 bar
67 67 >>> show(files, verbose=True)
68 68 foo 1970-01-01 00:00:00
69 69 bar 1970-01-01 00:00:01
70 70 >>> show(files, template='json')
71 71 [
72 72 {
73 73 "date": [0, 0],
74 74 "path": "foo",
75 75 "size": 123
76 76 },
77 77 {
78 78 "date": [1, 0],
79 79 "path": "bar",
80 80 "size": 456
81 81 }
82 82 ]
83 83 >>> show(files, template='path: {path}\\ndate: {date|rfc3339date}\\n')
84 84 path: foo
85 85 date: 1970-01-01T00:00:00+00:00
86 86 path: bar
87 87 date: 1970-01-01T00:00:01+00:00
88 88
89 89 Nested example:
90 90
91 91 >>> def subrepos(ui, fm):
92 92 ... fm.startitem()
93 93 ... fm.write('repo', '[%s]\\n', 'baz')
94 94 ... files(ui, fm.nested('files'))
95 95 ... fm.end()
96 96 >>> show(subrepos)
97 97 [baz]
98 98 foo
99 99 bar
100 100 >>> show(subrepos, template='{repo}: {join(files % "{path}", ", ")}\\n')
101 101 baz: foo, bar
102 102 """
103 103
104 104 from __future__ import absolute_import
105 105
106 import itertools
106 107 import os
107 108
108 109 from .i18n import _
109 110 from .node import (
110 111 hex,
111 112 short,
112 113 )
113 114
114 115 from . import (
115 116 error,
116 117 templatefilters,
117 118 templatekw,
118 119 templater,
119 120 util,
120 121 )
121 122
122 123 pickle = util.pickle
123 124
124 125 class _nullconverter(object):
125 126 '''convert non-primitive data types to be processed by formatter'''
126 127 @staticmethod
127 128 def formatdate(date, fmt):
128 129 '''convert date tuple to appropriate format'''
129 130 return date
130 131 @staticmethod
131 132 def formatdict(data, key, value, fmt, sep):
132 133 '''convert dict or key-value pairs to appropriate dict format'''
133 134 # use plain dict instead of util.sortdict so that data can be
134 135 # serialized as a builtin dict in pickle output
135 136 return dict(data)
136 137 @staticmethod
137 138 def formatlist(data, name, fmt, sep):
138 139 '''convert iterable to appropriate list format'''
139 140 return list(data)
140 141
141 142 class baseformatter(object):
142 143 def __init__(self, ui, topic, opts, converter):
143 144 self._ui = ui
144 145 self._topic = topic
145 146 self._style = opts.get("style")
146 147 self._template = opts.get("template")
147 148 self._converter = converter
148 149 self._item = None
149 150 # function to convert node to string suitable for this output
150 151 self.hexfunc = hex
151 152 def __enter__(self):
152 153 return self
153 154 def __exit__(self, exctype, excvalue, traceback):
154 155 if exctype is None:
155 156 self.end()
156 157 def _showitem(self):
157 158 '''show a formatted item once all data is collected'''
158 159 pass
159 160 def startitem(self):
160 161 '''begin an item in the format list'''
161 162 if self._item is not None:
162 163 self._showitem()
163 164 self._item = {}
164 165 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
165 166 '''convert date tuple to appropriate format'''
166 167 return self._converter.formatdate(date, fmt)
167 168 def formatdict(self, data, key='key', value='value', fmt='%s=%s', sep=' '):
168 169 '''convert dict or key-value pairs to appropriate dict format'''
169 170 return self._converter.formatdict(data, key, value, fmt, sep)
170 171 def formatlist(self, data, name, fmt='%s', sep=' '):
171 172 '''convert iterable to appropriate list format'''
172 173 # name is mandatory argument for now, but it could be optional if
173 174 # we have default template keyword, e.g. {item}
174 175 return self._converter.formatlist(data, name, fmt, sep)
175 176 def context(self, **ctxs):
176 177 '''insert context objects to be used to render template keywords'''
177 178 pass
178 179 def data(self, **data):
179 180 '''insert data into item that's not shown in default output'''
180 181 self._item.update(data)
181 182 def write(self, fields, deftext, *fielddata, **opts):
182 183 '''do default text output while assigning data to item'''
183 184 fieldkeys = fields.split()
184 185 assert len(fieldkeys) == len(fielddata)
185 186 self._item.update(zip(fieldkeys, fielddata))
186 187 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
187 188 '''do conditional write (primarily for plain formatter)'''
188 189 fieldkeys = fields.split()
189 190 assert len(fieldkeys) == len(fielddata)
190 191 self._item.update(zip(fieldkeys, fielddata))
191 192 def plain(self, text, **opts):
192 193 '''show raw text for non-templated mode'''
193 194 pass
194 195 def isplain(self):
195 196 '''check for plain formatter usage'''
196 197 return False
197 198 def nested(self, field):
198 199 '''sub formatter to store nested data in the specified field'''
199 200 self._item[field] = data = []
200 201 return _nestedformatter(self._ui, self._converter, data)
201 202 def end(self):
202 203 '''end output for the formatter'''
203 204 if self._item is not None:
204 205 self._showitem()
205 206
206 207 class _nestedformatter(baseformatter):
207 208 '''build sub items and store them in the parent formatter'''
208 209 def __init__(self, ui, converter, data):
209 210 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
210 211 self._data = data
211 212 def _showitem(self):
212 213 self._data.append(self._item)
213 214
214 215 def _iteritems(data):
215 216 '''iterate key-value pairs in stable order'''
216 217 if isinstance(data, dict):
217 218 return sorted(data.iteritems())
218 219 return data
219 220
220 221 class _plainconverter(object):
221 222 '''convert non-primitive data types to text'''
222 223 @staticmethod
223 224 def formatdate(date, fmt):
224 225 '''stringify date tuple in the given format'''
225 226 return util.datestr(date, fmt)
226 227 @staticmethod
227 228 def formatdict(data, key, value, fmt, sep):
228 229 '''stringify key-value pairs separated by sep'''
229 230 return sep.join(fmt % (k, v) for k, v in _iteritems(data))
230 231 @staticmethod
231 232 def formatlist(data, name, fmt, sep):
232 233 '''stringify iterable separated by sep'''
233 234 return sep.join(fmt % e for e in data)
234 235
235 236 class plainformatter(baseformatter):
236 237 '''the default text output scheme'''
237 238 def __init__(self, ui, topic, opts):
238 239 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
239 240 if ui.debugflag:
240 241 self.hexfunc = hex
241 242 else:
242 243 self.hexfunc = short
243 244 def startitem(self):
244 245 pass
245 246 def data(self, **data):
246 247 pass
247 248 def write(self, fields, deftext, *fielddata, **opts):
248 249 self._ui.write(deftext % fielddata, **opts)
249 250 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
250 251 '''do conditional write'''
251 252 if cond:
252 253 self._ui.write(deftext % fielddata, **opts)
253 254 def plain(self, text, **opts):
254 255 self._ui.write(text, **opts)
255 256 def isplain(self):
256 257 return True
257 258 def nested(self, field):
258 259 # nested data will be directly written to ui
259 260 return self
260 261 def end(self):
261 262 pass
262 263
263 264 class debugformatter(baseformatter):
264 265 def __init__(self, ui, out, topic, opts):
265 266 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
266 267 self._out = out
267 268 self._out.write("%s = [\n" % self._topic)
268 269 def _showitem(self):
269 270 self._out.write(" " + repr(self._item) + ",\n")
270 271 def end(self):
271 272 baseformatter.end(self)
272 273 self._out.write("]\n")
273 274
274 275 class pickleformatter(baseformatter):
275 276 def __init__(self, ui, out, topic, opts):
276 277 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
277 278 self._out = out
278 279 self._data = []
279 280 def _showitem(self):
280 281 self._data.append(self._item)
281 282 def end(self):
282 283 baseformatter.end(self)
283 284 self._out.write(pickle.dumps(self._data))
284 285
285 286 class jsonformatter(baseformatter):
286 287 def __init__(self, ui, out, topic, opts):
287 288 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
288 289 self._out = out
289 290 self._out.write("[")
290 291 self._first = True
291 292 def _showitem(self):
292 293 if self._first:
293 294 self._first = False
294 295 else:
295 296 self._out.write(",")
296 297
297 298 self._out.write("\n {\n")
298 299 first = True
299 300 for k, v in sorted(self._item.items()):
300 301 if first:
301 302 first = False
302 303 else:
303 304 self._out.write(",\n")
304 305 u = templatefilters.json(v, paranoid=False)
305 306 self._out.write(' "%s": %s' % (k, u))
306 307 self._out.write("\n }")
307 308 def end(self):
308 309 baseformatter.end(self)
309 310 self._out.write("\n]\n")
310 311
311 312 class _templateconverter(object):
312 313 '''convert non-primitive data types to be processed by templater'''
313 314 @staticmethod
314 315 def formatdate(date, fmt):
315 316 '''return date tuple'''
316 317 return date
317 318 @staticmethod
318 319 def formatdict(data, key, value, fmt, sep):
319 320 '''build object that can be evaluated as either plain string or dict'''
320 321 data = util.sortdict(_iteritems(data))
321 322 def f():
322 323 yield _plainconverter.formatdict(data, key, value, fmt, sep)
323 324 return templatekw._hybrid(f(), data, lambda k: {key: k, value: data[k]},
324 325 lambda d: fmt % (d[key], d[value]))
325 326 @staticmethod
326 327 def formatlist(data, name, fmt, sep):
327 328 '''build object that can be evaluated as either plain string or list'''
328 329 data = list(data)
329 330 def f():
330 331 yield _plainconverter.formatlist(data, name, fmt, sep)
331 332 return templatekw._hybrid(f(), data, lambda x: {name: x},
332 333 lambda d: fmt % d[name])
333 334
334 335 class templateformatter(baseformatter):
335 336 def __init__(self, ui, out, topic, opts):
336 337 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
337 338 self._out = out
338 339 self._topic = topic
339 340 self._t = gettemplater(ui, topic, opts.get('template', ''),
340 341 cache=templatekw.defaulttempl)
342 self._counter = itertools.count()
341 343 self._cache = {} # for templatekw/funcs to store reusable data
342 344 def context(self, **ctxs):
343 345 '''insert context objects to be used to render template keywords'''
344 346 assert all(k == 'ctx' for k in ctxs)
345 347 self._item.update(ctxs)
346 348 def _showitem(self):
347 349 # TODO: add support for filectx. probably each template keyword or
348 350 # function will have to declare dependent resources. e.g.
349 351 # @templatekeyword(..., requires=('ctx',))
350 352 props = {}
351 353 if 'ctx' in self._item:
352 354 props.update(templatekw.keywords)
355 props['index'] = next(self._counter)
353 356 # explicitly-defined fields precede templatekw
354 357 props.update(self._item)
355 358 if 'ctx' in self._item:
356 359 # but template resources must be always available
357 360 props['templ'] = self._t
358 361 props['repo'] = props['ctx'].repo()
359 362 props['revcache'] = {}
360 363 g = self._t(self._topic, ui=self._ui, cache=self._cache, **props)
361 364 self._out.write(templater.stringify(g))
362 365
363 366 def lookuptemplate(ui, topic, tmpl):
364 367 # looks like a literal template?
365 368 if '{' in tmpl:
366 369 return tmpl, None
367 370
368 371 # perhaps a stock style?
369 372 if not os.path.split(tmpl)[0]:
370 373 mapname = (templater.templatepath('map-cmdline.' + tmpl)
371 374 or templater.templatepath(tmpl))
372 375 if mapname and os.path.isfile(mapname):
373 376 return None, mapname
374 377
375 378 # perhaps it's a reference to [templates]
376 379 t = ui.config('templates', tmpl)
377 380 if t:
378 381 return templater.unquotestring(t), None
379 382
380 383 if tmpl == 'list':
381 384 ui.write(_("available styles: %s\n") % templater.stylelist())
382 385 raise error.Abort(_("specify a template"))
383 386
384 387 # perhaps it's a path to a map or a template
385 388 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
386 389 # is it a mapfile for a style?
387 390 if os.path.basename(tmpl).startswith("map-"):
388 391 return None, os.path.realpath(tmpl)
389 392 tmpl = open(tmpl).read()
390 393 return tmpl, None
391 394
392 395 # constant string?
393 396 return tmpl, None
394 397
395 398 def gettemplater(ui, topic, spec, cache=None):
396 399 tmpl, mapfile = lookuptemplate(ui, topic, spec)
397 400 assert not (tmpl and mapfile)
398 401 if mapfile:
399 402 return templater.templater.frommapfile(mapfile, cache=cache)
400 403 return maketemplater(ui, topic, tmpl, cache=cache)
401 404
402 405 def maketemplater(ui, topic, tmpl, cache=None):
403 406 """Create a templater from a string template 'tmpl'"""
404 407 aliases = ui.configitems('templatealias')
405 408 t = templater.templater(cache=cache, aliases=aliases)
406 409 if tmpl:
407 410 t.cache[topic] = tmpl
408 411 return t
409 412
410 413 def formatter(ui, topic, opts):
411 414 template = opts.get("template", "")
412 415 if template == "json":
413 416 return jsonformatter(ui, ui, topic, opts)
414 417 elif template == "pickle":
415 418 return pickleformatter(ui, ui, topic, opts)
416 419 elif template == "debug":
417 420 return debugformatter(ui, ui, topic, opts)
418 421 elif template != "":
419 422 return templateformatter(ui, ui, topic, opts)
420 423 # developer config: ui.formatdebug
421 424 elif ui.configbool('ui', 'formatdebug'):
422 425 return debugformatter(ui, ui, topic, opts)
423 426 # deprecated config: ui.formatjson
424 427 elif ui.configbool('ui', 'formatjson'):
425 428 return jsonformatter(ui, ui, topic, opts)
426 429 return plainformatter(ui, topic, opts)
@@ -1,638 +1,645
1 1 # templatekw.py - common changeset template keywords
2 2 #
3 3 # Copyright 2005-2009 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 from .i18n import _
10 11 from .node import hex, nullid
11 12 from . import (
12 13 encoding,
13 14 error,
14 15 hbisect,
15 16 patch,
16 17 registrar,
17 18 scmutil,
18 19 util,
19 20 )
20 21
21 22 # This helper class allows us to handle both:
22 23 # "{files}" (legacy command-line-specific list hack) and
23 24 # "{files % '{file}\n'}" (hgweb-style with inlining and function support)
24 25 # and to access raw values:
25 26 # "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
26 27 # "{get(extras, key)}"
27 28
28 29 class _hybrid(object):
29 30 def __init__(self, gen, values, makemap, joinfmt):
30 31 self.gen = gen
31 32 self.values = values
32 33 self._makemap = makemap
33 34 self.joinfmt = joinfmt
34 35 def __iter__(self):
35 36 return self.gen
36 37 def itermaps(self):
37 38 makemap = self._makemap
38 39 for x in self.values:
39 40 yield makemap(x)
40 41 def __contains__(self, x):
41 42 return x in self.values
42 43 def __len__(self):
43 44 return len(self.values)
44 45 def __getattr__(self, name):
45 46 if name != 'get':
46 47 raise AttributeError(name)
47 48 return getattr(self.values, name)
48 49
49 50 def showlist(name, values, plural=None, element=None, separator=' ', **args):
50 51 if not element:
51 52 element = name
52 53 f = _showlist(name, values, plural, separator, **args)
53 54 return _hybrid(f, values, lambda x: {element: x}, lambda d: d[element])
54 55
55 56 def _showlist(name, values, plural=None, separator=' ', **args):
56 57 '''expand set of values.
57 58 name is name of key in template map.
58 59 values is list of strings or dicts.
59 60 plural is plural of name, if not simply name + 's'.
60 61 separator is used to join values as a string
61 62
62 63 expansion works like this, given name 'foo'.
63 64
64 65 if values is empty, expand 'no_foos'.
65 66
66 67 if 'foo' not in template map, return values as a string,
67 68 joined by 'separator'.
68 69
69 70 expand 'start_foos'.
70 71
71 72 for each value, expand 'foo'. if 'last_foo' in template
72 73 map, expand it instead of 'foo' for last key.
73 74
74 75 expand 'end_foos'.
75 76 '''
76 77 templ = args['templ']
77 78 if plural:
78 79 names = plural
79 80 else: names = name + 's'
80 81 if not values:
81 82 noname = 'no_' + names
82 83 if noname in templ:
83 84 yield templ(noname, **args)
84 85 return
85 86 if name not in templ:
86 87 if isinstance(values[0], str):
87 88 yield separator.join(values)
88 89 else:
89 90 for v in values:
90 91 yield dict(v, **args)
91 92 return
92 93 startname = 'start_' + names
93 94 if startname in templ:
94 95 yield templ(startname, **args)
95 96 vargs = args.copy()
96 97 def one(v, tag=name):
97 98 try:
98 99 vargs.update(v)
99 100 except (AttributeError, ValueError):
100 101 try:
101 102 for a, b in v:
102 103 vargs[a] = b
103 104 except ValueError:
104 105 vargs[name] = v
105 106 return templ(tag, **vargs)
106 107 lastname = 'last_' + name
107 108 if lastname in templ:
108 109 last = values.pop()
109 110 else:
110 111 last = None
111 112 for v in values:
112 113 yield one(v)
113 114 if last is not None:
114 115 yield one(last, tag=lastname)
115 116 endname = 'end_' + names
116 117 if endname in templ:
117 118 yield templ(endname, **args)
118 119
119 120 def _formatrevnode(ctx):
120 121 """Format changeset as '{rev}:{node|formatnode}', which is the default
121 122 template provided by cmdutil.changeset_templater"""
122 123 repo = ctx.repo()
123 124 if repo.ui.debugflag:
124 125 hexnode = ctx.hex()
125 126 else:
126 127 hexnode = ctx.hex()[:12]
127 128 return '%d:%s' % (scmutil.intrev(ctx.rev()), hexnode)
128 129
129 130 def getfiles(repo, ctx, revcache):
130 131 if 'files' not in revcache:
131 132 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
132 133 return revcache['files']
133 134
134 135 def getlatesttags(repo, ctx, cache, pattern=None):
135 136 '''return date, distance and name for the latest tag of rev'''
136 137
137 138 cachename = 'latesttags'
138 139 if pattern is not None:
139 140 cachename += '-' + pattern
140 141 match = util.stringmatcher(pattern)[2]
141 142 else:
142 143 match = util.always
143 144
144 145 if cachename not in cache:
145 146 # Cache mapping from rev to a tuple with tag date, tag
146 147 # distance and tag name
147 148 cache[cachename] = {-1: (0, 0, ['null'])}
148 149 latesttags = cache[cachename]
149 150
150 151 rev = ctx.rev()
151 152 todo = [rev]
152 153 while todo:
153 154 rev = todo.pop()
154 155 if rev in latesttags:
155 156 continue
156 157 ctx = repo[rev]
157 158 tags = [t for t in ctx.tags()
158 159 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
159 160 and match(t))]
160 161 if tags:
161 162 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
162 163 continue
163 164 try:
164 165 # The tuples are laid out so the right one can be found by
165 166 # comparison.
166 167 pdate, pdist, ptag = max(
167 168 latesttags[p.rev()] for p in ctx.parents())
168 169 except KeyError:
169 170 # Cache miss - recurse
170 171 todo.append(rev)
171 172 todo.extend(p.rev() for p in ctx.parents())
172 173 continue
173 174 latesttags[rev] = pdate, pdist + 1, ptag
174 175 return latesttags[rev]
175 176
176 177 def getrenamedfn(repo, endrev=None):
177 178 rcache = {}
178 179 if endrev is None:
179 180 endrev = len(repo)
180 181
181 182 def getrenamed(fn, rev):
182 183 '''looks up all renames for a file (up to endrev) the first
183 184 time the file is given. It indexes on the changerev and only
184 185 parses the manifest if linkrev != changerev.
185 186 Returns rename info for fn at changerev rev.'''
186 187 if fn not in rcache:
187 188 rcache[fn] = {}
188 189 fl = repo.file(fn)
189 190 for i in fl:
190 191 lr = fl.linkrev(i)
191 192 renamed = fl.renamed(fl.node(i))
192 193 rcache[fn][lr] = renamed
193 194 if lr >= endrev:
194 195 break
195 196 if rev in rcache[fn]:
196 197 return rcache[fn][rev]
197 198
198 199 # If linkrev != rev (i.e. rev not found in rcache) fallback to
199 200 # filectx logic.
200 201 try:
201 202 return repo[rev][fn].renamed()
202 203 except error.LookupError:
203 204 return None
204 205
205 206 return getrenamed
206 207
207 208 # default templates internally used for rendering of lists
208 209 defaulttempl = {
209 210 'parent': '{rev}:{node|formatnode} ',
210 211 'manifest': '{rev}:{node|formatnode}',
211 212 'file_copy': '{name} ({source})',
212 213 'envvar': '{key}={value}',
213 214 'extra': '{key}={value|stringescape}'
214 215 }
215 216 # filecopy is preserved for compatibility reasons
216 217 defaulttempl['filecopy'] = defaulttempl['file_copy']
217 218
218 219 # keywords are callables like:
219 220 # fn(repo, ctx, templ, cache, revcache, **args)
220 221 # with:
221 222 # repo - current repository instance
222 223 # ctx - the changectx being displayed
223 224 # templ - the templater instance
224 225 # cache - a cache dictionary for the whole templater run
225 226 # revcache - a cache dictionary for the current revision
226 227 keywords = {}
227 228
228 229 templatekeyword = registrar.templatekeyword(keywords)
229 230
230 231 @templatekeyword('author')
231 232 def showauthor(repo, ctx, templ, **args):
232 233 """String. The unmodified author of the changeset."""
233 234 return ctx.user()
234 235
235 236 @templatekeyword('bisect')
236 237 def showbisect(repo, ctx, templ, **args):
237 238 """String. The changeset bisection status."""
238 239 return hbisect.label(repo, ctx.node())
239 240
240 241 @templatekeyword('branch')
241 242 def showbranch(**args):
242 243 """String. The name of the branch on which the changeset was
243 244 committed.
244 245 """
245 246 return args['ctx'].branch()
246 247
247 248 @templatekeyword('branches')
248 249 def showbranches(**args):
249 250 """List of strings. The name of the branch on which the
250 251 changeset was committed. Will be empty if the branch name was
251 252 default. (DEPRECATED)
252 253 """
253 254 branch = args['ctx'].branch()
254 255 if branch != 'default':
255 256 return showlist('branch', [branch], plural='branches', **args)
256 257 return showlist('branch', [], plural='branches', **args)
257 258
258 259 @templatekeyword('bookmarks')
259 260 def showbookmarks(**args):
260 261 """List of strings. Any bookmarks associated with the
261 262 changeset. Also sets 'active', the name of the active bookmark.
262 263 """
263 264 repo = args['ctx']._repo
264 265 bookmarks = args['ctx'].bookmarks()
265 266 active = repo._activebookmark
266 267 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
267 268 f = _showlist('bookmark', bookmarks, **args)
268 269 return _hybrid(f, bookmarks, makemap, lambda x: x['bookmark'])
269 270
270 271 @templatekeyword('children')
271 272 def showchildren(**args):
272 273 """List of strings. The children of the changeset."""
273 274 ctx = args['ctx']
274 275 childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
275 276 return showlist('children', childrevs, element='child', **args)
276 277
277 278 # Deprecated, but kept alive for help generation a purpose.
278 279 @templatekeyword('currentbookmark')
279 280 def showcurrentbookmark(**args):
280 281 """String. The active bookmark, if it is
281 282 associated with the changeset (DEPRECATED)"""
282 283 return showactivebookmark(**args)
283 284
284 285 @templatekeyword('activebookmark')
285 286 def showactivebookmark(**args):
286 287 """String. The active bookmark, if it is
287 288 associated with the changeset"""
288 289 active = args['repo']._activebookmark
289 290 if active and active in args['ctx'].bookmarks():
290 291 return active
291 292 return ''
292 293
293 294 @templatekeyword('date')
294 295 def showdate(repo, ctx, templ, **args):
295 296 """Date information. The date when the changeset was committed."""
296 297 return ctx.date()
297 298
298 299 @templatekeyword('desc')
299 300 def showdescription(repo, ctx, templ, **args):
300 301 """String. The text of the changeset description."""
301 302 s = ctx.description()
302 303 if isinstance(s, encoding.localstr):
303 304 # try hard to preserve utf-8 bytes
304 305 return encoding.tolocal(encoding.fromlocal(s).strip())
305 306 else:
306 307 return s.strip()
307 308
308 309 @templatekeyword('diffstat')
309 310 def showdiffstat(repo, ctx, templ, **args):
310 311 """String. Statistics of changes with the following format:
311 312 "modified files: +added/-removed lines"
312 313 """
313 314 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
314 315 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
315 316 return '%s: +%s/-%s' % (len(stats), adds, removes)
316 317
317 318 @templatekeyword('envvars')
318 319 def showenvvars(repo, **args):
319 320 """A dictionary of environment variables. (EXPERIMENTAL)"""
320 321
321 322 env = repo.ui.exportableenviron()
322 323 env = util.sortdict((k, env[k]) for k in sorted(env))
323 324 makemap = lambda k: {'key': k, 'value': env[k]}
324 325 c = [makemap(k) for k in env]
325 326 f = _showlist('envvar', c, plural='envvars', **args)
326 327 return _hybrid(f, env, makemap,
327 328 lambda x: '%s=%s' % (x['key'], x['value']))
328 329
329 330 @templatekeyword('extras')
330 331 def showextras(**args):
331 332 """List of dicts with key, value entries of the 'extras'
332 333 field of this changeset."""
333 334 extras = args['ctx'].extra()
334 335 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
335 336 makemap = lambda k: {'key': k, 'value': extras[k]}
336 337 c = [makemap(k) for k in extras]
337 338 f = _showlist('extra', c, plural='extras', **args)
338 339 return _hybrid(f, extras, makemap,
339 340 lambda x: '%s=%s' % (x['key'], util.escapestr(x['value'])))
340 341
341 342 @templatekeyword('file_adds')
342 343 def showfileadds(**args):
343 344 """List of strings. Files added by this changeset."""
344 345 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
345 346 return showlist('file_add', getfiles(repo, ctx, revcache)[1],
346 347 element='file', **args)
347 348
348 349 @templatekeyword('file_copies')
349 350 def showfilecopies(**args):
350 351 """List of strings. Files copied in this changeset with
351 352 their sources.
352 353 """
353 354 cache, ctx = args['cache'], args['ctx']
354 355 copies = args['revcache'].get('copies')
355 356 if copies is None:
356 357 if 'getrenamed' not in cache:
357 358 cache['getrenamed'] = getrenamedfn(args['repo'])
358 359 copies = []
359 360 getrenamed = cache['getrenamed']
360 361 for fn in ctx.files():
361 362 rename = getrenamed(fn, ctx.rev())
362 363 if rename:
363 364 copies.append((fn, rename[0]))
364 365
365 366 copies = util.sortdict(copies)
366 367 makemap = lambda k: {'name': k, 'source': copies[k]}
367 368 c = [makemap(k) for k in copies]
368 369 f = _showlist('file_copy', c, plural='file_copies', **args)
369 370 return _hybrid(f, copies, makemap,
370 371 lambda x: '%s (%s)' % (x['name'], x['source']))
371 372
372 373 # showfilecopiesswitch() displays file copies only if copy records are
373 374 # provided before calling the templater, usually with a --copies
374 375 # command line switch.
375 376 @templatekeyword('file_copies_switch')
376 377 def showfilecopiesswitch(**args):
377 378 """List of strings. Like "file_copies" but displayed
378 379 only if the --copied switch is set.
379 380 """
380 381 copies = args['revcache'].get('copies') or []
381 382 copies = util.sortdict(copies)
382 383 makemap = lambda k: {'name': k, 'source': copies[k]}
383 384 c = [makemap(k) for k in copies]
384 385 f = _showlist('file_copy', c, plural='file_copies', **args)
385 386 return _hybrid(f, copies, makemap,
386 387 lambda x: '%s (%s)' % (x['name'], x['source']))
387 388
388 389 @templatekeyword('file_dels')
389 390 def showfiledels(**args):
390 391 """List of strings. Files removed by this changeset."""
391 392 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
392 393 return showlist('file_del', getfiles(repo, ctx, revcache)[2],
393 394 element='file', **args)
394 395
395 396 @templatekeyword('file_mods')
396 397 def showfilemods(**args):
397 398 """List of strings. Files modified by this changeset."""
398 399 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
399 400 return showlist('file_mod', getfiles(repo, ctx, revcache)[0],
400 401 element='file', **args)
401 402
402 403 @templatekeyword('files')
403 404 def showfiles(**args):
404 405 """List of strings. All files modified, added, or removed by this
405 406 changeset.
406 407 """
407 408 return showlist('file', args['ctx'].files(), **args)
408 409
409 410 @templatekeyword('graphnode')
410 411 def showgraphnode(repo, ctx, **args):
411 412 """String. The character representing the changeset node in
412 413 an ASCII revision graph"""
413 414 wpnodes = repo.dirstate.parents()
414 415 if wpnodes[1] == nullid:
415 416 wpnodes = wpnodes[:1]
416 417 if ctx.node() in wpnodes:
417 418 return '@'
418 419 elif ctx.obsolete():
419 420 return 'x'
420 421 elif ctx.closesbranch():
421 422 return '_'
422 423 else:
423 424 return 'o'
424 425
426 @templatekeyword('index')
427 def showindex(**args):
428 """Integer. The current iteration of the loop. (0 indexed)"""
429 # just hosts documentation; should be overridden by template mapping
430 raise error.Abort(_("can't use index in this context"))
431
425 432 @templatekeyword('latesttag')
426 433 def showlatesttag(**args):
427 434 """List of strings. The global tags on the most recent globally
428 435 tagged ancestor of this changeset.
429 436 """
430 437 return showlatesttags(None, **args)
431 438
432 439 def showlatesttags(pattern, **args):
433 440 """helper method for the latesttag keyword and function"""
434 441 repo, ctx = args['repo'], args['ctx']
435 442 cache = args['cache']
436 443 latesttags = getlatesttags(repo, ctx, cache, pattern)
437 444
438 445 # latesttag[0] is an implementation detail for sorting csets on different
439 446 # branches in a stable manner- it is the date the tagged cset was created,
440 447 # not the date the tag was created. Therefore it isn't made visible here.
441 448 makemap = lambda v: {
442 449 'changes': _showchangessincetag,
443 450 'distance': latesttags[1],
444 451 'latesttag': v, # BC with {latesttag % '{latesttag}'}
445 452 'tag': v
446 453 }
447 454
448 455 tags = latesttags[2]
449 456 f = _showlist('latesttag', tags, separator=':', **args)
450 457 return _hybrid(f, tags, makemap, lambda x: x['latesttag'])
451 458
452 459 @templatekeyword('latesttagdistance')
453 460 def showlatesttagdistance(repo, ctx, templ, cache, **args):
454 461 """Integer. Longest path to the latest tag."""
455 462 return getlatesttags(repo, ctx, cache)[1]
456 463
457 464 @templatekeyword('changessincelatesttag')
458 465 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
459 466 """Integer. All ancestors not in the latest tag."""
460 467 latesttag = getlatesttags(repo, ctx, cache)[2][0]
461 468
462 469 return _showchangessincetag(repo, ctx, tag=latesttag, **args)
463 470
464 471 def _showchangessincetag(repo, ctx, **args):
465 472 offset = 0
466 473 revs = [ctx.rev()]
467 474 tag = args['tag']
468 475
469 476 # The only() revset doesn't currently support wdir()
470 477 if ctx.rev() is None:
471 478 offset = 1
472 479 revs = [p.rev() for p in ctx.parents()]
473 480
474 481 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
475 482
476 483 @templatekeyword('manifest')
477 484 def showmanifest(**args):
478 485 repo, ctx, templ = args['repo'], args['ctx'], args['templ']
479 486 mnode = ctx.manifestnode()
480 487 if mnode is None:
481 488 # just avoid crash, we might want to use the 'ff...' hash in future
482 489 return
483 490 args = args.copy()
484 491 args.update({'rev': repo.manifestlog._revlog.rev(mnode),
485 492 'node': hex(mnode)})
486 493 return templ('manifest', **args)
487 494
488 495 def shownames(namespace, **args):
489 496 """helper method to generate a template keyword for a namespace"""
490 497 ctx = args['ctx']
491 498 repo = ctx.repo()
492 499 ns = repo.names[namespace]
493 500 names = ns.names(repo, ctx.node())
494 501 return showlist(ns.templatename, names, plural=namespace, **args)
495 502
496 503 @templatekeyword('namespaces')
497 504 def shownamespaces(**args):
498 505 """Dict of lists. Names attached to this changeset per
499 506 namespace."""
500 507 ctx = args['ctx']
501 508 repo = ctx.repo()
502 509 namespaces = util.sortdict((k, showlist('name', ns.names(repo, ctx.node()),
503 510 **args))
504 511 for k, ns in repo.names.iteritems())
505 512 f = _showlist('namespace', list(namespaces), **args)
506 513 return _hybrid(f, namespaces,
507 514 lambda k: {'namespace': k, 'names': namespaces[k]},
508 515 lambda x: x['namespace'])
509 516
510 517 @templatekeyword('node')
511 518 def shownode(repo, ctx, templ, **args):
512 519 """String. The changeset identification hash, as a 40 hexadecimal
513 520 digit string.
514 521 """
515 522 return ctx.hex()
516 523
517 524 @templatekeyword('obsolete')
518 525 def showobsolete(repo, ctx, templ, **args):
519 526 """String. Whether the changeset is obsolete.
520 527 """
521 528 if ctx.obsolete():
522 529 return 'obsolete'
523 530 return ''
524 531
525 532 @templatekeyword('p1rev')
526 533 def showp1rev(repo, ctx, templ, **args):
527 534 """Integer. The repository-local revision number of the changeset's
528 535 first parent, or -1 if the changeset has no parents."""
529 536 return ctx.p1().rev()
530 537
531 538 @templatekeyword('p2rev')
532 539 def showp2rev(repo, ctx, templ, **args):
533 540 """Integer. The repository-local revision number of the changeset's
534 541 second parent, or -1 if the changeset has no second parent."""
535 542 return ctx.p2().rev()
536 543
537 544 @templatekeyword('p1node')
538 545 def showp1node(repo, ctx, templ, **args):
539 546 """String. The identification hash of the changeset's first parent,
540 547 as a 40 digit hexadecimal string. If the changeset has no parents, all
541 548 digits are 0."""
542 549 return ctx.p1().hex()
543 550
544 551 @templatekeyword('p2node')
545 552 def showp2node(repo, ctx, templ, **args):
546 553 """String. The identification hash of the changeset's second
547 554 parent, as a 40 digit hexadecimal string. If the changeset has no second
548 555 parent, all digits are 0."""
549 556 return ctx.p2().hex()
550 557
551 558 @templatekeyword('parents')
552 559 def showparents(**args):
553 560 """List of strings. The parents of the changeset in "rev:node"
554 561 format. If the changeset has only one "natural" parent (the predecessor
555 562 revision) nothing is shown."""
556 563 repo = args['repo']
557 564 ctx = args['ctx']
558 565 pctxs = scmutil.meaningfulparents(repo, ctx)
559 566 prevs = [str(p.rev()) for p in pctxs] # ifcontains() needs a list of str
560 567 parents = [[('rev', p.rev()),
561 568 ('node', p.hex()),
562 569 ('phase', p.phasestr())]
563 570 for p in pctxs]
564 571 f = _showlist('parent', parents, **args)
565 572 return _hybrid(f, prevs, lambda x: {'ctx': repo[int(x)], 'revcache': {}},
566 573 lambda d: _formatrevnode(d['ctx']))
567 574
568 575 @templatekeyword('phase')
569 576 def showphase(repo, ctx, templ, **args):
570 577 """String. The changeset phase name."""
571 578 return ctx.phasestr()
572 579
573 580 @templatekeyword('phaseidx')
574 581 def showphaseidx(repo, ctx, templ, **args):
575 582 """Integer. The changeset phase index."""
576 583 return ctx.phase()
577 584
578 585 @templatekeyword('rev')
579 586 def showrev(repo, ctx, templ, **args):
580 587 """Integer. The repository-local changeset revision number."""
581 588 return scmutil.intrev(ctx.rev())
582 589
583 590 def showrevslist(name, revs, **args):
584 591 """helper to generate a list of revisions in which a mapped template will
585 592 be evaluated"""
586 593 repo = args['ctx'].repo()
587 594 revs = [str(r) for r in revs] # ifcontains() needs a list of str
588 595 f = _showlist(name, revs, **args)
589 596 return _hybrid(f, revs,
590 597 lambda x: {name: x, 'ctx': repo[int(x)], 'revcache': {}},
591 598 lambda d: d[name])
592 599
593 600 @templatekeyword('subrepos')
594 601 def showsubrepos(**args):
595 602 """List of strings. Updated subrepositories in the changeset."""
596 603 ctx = args['ctx']
597 604 substate = ctx.substate
598 605 if not substate:
599 606 return showlist('subrepo', [], **args)
600 607 psubstate = ctx.parents()[0].substate or {}
601 608 subrepos = []
602 609 for sub in substate:
603 610 if sub not in psubstate or substate[sub] != psubstate[sub]:
604 611 subrepos.append(sub) # modified or newly added in ctx
605 612 for sub in psubstate:
606 613 if sub not in substate:
607 614 subrepos.append(sub) # removed in ctx
608 615 return showlist('subrepo', sorted(subrepos), **args)
609 616
610 617 # don't remove "showtags" definition, even though namespaces will put
611 618 # a helper function for "tags" keyword into "keywords" map automatically,
612 619 # because online help text is built without namespaces initialization
613 620 @templatekeyword('tags')
614 621 def showtags(**args):
615 622 """List of strings. Any tags associated with the changeset."""
616 623 return shownames('tags', **args)
617 624
618 625 def loadkeyword(ui, extname, registrarobj):
619 626 """Load template keyword from specified registrarobj
620 627 """
621 628 for name, func in registrarobj._table.iteritems():
622 629 keywords[name] = func
623 630
624 631 @templatekeyword('termwidth')
625 632 def termwidth(repo, ctx, templ, **args):
626 633 """Integer. The width of the current terminal."""
627 634 return repo.ui.termwidth()
628 635
629 636 @templatekeyword('troubles')
630 637 def showtroubles(**args):
631 638 """List of strings. Evolution troubles affecting the changeset.
632 639
633 640 (EXPERIMENTAL)
634 641 """
635 642 return showlist('trouble', args['ctx'].troubles(), **args)
636 643
637 644 # tell hggettext to extract docstrings from these functions:
638 645 i18nfunctions = keywords.values()
@@ -1,1290 +1,1291
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 color,
17 17 config,
18 18 encoding,
19 19 error,
20 20 minirst,
21 21 parser,
22 22 pycompat,
23 23 registrar,
24 24 revset as revsetmod,
25 25 revsetlang,
26 26 templatefilters,
27 27 templatekw,
28 28 util,
29 29 )
30 30
31 31 # template parsing
32 32
33 33 elements = {
34 34 # token-type: binding-strength, primary, prefix, infix, suffix
35 35 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
36 36 ",": (2, None, None, ("list", 2), None),
37 37 "|": (5, None, None, ("|", 5), None),
38 38 "%": (6, None, None, ("%", 6), None),
39 39 ")": (0, None, None, None, None),
40 40 "+": (3, None, None, ("+", 3), None),
41 41 "-": (3, None, ("negate", 10), ("-", 3), None),
42 42 "*": (4, None, None, ("*", 4), None),
43 43 "/": (4, None, None, ("/", 4), None),
44 44 "integer": (0, "integer", None, None, None),
45 45 "symbol": (0, "symbol", None, None, None),
46 46 "string": (0, "string", None, None, None),
47 47 "template": (0, "template", None, None, None),
48 48 "end": (0, None, None, None, None),
49 49 }
50 50
51 51 def tokenize(program, start, end, term=None):
52 52 """Parse a template expression into a stream of tokens, which must end
53 53 with term if specified"""
54 54 pos = start
55 55 while pos < end:
56 56 c = program[pos]
57 57 if c.isspace(): # skip inter-token whitespace
58 58 pass
59 59 elif c in "(,)%|+-*/": # handle simple operators
60 60 yield (c, None, pos)
61 61 elif c in '"\'': # handle quoted templates
62 62 s = pos + 1
63 63 data, pos = _parsetemplate(program, s, end, c)
64 64 yield ('template', data, s)
65 65 pos -= 1
66 66 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
67 67 # handle quoted strings
68 68 c = program[pos + 1]
69 69 s = pos = pos + 2
70 70 while pos < end: # find closing quote
71 71 d = program[pos]
72 72 if d == '\\': # skip over escaped characters
73 73 pos += 2
74 74 continue
75 75 if d == c:
76 76 yield ('string', program[s:pos], s)
77 77 break
78 78 pos += 1
79 79 else:
80 80 raise error.ParseError(_("unterminated string"), s)
81 81 elif c.isdigit():
82 82 s = pos
83 83 while pos < end:
84 84 d = program[pos]
85 85 if not d.isdigit():
86 86 break
87 87 pos += 1
88 88 yield ('integer', program[s:pos], s)
89 89 pos -= 1
90 90 elif (c == '\\' and program[pos:pos + 2] in (r"\'", r'\"')
91 91 or c == 'r' and program[pos:pos + 3] in (r"r\'", r'r\"')):
92 92 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
93 93 # where some of nested templates were preprocessed as strings and
94 94 # then compiled. therefore, \"...\" was allowed. (issue4733)
95 95 #
96 96 # processing flow of _evalifliteral() at 5ab28a2e9962:
97 97 # outer template string -> stringify() -> compiletemplate()
98 98 # ------------------------ ------------ ------------------
99 99 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
100 100 # ~~~~~~~~
101 101 # escaped quoted string
102 102 if c == 'r':
103 103 pos += 1
104 104 token = 'string'
105 105 else:
106 106 token = 'template'
107 107 quote = program[pos:pos + 2]
108 108 s = pos = pos + 2
109 109 while pos < end: # find closing escaped quote
110 110 if program.startswith('\\\\\\', pos, end):
111 111 pos += 4 # skip over double escaped characters
112 112 continue
113 113 if program.startswith(quote, pos, end):
114 114 # interpret as if it were a part of an outer string
115 115 data = parser.unescapestr(program[s:pos])
116 116 if token == 'template':
117 117 data = _parsetemplate(data, 0, len(data))[0]
118 118 yield (token, data, s)
119 119 pos += 1
120 120 break
121 121 pos += 1
122 122 else:
123 123 raise error.ParseError(_("unterminated string"), s)
124 124 elif c.isalnum() or c in '_':
125 125 s = pos
126 126 pos += 1
127 127 while pos < end: # find end of symbol
128 128 d = program[pos]
129 129 if not (d.isalnum() or d == "_"):
130 130 break
131 131 pos += 1
132 132 sym = program[s:pos]
133 133 yield ('symbol', sym, s)
134 134 pos -= 1
135 135 elif c == term:
136 136 yield ('end', None, pos + 1)
137 137 return
138 138 else:
139 139 raise error.ParseError(_("syntax error"), pos)
140 140 pos += 1
141 141 if term:
142 142 raise error.ParseError(_("unterminated template expansion"), start)
143 143 yield ('end', None, pos)
144 144
145 145 def _parsetemplate(tmpl, start, stop, quote=''):
146 146 r"""
147 147 >>> _parsetemplate('foo{bar}"baz', 0, 12)
148 148 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
149 149 >>> _parsetemplate('foo{bar}"baz', 0, 12, quote='"')
150 150 ([('string', 'foo'), ('symbol', 'bar')], 9)
151 151 >>> _parsetemplate('foo"{bar}', 0, 9, quote='"')
152 152 ([('string', 'foo')], 4)
153 153 >>> _parsetemplate(r'foo\"bar"baz', 0, 12, quote='"')
154 154 ([('string', 'foo"'), ('string', 'bar')], 9)
155 155 >>> _parsetemplate(r'foo\\"bar', 0, 10, quote='"')
156 156 ([('string', 'foo\\')], 6)
157 157 """
158 158 parsed = []
159 159 sepchars = '{' + quote
160 160 pos = start
161 161 p = parser.parser(elements)
162 162 while pos < stop:
163 163 n = min((tmpl.find(c, pos, stop) for c in sepchars),
164 164 key=lambda n: (n < 0, n))
165 165 if n < 0:
166 166 parsed.append(('string', parser.unescapestr(tmpl[pos:stop])))
167 167 pos = stop
168 168 break
169 169 c = tmpl[n]
170 170 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
171 171 if bs % 2 == 1:
172 172 # escaped (e.g. '\{', '\\\{', but not '\\{')
173 173 parsed.append(('string', parser.unescapestr(tmpl[pos:n - 1]) + c))
174 174 pos = n + 1
175 175 continue
176 176 if n > pos:
177 177 parsed.append(('string', parser.unescapestr(tmpl[pos:n])))
178 178 if c == quote:
179 179 return parsed, n + 1
180 180
181 181 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
182 182 parsed.append(parseres)
183 183
184 184 if quote:
185 185 raise error.ParseError(_("unterminated string"), start)
186 186 return parsed, pos
187 187
188 188 def _unnesttemplatelist(tree):
189 189 """Expand list of templates to node tuple
190 190
191 191 >>> def f(tree):
192 192 ... print prettyformat(_unnesttemplatelist(tree))
193 193 >>> f(('template', []))
194 194 ('string', '')
195 195 >>> f(('template', [('string', 'foo')]))
196 196 ('string', 'foo')
197 197 >>> f(('template', [('string', 'foo'), ('symbol', 'rev')]))
198 198 (template
199 199 ('string', 'foo')
200 200 ('symbol', 'rev'))
201 201 >>> f(('template', [('symbol', 'rev')])) # template(rev) -> str
202 202 (template
203 203 ('symbol', 'rev'))
204 204 >>> f(('template', [('template', [('string', 'foo')])]))
205 205 ('string', 'foo')
206 206 """
207 207 if not isinstance(tree, tuple):
208 208 return tree
209 209 op = tree[0]
210 210 if op != 'template':
211 211 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
212 212
213 213 assert len(tree) == 2
214 214 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
215 215 if not xs:
216 216 return ('string', '') # empty template ""
217 217 elif len(xs) == 1 and xs[0][0] == 'string':
218 218 return xs[0] # fast path for string with no template fragment "x"
219 219 else:
220 220 return (op,) + xs
221 221
222 222 def parse(tmpl):
223 223 """Parse template string into tree"""
224 224 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
225 225 assert pos == len(tmpl), 'unquoted template should be consumed'
226 226 return _unnesttemplatelist(('template', parsed))
227 227
228 228 def _parseexpr(expr):
229 229 """Parse a template expression into tree
230 230
231 231 >>> _parseexpr('"foo"')
232 232 ('string', 'foo')
233 233 >>> _parseexpr('foo(bar)')
234 234 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
235 235 >>> _parseexpr('foo(')
236 236 Traceback (most recent call last):
237 237 ...
238 238 ParseError: ('not a prefix: end', 4)
239 239 >>> _parseexpr('"foo" "bar"')
240 240 Traceback (most recent call last):
241 241 ...
242 242 ParseError: ('invalid token', 7)
243 243 """
244 244 p = parser.parser(elements)
245 245 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
246 246 if pos != len(expr):
247 247 raise error.ParseError(_('invalid token'), pos)
248 248 return _unnesttemplatelist(tree)
249 249
250 250 def prettyformat(tree):
251 251 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
252 252
253 253 def compileexp(exp, context, curmethods):
254 254 """Compile parsed template tree to (func, data) pair"""
255 255 t = exp[0]
256 256 if t in curmethods:
257 257 return curmethods[t](exp, context)
258 258 raise error.ParseError(_("unknown method '%s'") % t)
259 259
260 260 # template evaluation
261 261
262 262 def getsymbol(exp):
263 263 if exp[0] == 'symbol':
264 264 return exp[1]
265 265 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
266 266
267 267 def getlist(x):
268 268 if not x:
269 269 return []
270 270 if x[0] == 'list':
271 271 return getlist(x[1]) + [x[2]]
272 272 return [x]
273 273
274 274 def gettemplate(exp, context):
275 275 """Compile given template tree or load named template from map file;
276 276 returns (func, data) pair"""
277 277 if exp[0] in ('template', 'string'):
278 278 return compileexp(exp, context, methods)
279 279 if exp[0] == 'symbol':
280 280 # unlike runsymbol(), here 'symbol' is always taken as template name
281 281 # even if it exists in mapping. this allows us to override mapping
282 282 # by web templates, e.g. 'changelogtag' is redefined in map file.
283 283 return context._load(exp[1])
284 284 raise error.ParseError(_("expected template specifier"))
285 285
286 286 def evalfuncarg(context, mapping, arg):
287 287 func, data = arg
288 288 # func() may return string, generator of strings or arbitrary object such
289 289 # as date tuple, but filter does not want generator.
290 290 thing = func(context, mapping, data)
291 291 if isinstance(thing, types.GeneratorType):
292 292 thing = stringify(thing)
293 293 return thing
294 294
295 295 def evalboolean(context, mapping, arg):
296 296 """Evaluate given argument as boolean, but also takes boolean literals"""
297 297 func, data = arg
298 298 if func is runsymbol:
299 299 thing = func(context, mapping, data, default=None)
300 300 if thing is None:
301 301 # not a template keyword, takes as a boolean literal
302 302 thing = util.parsebool(data)
303 303 else:
304 304 thing = func(context, mapping, data)
305 305 if isinstance(thing, bool):
306 306 return thing
307 307 # other objects are evaluated as strings, which means 0 is True, but
308 308 # empty dict/list should be False as they are expected to be ''
309 309 return bool(stringify(thing))
310 310
311 311 def evalinteger(context, mapping, arg, err):
312 312 v = evalfuncarg(context, mapping, arg)
313 313 try:
314 314 return int(v)
315 315 except (TypeError, ValueError):
316 316 raise error.ParseError(err)
317 317
318 318 def evalstring(context, mapping, arg):
319 319 func, data = arg
320 320 return stringify(func(context, mapping, data))
321 321
322 322 def evalstringliteral(context, mapping, arg):
323 323 """Evaluate given argument as string template, but returns symbol name
324 324 if it is unknown"""
325 325 func, data = arg
326 326 if func is runsymbol:
327 327 thing = func(context, mapping, data, default=data)
328 328 else:
329 329 thing = func(context, mapping, data)
330 330 return stringify(thing)
331 331
332 332 def runinteger(context, mapping, data):
333 333 return int(data)
334 334
335 335 def runstring(context, mapping, data):
336 336 return data
337 337
338 338 def _recursivesymbolblocker(key):
339 339 def showrecursion(**args):
340 340 raise error.Abort(_("recursive reference '%s' in template") % key)
341 341 return showrecursion
342 342
343 343 def _runrecursivesymbol(context, mapping, key):
344 344 raise error.Abort(_("recursive reference '%s' in template") % key)
345 345
346 346 def runsymbol(context, mapping, key, default=''):
347 347 v = mapping.get(key)
348 348 if v is None:
349 349 v = context._defaults.get(key)
350 350 if v is None:
351 351 # put poison to cut recursion. we can't move this to parsing phase
352 352 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
353 353 safemapping = mapping.copy()
354 354 safemapping[key] = _recursivesymbolblocker(key)
355 355 try:
356 356 v = context.process(key, safemapping)
357 357 except TemplateNotFound:
358 358 v = default
359 359 if callable(v):
360 360 return v(**mapping)
361 361 return v
362 362
363 363 def buildtemplate(exp, context):
364 364 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
365 365 return (runtemplate, ctmpl)
366 366
367 367 def runtemplate(context, mapping, template):
368 368 for func, data in template:
369 369 yield func(context, mapping, data)
370 370
371 371 def buildfilter(exp, context):
372 372 arg = compileexp(exp[1], context, methods)
373 373 n = getsymbol(exp[2])
374 374 if n in context._filters:
375 375 filt = context._filters[n]
376 376 return (runfilter, (arg, filt))
377 377 if n in funcs:
378 378 f = funcs[n]
379 379 return (f, [arg])
380 380 raise error.ParseError(_("unknown function '%s'") % n)
381 381
382 382 def runfilter(context, mapping, data):
383 383 arg, filt = data
384 384 thing = evalfuncarg(context, mapping, arg)
385 385 try:
386 386 return filt(thing)
387 387 except (ValueError, AttributeError, TypeError):
388 388 if isinstance(arg[1], tuple):
389 389 dt = arg[1][1]
390 390 else:
391 391 dt = arg[1]
392 392 raise error.Abort(_("template filter '%s' is not compatible with "
393 393 "keyword '%s'") % (filt.func_name, dt))
394 394
395 395 def buildmap(exp, context):
396 396 func, data = compileexp(exp[1], context, methods)
397 397 tfunc, tdata = gettemplate(exp[2], context)
398 398 return (runmap, (func, data, tfunc, tdata))
399 399
400 400 def runmap(context, mapping, data):
401 401 func, data, tfunc, tdata = data
402 402 d = func(context, mapping, data)
403 403 if util.safehasattr(d, 'itermaps'):
404 404 diter = d.itermaps()
405 405 else:
406 406 try:
407 407 diter = iter(d)
408 408 except TypeError:
409 409 if func is runsymbol:
410 410 raise error.ParseError(_("keyword '%s' is not iterable") % data)
411 411 else:
412 412 raise error.ParseError(_("%r is not iterable") % d)
413 413
414 for v in diter:
414 for i, v in enumerate(diter):
415 415 lm = mapping.copy()
416 lm['index'] = i
416 417 if isinstance(v, dict):
417 418 lm.update(v)
418 419 lm['originalnode'] = mapping.get('node')
419 420 yield tfunc(context, lm, tdata)
420 421 else:
421 422 # v is not an iterable of dicts, this happen when 'key'
422 423 # has been fully expanded already and format is useless.
423 424 # If so, return the expanded value.
424 425 yield v
425 426
426 427 def buildnegate(exp, context):
427 428 arg = compileexp(exp[1], context, exprmethods)
428 429 return (runnegate, arg)
429 430
430 431 def runnegate(context, mapping, data):
431 432 data = evalinteger(context, mapping, data,
432 433 _('negation needs an integer argument'))
433 434 return -data
434 435
435 436 def buildarithmetic(exp, context, func):
436 437 left = compileexp(exp[1], context, exprmethods)
437 438 right = compileexp(exp[2], context, exprmethods)
438 439 return (runarithmetic, (func, left, right))
439 440
440 441 def runarithmetic(context, mapping, data):
441 442 func, left, right = data
442 443 left = evalinteger(context, mapping, left,
443 444 _('arithmetic only defined on integers'))
444 445 right = evalinteger(context, mapping, right,
445 446 _('arithmetic only defined on integers'))
446 447 try:
447 448 return func(left, right)
448 449 except ZeroDivisionError:
449 450 raise error.Abort(_('division by zero is not defined'))
450 451
451 452 def buildfunc(exp, context):
452 453 n = getsymbol(exp[1])
453 454 args = [compileexp(x, context, exprmethods) for x in getlist(exp[2])]
454 455 if n in funcs:
455 456 f = funcs[n]
456 457 return (f, args)
457 458 if n in context._filters:
458 459 if len(args) != 1:
459 460 raise error.ParseError(_("filter %s expects one argument") % n)
460 461 f = context._filters[n]
461 462 return (runfilter, (args[0], f))
462 463 raise error.ParseError(_("unknown function '%s'") % n)
463 464
464 465 # dict of template built-in functions
465 466 funcs = {}
466 467
467 468 templatefunc = registrar.templatefunc(funcs)
468 469
469 470 @templatefunc('date(date[, fmt])')
470 471 def date(context, mapping, args):
471 472 """Format a date. See :hg:`help dates` for formatting
472 473 strings. The default is a Unix date format, including the timezone:
473 474 "Mon Sep 04 15:13:13 2006 0700"."""
474 475 if not (1 <= len(args) <= 2):
475 476 # i18n: "date" is a keyword
476 477 raise error.ParseError(_("date expects one or two arguments"))
477 478
478 479 date = evalfuncarg(context, mapping, args[0])
479 480 fmt = None
480 481 if len(args) == 2:
481 482 fmt = evalstring(context, mapping, args[1])
482 483 try:
483 484 if fmt is None:
484 485 return util.datestr(date)
485 486 else:
486 487 return util.datestr(date, fmt)
487 488 except (TypeError, ValueError):
488 489 # i18n: "date" is a keyword
489 490 raise error.ParseError(_("date expects a date information"))
490 491
491 492 @templatefunc('diff([includepattern [, excludepattern]])')
492 493 def diff(context, mapping, args):
493 494 """Show a diff, optionally
494 495 specifying files to include or exclude."""
495 496 if len(args) > 2:
496 497 # i18n: "diff" is a keyword
497 498 raise error.ParseError(_("diff expects zero, one, or two arguments"))
498 499
499 500 def getpatterns(i):
500 501 if i < len(args):
501 502 s = evalstring(context, mapping, args[i]).strip()
502 503 if s:
503 504 return [s]
504 505 return []
505 506
506 507 ctx = mapping['ctx']
507 508 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
508 509
509 510 return ''.join(chunks)
510 511
511 512 @templatefunc('files(pattern)')
512 513 def files(context, mapping, args):
513 514 """All files of the current changeset matching the pattern. See
514 515 :hg:`help patterns`."""
515 516 if not len(args) == 1:
516 517 # i18n: "files" is a keyword
517 518 raise error.ParseError(_("files expects one argument"))
518 519
519 520 raw = evalstring(context, mapping, args[0])
520 521 ctx = mapping['ctx']
521 522 m = ctx.match([raw])
522 523 files = list(ctx.matches(m))
523 524 return templatekw.showlist("file", files, **mapping)
524 525
525 526 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
526 527 def fill(context, mapping, args):
527 528 """Fill many
528 529 paragraphs with optional indentation. See the "fill" filter."""
529 530 if not (1 <= len(args) <= 4):
530 531 # i18n: "fill" is a keyword
531 532 raise error.ParseError(_("fill expects one to four arguments"))
532 533
533 534 text = evalstring(context, mapping, args[0])
534 535 width = 76
535 536 initindent = ''
536 537 hangindent = ''
537 538 if 2 <= len(args) <= 4:
538 539 width = evalinteger(context, mapping, args[1],
539 540 # i18n: "fill" is a keyword
540 541 _("fill expects an integer width"))
541 542 try:
542 543 initindent = evalstring(context, mapping, args[2])
543 544 hangindent = evalstring(context, mapping, args[3])
544 545 except IndexError:
545 546 pass
546 547
547 548 return templatefilters.fill(text, width, initindent, hangindent)
548 549
549 550 @templatefunc('formatnode(node)')
550 551 def formatnode(context, mapping, args):
551 552 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
552 553 if len(args) != 1:
553 554 # i18n: "formatnode" is a keyword
554 555 raise error.ParseError(_("formatnode expects one argument"))
555 556
556 557 ui = mapping['ui']
557 558 node = evalstring(context, mapping, args[0])
558 559 if ui.debugflag:
559 560 return node
560 561 return templatefilters.short(node)
561 562
562 563 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])')
563 564 def pad(context, mapping, args):
564 565 """Pad text with a
565 566 fill character."""
566 567 if not (2 <= len(args) <= 4):
567 568 # i18n: "pad" is a keyword
568 569 raise error.ParseError(_("pad() expects two to four arguments"))
569 570
570 571 width = evalinteger(context, mapping, args[1],
571 572 # i18n: "pad" is a keyword
572 573 _("pad() expects an integer width"))
573 574
574 575 text = evalstring(context, mapping, args[0])
575 576
576 577 left = False
577 578 fillchar = ' '
578 579 if len(args) > 2:
579 580 fillchar = evalstring(context, mapping, args[2])
580 581 if len(color.stripeffects(fillchar)) != 1:
581 582 # i18n: "pad" is a keyword
582 583 raise error.ParseError(_("pad() expects a single fill character"))
583 584 if len(args) > 3:
584 585 left = evalboolean(context, mapping, args[3])
585 586
586 587 fillwidth = width - encoding.colwidth(color.stripeffects(text))
587 588 if fillwidth <= 0:
588 589 return text
589 590 if left:
590 591 return fillchar * fillwidth + text
591 592 else:
592 593 return text + fillchar * fillwidth
593 594
594 595 @templatefunc('indent(text, indentchars[, firstline])')
595 596 def indent(context, mapping, args):
596 597 """Indents all non-empty lines
597 598 with the characters given in the indentchars string. An optional
598 599 third parameter will override the indent for the first line only
599 600 if present."""
600 601 if not (2 <= len(args) <= 3):
601 602 # i18n: "indent" is a keyword
602 603 raise error.ParseError(_("indent() expects two or three arguments"))
603 604
604 605 text = evalstring(context, mapping, args[0])
605 606 indent = evalstring(context, mapping, args[1])
606 607
607 608 if len(args) == 3:
608 609 firstline = evalstring(context, mapping, args[2])
609 610 else:
610 611 firstline = indent
611 612
612 613 # the indent function doesn't indent the first line, so we do it here
613 614 return templatefilters.indent(firstline + text, indent)
614 615
615 616 @templatefunc('get(dict, key)')
616 617 def get(context, mapping, args):
617 618 """Get an attribute/key from an object. Some keywords
618 619 are complex types. This function allows you to obtain the value of an
619 620 attribute on these types."""
620 621 if len(args) != 2:
621 622 # i18n: "get" is a keyword
622 623 raise error.ParseError(_("get() expects two arguments"))
623 624
624 625 dictarg = evalfuncarg(context, mapping, args[0])
625 626 if not util.safehasattr(dictarg, 'get'):
626 627 # i18n: "get" is a keyword
627 628 raise error.ParseError(_("get() expects a dict as first argument"))
628 629
629 630 key = evalfuncarg(context, mapping, args[1])
630 631 return dictarg.get(key)
631 632
632 633 @templatefunc('if(expr, then[, else])')
633 634 def if_(context, mapping, args):
634 635 """Conditionally execute based on the result of
635 636 an expression."""
636 637 if not (2 <= len(args) <= 3):
637 638 # i18n: "if" is a keyword
638 639 raise error.ParseError(_("if expects two or three arguments"))
639 640
640 641 test = evalboolean(context, mapping, args[0])
641 642 if test:
642 643 yield args[1][0](context, mapping, args[1][1])
643 644 elif len(args) == 3:
644 645 yield args[2][0](context, mapping, args[2][1])
645 646
646 647 @templatefunc('ifcontains(needle, haystack, then[, else])')
647 648 def ifcontains(context, mapping, args):
648 649 """Conditionally execute based
649 650 on whether the item "needle" is in "haystack"."""
650 651 if not (3 <= len(args) <= 4):
651 652 # i18n: "ifcontains" is a keyword
652 653 raise error.ParseError(_("ifcontains expects three or four arguments"))
653 654
654 655 needle = evalstring(context, mapping, args[0])
655 656 haystack = evalfuncarg(context, mapping, args[1])
656 657
657 658 if needle in haystack:
658 659 yield args[2][0](context, mapping, args[2][1])
659 660 elif len(args) == 4:
660 661 yield args[3][0](context, mapping, args[3][1])
661 662
662 663 @templatefunc('ifeq(expr1, expr2, then[, else])')
663 664 def ifeq(context, mapping, args):
664 665 """Conditionally execute based on
665 666 whether 2 items are equivalent."""
666 667 if not (3 <= len(args) <= 4):
667 668 # i18n: "ifeq" is a keyword
668 669 raise error.ParseError(_("ifeq expects three or four arguments"))
669 670
670 671 test = evalstring(context, mapping, args[0])
671 672 match = evalstring(context, mapping, args[1])
672 673 if test == match:
673 674 yield args[2][0](context, mapping, args[2][1])
674 675 elif len(args) == 4:
675 676 yield args[3][0](context, mapping, args[3][1])
676 677
677 678 @templatefunc('join(list, sep)')
678 679 def join(context, mapping, args):
679 680 """Join items in a list with a delimiter."""
680 681 if not (1 <= len(args) <= 2):
681 682 # i18n: "join" is a keyword
682 683 raise error.ParseError(_("join expects one or two arguments"))
683 684
684 685 joinset = args[0][0](context, mapping, args[0][1])
685 686 if util.safehasattr(joinset, 'itermaps'):
686 687 jf = joinset.joinfmt
687 688 joinset = [jf(x) for x in joinset.itermaps()]
688 689
689 690 joiner = " "
690 691 if len(args) > 1:
691 692 joiner = evalstring(context, mapping, args[1])
692 693
693 694 first = True
694 695 for x in joinset:
695 696 if first:
696 697 first = False
697 698 else:
698 699 yield joiner
699 700 yield x
700 701
701 702 @templatefunc('label(label, expr)')
702 703 def label(context, mapping, args):
703 704 """Apply a label to generated content. Content with
704 705 a label applied can result in additional post-processing, such as
705 706 automatic colorization."""
706 707 if len(args) != 2:
707 708 # i18n: "label" is a keyword
708 709 raise error.ParseError(_("label expects two arguments"))
709 710
710 711 ui = mapping['ui']
711 712 thing = evalstring(context, mapping, args[1])
712 713 # preserve unknown symbol as literal so effects like 'red', 'bold',
713 714 # etc. don't need to be quoted
714 715 label = evalstringliteral(context, mapping, args[0])
715 716
716 717 return ui.label(thing, label)
717 718
718 719 @templatefunc('latesttag([pattern])')
719 720 def latesttag(context, mapping, args):
720 721 """The global tags matching the given pattern on the
721 722 most recent globally tagged ancestor of this changeset."""
722 723 if len(args) > 1:
723 724 # i18n: "latesttag" is a keyword
724 725 raise error.ParseError(_("latesttag expects at most one argument"))
725 726
726 727 pattern = None
727 728 if len(args) == 1:
728 729 pattern = evalstring(context, mapping, args[0])
729 730
730 731 return templatekw.showlatesttags(pattern, **mapping)
731 732
732 733 @templatefunc('localdate(date[, tz])')
733 734 def localdate(context, mapping, args):
734 735 """Converts a date to the specified timezone.
735 736 The default is local date."""
736 737 if not (1 <= len(args) <= 2):
737 738 # i18n: "localdate" is a keyword
738 739 raise error.ParseError(_("localdate expects one or two arguments"))
739 740
740 741 date = evalfuncarg(context, mapping, args[0])
741 742 try:
742 743 date = util.parsedate(date)
743 744 except AttributeError: # not str nor date tuple
744 745 # i18n: "localdate" is a keyword
745 746 raise error.ParseError(_("localdate expects a date information"))
746 747 if len(args) >= 2:
747 748 tzoffset = None
748 749 tz = evalfuncarg(context, mapping, args[1])
749 750 if isinstance(tz, str):
750 751 tzoffset, remainder = util.parsetimezone(tz)
751 752 if remainder:
752 753 tzoffset = None
753 754 if tzoffset is None:
754 755 try:
755 756 tzoffset = int(tz)
756 757 except (TypeError, ValueError):
757 758 # i18n: "localdate" is a keyword
758 759 raise error.ParseError(_("localdate expects a timezone"))
759 760 else:
760 761 tzoffset = util.makedate()[1]
761 762 return (date[0], tzoffset)
762 763
763 764 @templatefunc('mod(a, b)')
764 765 def mod(context, mapping, args):
765 766 """Calculate a mod b such that a / b + a mod b == a"""
766 767 if not len(args) == 2:
767 768 # i18n: "mod" is a keyword
768 769 raise error.ParseError(_("mod expects two arguments"))
769 770
770 771 func = lambda a, b: a % b
771 772 return runarithmetic(context, mapping, (func, args[0], args[1]))
772 773
773 774 @templatefunc('relpath(path)')
774 775 def relpath(context, mapping, args):
775 776 """Convert a repository-absolute path into a filesystem path relative to
776 777 the current working directory."""
777 778 if len(args) != 1:
778 779 # i18n: "relpath" is a keyword
779 780 raise error.ParseError(_("relpath expects one argument"))
780 781
781 782 repo = mapping['ctx'].repo()
782 783 path = evalstring(context, mapping, args[0])
783 784 return repo.pathto(path)
784 785
785 786 @templatefunc('revset(query[, formatargs...])')
786 787 def revset(context, mapping, args):
787 788 """Execute a revision set query. See
788 789 :hg:`help revset`."""
789 790 if not len(args) > 0:
790 791 # i18n: "revset" is a keyword
791 792 raise error.ParseError(_("revset expects one or more arguments"))
792 793
793 794 raw = evalstring(context, mapping, args[0])
794 795 ctx = mapping['ctx']
795 796 repo = ctx.repo()
796 797
797 798 def query(expr):
798 799 m = revsetmod.match(repo.ui, expr)
799 800 return m(repo)
800 801
801 802 if len(args) > 1:
802 803 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
803 804 revs = query(revsetlang.formatspec(raw, *formatargs))
804 805 revs = list(revs)
805 806 else:
806 807 revsetcache = mapping['cache'].setdefault("revsetcache", {})
807 808 if raw in revsetcache:
808 809 revs = revsetcache[raw]
809 810 else:
810 811 revs = query(raw)
811 812 revs = list(revs)
812 813 revsetcache[raw] = revs
813 814
814 815 return templatekw.showrevslist("revision", revs, **mapping)
815 816
816 817 @templatefunc('rstdoc(text, style)')
817 818 def rstdoc(context, mapping, args):
818 819 """Format reStructuredText."""
819 820 if len(args) != 2:
820 821 # i18n: "rstdoc" is a keyword
821 822 raise error.ParseError(_("rstdoc expects two arguments"))
822 823
823 824 text = evalstring(context, mapping, args[0])
824 825 style = evalstring(context, mapping, args[1])
825 826
826 827 return minirst.format(text, style=style, keep=['verbose'])
827 828
828 829 @templatefunc('separate(sep, args)')
829 830 def separate(context, mapping, args):
830 831 """Add a separator between non-empty arguments."""
831 832 if not args:
832 833 # i18n: "separate" is a keyword
833 834 raise error.ParseError(_("separate expects at least one argument"))
834 835
835 836 sep = evalstring(context, mapping, args[0])
836 837 first = True
837 838 for arg in args[1:]:
838 839 argstr = evalstring(context, mapping, arg)
839 840 if not argstr:
840 841 continue
841 842 if first:
842 843 first = False
843 844 else:
844 845 yield sep
845 846 yield argstr
846 847
847 848 @templatefunc('shortest(node, minlength=4)')
848 849 def shortest(context, mapping, args):
849 850 """Obtain the shortest representation of
850 851 a node."""
851 852 if not (1 <= len(args) <= 2):
852 853 # i18n: "shortest" is a keyword
853 854 raise error.ParseError(_("shortest() expects one or two arguments"))
854 855
855 856 node = evalstring(context, mapping, args[0])
856 857
857 858 minlength = 4
858 859 if len(args) > 1:
859 860 minlength = evalinteger(context, mapping, args[1],
860 861 # i18n: "shortest" is a keyword
861 862 _("shortest() expects an integer minlength"))
862 863
863 864 # _partialmatch() of filtered changelog could take O(len(repo)) time,
864 865 # which would be unacceptably slow. so we look for hash collision in
865 866 # unfiltered space, which means some hashes may be slightly longer.
866 867 cl = mapping['ctx']._repo.unfiltered().changelog
867 868 def isvalid(test):
868 869 try:
869 870 if cl._partialmatch(test) is None:
870 871 return False
871 872
872 873 try:
873 874 i = int(test)
874 875 # if we are a pure int, then starting with zero will not be
875 876 # confused as a rev; or, obviously, if the int is larger than
876 877 # the value of the tip rev
877 878 if test[0] == '0' or i > len(cl):
878 879 return True
879 880 return False
880 881 except ValueError:
881 882 return True
882 883 except error.RevlogError:
883 884 return False
884 885
885 886 shortest = node
886 887 startlength = max(6, minlength)
887 888 length = startlength
888 889 while True:
889 890 test = node[:length]
890 891 if isvalid(test):
891 892 shortest = test
892 893 if length == minlength or length > startlength:
893 894 return shortest
894 895 length -= 1
895 896 else:
896 897 length += 1
897 898 if len(shortest) <= length:
898 899 return shortest
899 900
900 901 @templatefunc('strip(text[, chars])')
901 902 def strip(context, mapping, args):
902 903 """Strip characters from a string. By default,
903 904 strips all leading and trailing whitespace."""
904 905 if not (1 <= len(args) <= 2):
905 906 # i18n: "strip" is a keyword
906 907 raise error.ParseError(_("strip expects one or two arguments"))
907 908
908 909 text = evalstring(context, mapping, args[0])
909 910 if len(args) == 2:
910 911 chars = evalstring(context, mapping, args[1])
911 912 return text.strip(chars)
912 913 return text.strip()
913 914
914 915 @templatefunc('sub(pattern, replacement, expression)')
915 916 def sub(context, mapping, args):
916 917 """Perform text substitution
917 918 using regular expressions."""
918 919 if len(args) != 3:
919 920 # i18n: "sub" is a keyword
920 921 raise error.ParseError(_("sub expects three arguments"))
921 922
922 923 pat = evalstring(context, mapping, args[0])
923 924 rpl = evalstring(context, mapping, args[1])
924 925 src = evalstring(context, mapping, args[2])
925 926 try:
926 927 patre = re.compile(pat)
927 928 except re.error:
928 929 # i18n: "sub" is a keyword
929 930 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
930 931 try:
931 932 yield patre.sub(rpl, src)
932 933 except re.error:
933 934 # i18n: "sub" is a keyword
934 935 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
935 936
936 937 @templatefunc('startswith(pattern, text)')
937 938 def startswith(context, mapping, args):
938 939 """Returns the value from the "text" argument
939 940 if it begins with the content from the "pattern" argument."""
940 941 if len(args) != 2:
941 942 # i18n: "startswith" is a keyword
942 943 raise error.ParseError(_("startswith expects two arguments"))
943 944
944 945 patn = evalstring(context, mapping, args[0])
945 946 text = evalstring(context, mapping, args[1])
946 947 if text.startswith(patn):
947 948 return text
948 949 return ''
949 950
950 951 @templatefunc('word(number, text[, separator])')
951 952 def word(context, mapping, args):
952 953 """Return the nth word from a string."""
953 954 if not (2 <= len(args) <= 3):
954 955 # i18n: "word" is a keyword
955 956 raise error.ParseError(_("word expects two or three arguments, got %d")
956 957 % len(args))
957 958
958 959 num = evalinteger(context, mapping, args[0],
959 960 # i18n: "word" is a keyword
960 961 _("word expects an integer index"))
961 962 text = evalstring(context, mapping, args[1])
962 963 if len(args) == 3:
963 964 splitter = evalstring(context, mapping, args[2])
964 965 else:
965 966 splitter = None
966 967
967 968 tokens = text.split(splitter)
968 969 if num >= len(tokens) or num < -len(tokens):
969 970 return ''
970 971 else:
971 972 return tokens[num]
972 973
973 974 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
974 975 exprmethods = {
975 976 "integer": lambda e, c: (runinteger, e[1]),
976 977 "string": lambda e, c: (runstring, e[1]),
977 978 "symbol": lambda e, c: (runsymbol, e[1]),
978 979 "template": buildtemplate,
979 980 "group": lambda e, c: compileexp(e[1], c, exprmethods),
980 981 # ".": buildmember,
981 982 "|": buildfilter,
982 983 "%": buildmap,
983 984 "func": buildfunc,
984 985 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
985 986 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
986 987 "negate": buildnegate,
987 988 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
988 989 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
989 990 }
990 991
991 992 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
992 993 methods = exprmethods.copy()
993 994 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
994 995
995 996 class _aliasrules(parser.basealiasrules):
996 997 """Parsing and expansion rule set of template aliases"""
997 998 _section = _('template alias')
998 999 _parse = staticmethod(_parseexpr)
999 1000
1000 1001 @staticmethod
1001 1002 def _trygetfunc(tree):
1002 1003 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1003 1004 None"""
1004 1005 if tree[0] == 'func' and tree[1][0] == 'symbol':
1005 1006 return tree[1][1], getlist(tree[2])
1006 1007 if tree[0] == '|' and tree[2][0] == 'symbol':
1007 1008 return tree[2][1], [tree[1]]
1008 1009
1009 1010 def expandaliases(tree, aliases):
1010 1011 """Return new tree of aliases are expanded"""
1011 1012 aliasmap = _aliasrules.buildmap(aliases)
1012 1013 return _aliasrules.expand(aliasmap, tree)
1013 1014
1014 1015 # template engine
1015 1016
1016 1017 stringify = templatefilters.stringify
1017 1018
1018 1019 def _flatten(thing):
1019 1020 '''yield a single stream from a possibly nested set of iterators'''
1020 1021 if isinstance(thing, str):
1021 1022 yield thing
1022 1023 elif thing is None:
1023 1024 pass
1024 1025 elif not util.safehasattr(thing, '__iter__'):
1025 1026 yield str(thing)
1026 1027 else:
1027 1028 for i in thing:
1028 1029 if isinstance(i, str):
1029 1030 yield i
1030 1031 elif i is None:
1031 1032 pass
1032 1033 elif not util.safehasattr(i, '__iter__'):
1033 1034 yield str(i)
1034 1035 else:
1035 1036 for j in _flatten(i):
1036 1037 yield j
1037 1038
1038 1039 def unquotestring(s):
1039 1040 '''unwrap quotes if any; otherwise returns unmodified string'''
1040 1041 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1041 1042 return s
1042 1043 return s[1:-1]
1043 1044
1044 1045 class engine(object):
1045 1046 '''template expansion engine.
1046 1047
1047 1048 template expansion works like this. a map file contains key=value
1048 1049 pairs. if value is quoted, it is treated as string. otherwise, it
1049 1050 is treated as name of template file.
1050 1051
1051 1052 templater is asked to expand a key in map. it looks up key, and
1052 1053 looks for strings like this: {foo}. it expands {foo} by looking up
1053 1054 foo in map, and substituting it. expansion is recursive: it stops
1054 1055 when there is no more {foo} to replace.
1055 1056
1056 1057 expansion also allows formatting and filtering.
1057 1058
1058 1059 format uses key to expand each item in list. syntax is
1059 1060 {key%format}.
1060 1061
1061 1062 filter uses function to transform value. syntax is
1062 1063 {key|filter1|filter2|...}.'''
1063 1064
1064 1065 def __init__(self, loader, filters=None, defaults=None, aliases=()):
1065 1066 self._loader = loader
1066 1067 if filters is None:
1067 1068 filters = {}
1068 1069 self._filters = filters
1069 1070 if defaults is None:
1070 1071 defaults = {}
1071 1072 self._defaults = defaults
1072 1073 self._aliasmap = _aliasrules.buildmap(aliases)
1073 1074 self._cache = {} # key: (func, data)
1074 1075
1075 1076 def _load(self, t):
1076 1077 '''load, parse, and cache a template'''
1077 1078 if t not in self._cache:
1078 1079 # put poison to cut recursion while compiling 't'
1079 1080 self._cache[t] = (_runrecursivesymbol, t)
1080 1081 try:
1081 1082 x = parse(self._loader(t))
1082 1083 if self._aliasmap:
1083 1084 x = _aliasrules.expand(self._aliasmap, x)
1084 1085 self._cache[t] = compileexp(x, self, methods)
1085 1086 except: # re-raises
1086 1087 del self._cache[t]
1087 1088 raise
1088 1089 return self._cache[t]
1089 1090
1090 1091 def process(self, t, mapping):
1091 1092 '''Perform expansion. t is name of map element to expand.
1092 1093 mapping contains added elements for use during expansion. Is a
1093 1094 generator.'''
1094 1095 func, data = self._load(t)
1095 1096 return _flatten(func(self, mapping, data))
1096 1097
1097 1098 engines = {'default': engine}
1098 1099
1099 1100 def stylelist():
1100 1101 paths = templatepaths()
1101 1102 if not paths:
1102 1103 return _('no templates found, try `hg debuginstall` for more info')
1103 1104 dirlist = os.listdir(paths[0])
1104 1105 stylelist = []
1105 1106 for file in dirlist:
1106 1107 split = file.split(".")
1107 1108 if split[-1] in ('orig', 'rej'):
1108 1109 continue
1109 1110 if split[0] == "map-cmdline":
1110 1111 stylelist.append(split[1])
1111 1112 return ", ".join(sorted(stylelist))
1112 1113
1113 1114 def _readmapfile(mapfile):
1114 1115 """Load template elements from the given map file"""
1115 1116 if not os.path.exists(mapfile):
1116 1117 raise error.Abort(_("style '%s' not found") % mapfile,
1117 1118 hint=_("available styles: %s") % stylelist())
1118 1119
1119 1120 base = os.path.dirname(mapfile)
1120 1121 conf = config.config(includepaths=templatepaths())
1121 1122 conf.read(mapfile)
1122 1123
1123 1124 cache = {}
1124 1125 tmap = {}
1125 1126 for key, val in conf[''].items():
1126 1127 if not val:
1127 1128 raise error.ParseError(_('missing value'), conf.source('', key))
1128 1129 if val[0] in "'\"":
1129 1130 if val[0] != val[-1]:
1130 1131 raise error.ParseError(_('unmatched quotes'),
1131 1132 conf.source('', key))
1132 1133 cache[key] = unquotestring(val)
1133 1134 elif key == "__base__":
1134 1135 # treat as a pointer to a base class for this style
1135 1136 path = util.normpath(os.path.join(base, val))
1136 1137
1137 1138 # fallback check in template paths
1138 1139 if not os.path.exists(path):
1139 1140 for p in templatepaths():
1140 1141 p2 = util.normpath(os.path.join(p, val))
1141 1142 if os.path.isfile(p2):
1142 1143 path = p2
1143 1144 break
1144 1145 p3 = util.normpath(os.path.join(p2, "map"))
1145 1146 if os.path.isfile(p3):
1146 1147 path = p3
1147 1148 break
1148 1149
1149 1150 bcache, btmap = _readmapfile(path)
1150 1151 for k in bcache:
1151 1152 if k not in cache:
1152 1153 cache[k] = bcache[k]
1153 1154 for k in btmap:
1154 1155 if k not in tmap:
1155 1156 tmap[k] = btmap[k]
1156 1157 else:
1157 1158 val = 'default', val
1158 1159 if ':' in val[1]:
1159 1160 val = val[1].split(':', 1)
1160 1161 tmap[key] = val[0], os.path.join(base, val[1])
1161 1162 return cache, tmap
1162 1163
1163 1164 class TemplateNotFound(error.Abort):
1164 1165 pass
1165 1166
1166 1167 class templater(object):
1167 1168
1168 1169 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1169 1170 minchunk=1024, maxchunk=65536):
1170 1171 '''set up template engine.
1171 1172 filters is dict of functions. each transforms a value into another.
1172 1173 defaults is dict of default map definitions.
1173 1174 aliases is list of alias (name, replacement) pairs.
1174 1175 '''
1175 1176 if filters is None:
1176 1177 filters = {}
1177 1178 if defaults is None:
1178 1179 defaults = {}
1179 1180 if cache is None:
1180 1181 cache = {}
1181 1182 self.cache = cache.copy()
1182 1183 self.map = {}
1183 1184 self.filters = templatefilters.filters.copy()
1184 1185 self.filters.update(filters)
1185 1186 self.defaults = defaults
1186 1187 self._aliases = aliases
1187 1188 self.minchunk, self.maxchunk = minchunk, maxchunk
1188 1189 self.ecache = {}
1189 1190
1190 1191 @classmethod
1191 1192 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1192 1193 minchunk=1024, maxchunk=65536):
1193 1194 """Create templater from the specified map file"""
1194 1195 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1195 1196 cache, tmap = _readmapfile(mapfile)
1196 1197 t.cache.update(cache)
1197 1198 t.map = tmap
1198 1199 return t
1199 1200
1200 1201 def __contains__(self, key):
1201 1202 return key in self.cache or key in self.map
1202 1203
1203 1204 def load(self, t):
1204 1205 '''Get the template for the given template name. Use a local cache.'''
1205 1206 if t not in self.cache:
1206 1207 try:
1207 1208 self.cache[t] = util.readfile(self.map[t][1])
1208 1209 except KeyError as inst:
1209 1210 raise TemplateNotFound(_('"%s" not in template map') %
1210 1211 inst.args[0])
1211 1212 except IOError as inst:
1212 1213 raise IOError(inst.args[0], _('template file %s: %s') %
1213 1214 (self.map[t][1], inst.args[1]))
1214 1215 return self.cache[t]
1215 1216
1216 1217 def __call__(self, t, **mapping):
1217 1218 ttype = t in self.map and self.map[t][0] or 'default'
1218 1219 if ttype not in self.ecache:
1219 1220 try:
1220 1221 ecls = engines[ttype]
1221 1222 except KeyError:
1222 1223 raise error.Abort(_('invalid template engine: %s') % ttype)
1223 1224 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1224 1225 self._aliases)
1225 1226 proc = self.ecache[ttype]
1226 1227
1227 1228 stream = proc.process(t, mapping)
1228 1229 if self.minchunk:
1229 1230 stream = util.increasingchunks(stream, min=self.minchunk,
1230 1231 max=self.maxchunk)
1231 1232 return stream
1232 1233
1233 1234 def templatepaths():
1234 1235 '''return locations used for template files.'''
1235 1236 pathsrel = ['templates']
1236 1237 paths = [os.path.normpath(os.path.join(util.datapath, f))
1237 1238 for f in pathsrel]
1238 1239 return [p for p in paths if os.path.isdir(p)]
1239 1240
1240 1241 def templatepath(name):
1241 1242 '''return location of template file. returns None if not found.'''
1242 1243 for p in templatepaths():
1243 1244 f = os.path.join(p, name)
1244 1245 if os.path.exists(f):
1245 1246 return f
1246 1247 return None
1247 1248
1248 1249 def stylemap(styles, paths=None):
1249 1250 """Return path to mapfile for a given style.
1250 1251
1251 1252 Searches mapfile in the following locations:
1252 1253 1. templatepath/style/map
1253 1254 2. templatepath/map-style
1254 1255 3. templatepath/map
1255 1256 """
1256 1257
1257 1258 if paths is None:
1258 1259 paths = templatepaths()
1259 1260 elif isinstance(paths, str):
1260 1261 paths = [paths]
1261 1262
1262 1263 if isinstance(styles, str):
1263 1264 styles = [styles]
1264 1265
1265 1266 for style in styles:
1266 1267 # only plain name is allowed to honor template paths
1267 1268 if (not style
1268 1269 or style in (os.curdir, os.pardir)
1269 1270 or pycompat.ossep in style
1270 1271 or pycompat.osaltsep and pycompat.osaltsep in style):
1271 1272 continue
1272 1273 locations = [os.path.join(style, 'map'), 'map-' + style]
1273 1274 locations.append('map')
1274 1275
1275 1276 for path in paths:
1276 1277 for location in locations:
1277 1278 mapfile = os.path.join(path, location)
1278 1279 if os.path.isfile(mapfile):
1279 1280 return style, mapfile
1280 1281
1281 1282 raise RuntimeError("No hgweb templates found in %r" % paths)
1282 1283
1283 1284 def loadfunction(ui, extname, registrarobj):
1284 1285 """Load template function from specified registrarobj
1285 1286 """
1286 1287 for name, func in registrarobj._table.iteritems():
1287 1288 funcs[name] = func
1288 1289
1289 1290 # tell hggettext to extract docstrings from these functions:
1290 1291 i18nfunctions = funcs.values()
@@ -1,4152 +1,4162
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 Test arithmetic operators have the right precedence:
33 33
34 34 $ hg log -l 1 -T '{date(date, "%Y") + 5 * 10} {date(date, "%Y") - 2 * 3}\n'
35 35 2020 1964
36 36 $ hg log -l 1 -T '{date(date, "%Y") * 5 + 10} {date(date, "%Y") * 3 - 2}\n'
37 37 9860 5908
38 38
39 39 Test division:
40 40
41 41 $ hg debugtemplate -r0 -v '{5 / 2} {mod(5, 2)}\n'
42 42 (template
43 43 (/
44 44 ('integer', '5')
45 45 ('integer', '2'))
46 46 ('string', ' ')
47 47 (func
48 48 ('symbol', 'mod')
49 49 (list
50 50 ('integer', '5')
51 51 ('integer', '2')))
52 52 ('string', '\n'))
53 53 2 1
54 54 $ hg debugtemplate -r0 -v '{5 / -2} {mod(5, -2)}\n'
55 55 (template
56 56 (/
57 57 ('integer', '5')
58 58 (negate
59 59 ('integer', '2')))
60 60 ('string', ' ')
61 61 (func
62 62 ('symbol', 'mod')
63 63 (list
64 64 ('integer', '5')
65 65 (negate
66 66 ('integer', '2'))))
67 67 ('string', '\n'))
68 68 -3 -1
69 69 $ hg debugtemplate -r0 -v '{-5 / 2} {mod(-5, 2)}\n'
70 70 (template
71 71 (/
72 72 (negate
73 73 ('integer', '5'))
74 74 ('integer', '2'))
75 75 ('string', ' ')
76 76 (func
77 77 ('symbol', 'mod')
78 78 (list
79 79 (negate
80 80 ('integer', '5'))
81 81 ('integer', '2')))
82 82 ('string', '\n'))
83 83 -3 1
84 84 $ hg debugtemplate -r0 -v '{-5 / -2} {mod(-5, -2)}\n'
85 85 (template
86 86 (/
87 87 (negate
88 88 ('integer', '5'))
89 89 (negate
90 90 ('integer', '2')))
91 91 ('string', ' ')
92 92 (func
93 93 ('symbol', 'mod')
94 94 (list
95 95 (negate
96 96 ('integer', '5'))
97 97 (negate
98 98 ('integer', '2'))))
99 99 ('string', '\n'))
100 100 2 -1
101 101
102 102 Filters bind closer than arithmetic:
103 103
104 104 $ hg debugtemplate -r0 -v '{revset(".")|count - 1}\n'
105 105 (template
106 106 (-
107 107 (|
108 108 (func
109 109 ('symbol', 'revset')
110 110 ('string', '.'))
111 111 ('symbol', 'count'))
112 112 ('integer', '1'))
113 113 ('string', '\n'))
114 114 0
115 115
116 116 But negate binds closer still:
117 117
118 118 $ hg debugtemplate -r0 -v '{1-3|stringify}\n'
119 119 (template
120 120 (-
121 121 ('integer', '1')
122 122 (|
123 123 ('integer', '3')
124 124 ('symbol', 'stringify')))
125 125 ('string', '\n'))
126 126 hg: parse error: arithmetic only defined on integers
127 127 [255]
128 128 $ hg debugtemplate -r0 -v '{-3|stringify}\n'
129 129 (template
130 130 (|
131 131 (negate
132 132 ('integer', '3'))
133 133 ('symbol', 'stringify'))
134 134 ('string', '\n'))
135 135 -3
136 136
137 137 Second branch starting at nullrev:
138 138
139 139 $ hg update null
140 140 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
141 141 $ echo second > second
142 142 $ hg add second
143 143 $ hg commit -m second -d '1000000 0' -u 'User Name <user@hostname>'
144 144 created new head
145 145
146 146 $ echo third > third
147 147 $ hg add third
148 148 $ hg mv second fourth
149 149 $ hg commit -m third -d "2020-01-01 10:01"
150 150
151 151 $ hg log --template '{join(file_copies, ",\n")}\n' -r .
152 152 fourth (second)
153 153 $ hg log -T '{file_copies % "{source} -> {name}\n"}' -r .
154 154 second -> fourth
155 155 $ hg log -T '{rev} {ifcontains("fourth", file_copies, "t", "f")}\n' -r .:7
156 156 8 t
157 157 7 f
158 158
159 159 Working-directory revision has special identifiers, though they are still
160 160 experimental:
161 161
162 162 $ hg log -r 'wdir()' -T '{rev}:{node}\n'
163 163 2147483647:ffffffffffffffffffffffffffffffffffffffff
164 164
165 165 Some keywords are invalid for working-directory revision, but they should
166 166 never cause crash:
167 167
168 168 $ hg log -r 'wdir()' -T '{manifest}\n'
169 169
170 170
171 171 Quoting for ui.logtemplate
172 172
173 173 $ hg tip --config "ui.logtemplate={rev}\n"
174 174 8
175 175 $ hg tip --config "ui.logtemplate='{rev}\n'"
176 176 8
177 177 $ hg tip --config 'ui.logtemplate="{rev}\n"'
178 178 8
179 179 $ hg tip --config 'ui.logtemplate=n{rev}\n'
180 180 n8
181 181
182 182 Make sure user/global hgrc does not affect tests
183 183
184 184 $ echo '[ui]' > .hg/hgrc
185 185 $ echo 'logtemplate =' >> .hg/hgrc
186 186 $ echo 'style =' >> .hg/hgrc
187 187
188 188 Add some simple styles to settings
189 189
190 190 $ echo '[templates]' >> .hg/hgrc
191 191 $ printf 'simple = "{rev}\\n"\n' >> .hg/hgrc
192 192 $ printf 'simple2 = {rev}\\n\n' >> .hg/hgrc
193 193
194 194 $ hg log -l1 -Tsimple
195 195 8
196 196 $ hg log -l1 -Tsimple2
197 197 8
198 198
199 199 Test templates and style maps in files:
200 200
201 201 $ echo "{rev}" > tmpl
202 202 $ hg log -l1 -T./tmpl
203 203 8
204 204 $ hg log -l1 -Tblah/blah
205 205 blah/blah (no-eol)
206 206
207 207 $ printf 'changeset = "{rev}\\n"\n' > map-simple
208 208 $ hg log -l1 -T./map-simple
209 209 8
210 210
211 211 Test template map inheritance
212 212
213 213 $ echo "__base__ = map-cmdline.default" > map-simple
214 214 $ printf 'cset = "changeset: ***{rev}***\\n"\n' >> map-simple
215 215 $ hg log -l1 -T./map-simple
216 216 changeset: ***8***
217 217 tag: tip
218 218 user: test
219 219 date: Wed Jan 01 10:01:00 2020 +0000
220 220 summary: third
221 221
222 222
223 223 Template should precede style option
224 224
225 225 $ hg log -l1 --style default -T '{rev}\n'
226 226 8
227 227
228 228 Add a commit with empty description, to ensure that the templates
229 229 below will omit the description line.
230 230
231 231 $ echo c >> c
232 232 $ hg add c
233 233 $ hg commit -qm ' '
234 234
235 235 Default style is like normal output. Phases style should be the same
236 236 as default style, except for extra phase lines.
237 237
238 238 $ hg log > log.out
239 239 $ hg log --style default > style.out
240 240 $ cmp log.out style.out || diff -u log.out style.out
241 241 $ hg log -T phases > phases.out
242 242 $ diff -U 0 log.out phases.out | egrep -v '^---|^\+\+\+|^@@'
243 243 +phase: draft
244 244 +phase: draft
245 245 +phase: draft
246 246 +phase: draft
247 247 +phase: draft
248 248 +phase: draft
249 249 +phase: draft
250 250 +phase: draft
251 251 +phase: draft
252 252 +phase: draft
253 253
254 254 $ hg log -v > log.out
255 255 $ hg log -v --style default > style.out
256 256 $ cmp log.out style.out || diff -u log.out style.out
257 257 $ hg log -v -T phases > phases.out
258 258 $ diff -U 0 log.out phases.out | egrep -v '^---|^\+\+\+|^@@'
259 259 +phase: draft
260 260 +phase: draft
261 261 +phase: draft
262 262 +phase: draft
263 263 +phase: draft
264 264 +phase: draft
265 265 +phase: draft
266 266 +phase: draft
267 267 +phase: draft
268 268 +phase: draft
269 269
270 270 $ hg log -q > log.out
271 271 $ hg log -q --style default > style.out
272 272 $ cmp log.out style.out || diff -u log.out style.out
273 273 $ hg log -q -T phases > phases.out
274 274 $ cmp log.out phases.out || diff -u log.out phases.out
275 275
276 276 $ hg log --debug > log.out
277 277 $ hg log --debug --style default > style.out
278 278 $ cmp log.out style.out || diff -u log.out style.out
279 279 $ hg log --debug -T phases > phases.out
280 280 $ cmp log.out phases.out || diff -u log.out phases.out
281 281
282 282 Default style of working-directory revision should also be the same (but
283 283 date may change while running tests):
284 284
285 285 $ hg log -r 'wdir()' | sed 's|^date:.*|date:|' > log.out
286 286 $ hg log -r 'wdir()' --style default | sed 's|^date:.*|date:|' > style.out
287 287 $ cmp log.out style.out || diff -u log.out style.out
288 288
289 289 $ hg log -r 'wdir()' -v | sed 's|^date:.*|date:|' > log.out
290 290 $ hg log -r 'wdir()' -v --style default | sed 's|^date:.*|date:|' > style.out
291 291 $ cmp log.out style.out || diff -u log.out style.out
292 292
293 293 $ hg log -r 'wdir()' -q > log.out
294 294 $ hg log -r 'wdir()' -q --style default > style.out
295 295 $ cmp log.out style.out || diff -u log.out style.out
296 296
297 297 $ hg log -r 'wdir()' --debug | sed 's|^date:.*|date:|' > log.out
298 298 $ hg log -r 'wdir()' --debug --style default \
299 299 > | sed 's|^date:.*|date:|' > style.out
300 300 $ cmp log.out style.out || diff -u log.out style.out
301 301
302 302 Default style should also preserve color information (issue2866):
303 303
304 304 $ cp $HGRCPATH $HGRCPATH-bak
305 305 $ cat <<EOF >> $HGRCPATH
306 306 > [extensions]
307 307 > color=
308 308 > EOF
309 309
310 310 $ hg --color=debug log > log.out
311 311 $ hg --color=debug log --style default > style.out
312 312 $ cmp log.out style.out || diff -u log.out style.out
313 313 $ hg --color=debug log -T phases > phases.out
314 314 $ diff -U 0 log.out phases.out | egrep -v '^---|^\+\+\+|^@@'
315 315 +[log.phase|phase: draft]
316 316 +[log.phase|phase: draft]
317 317 +[log.phase|phase: draft]
318 318 +[log.phase|phase: draft]
319 319 +[log.phase|phase: draft]
320 320 +[log.phase|phase: draft]
321 321 +[log.phase|phase: draft]
322 322 +[log.phase|phase: draft]
323 323 +[log.phase|phase: draft]
324 324 +[log.phase|phase: draft]
325 325
326 326 $ hg --color=debug -v log > log.out
327 327 $ hg --color=debug -v log --style default > style.out
328 328 $ cmp log.out style.out || diff -u log.out style.out
329 329 $ hg --color=debug -v log -T phases > phases.out
330 330 $ diff -U 0 log.out phases.out | egrep -v '^---|^\+\+\+|^@@'
331 331 +[log.phase|phase: draft]
332 332 +[log.phase|phase: draft]
333 333 +[log.phase|phase: draft]
334 334 +[log.phase|phase: draft]
335 335 +[log.phase|phase: draft]
336 336 +[log.phase|phase: draft]
337 337 +[log.phase|phase: draft]
338 338 +[log.phase|phase: draft]
339 339 +[log.phase|phase: draft]
340 340 +[log.phase|phase: draft]
341 341
342 342 $ hg --color=debug -q log > log.out
343 343 $ hg --color=debug -q log --style default > style.out
344 344 $ cmp log.out style.out || diff -u log.out style.out
345 345 $ hg --color=debug -q log -T phases > phases.out
346 346 $ cmp log.out phases.out || diff -u log.out phases.out
347 347
348 348 $ hg --color=debug --debug log > log.out
349 349 $ hg --color=debug --debug log --style default > style.out
350 350 $ cmp log.out style.out || diff -u log.out style.out
351 351 $ hg --color=debug --debug log -T phases > phases.out
352 352 $ cmp log.out phases.out || diff -u log.out phases.out
353 353
354 354 $ mv $HGRCPATH-bak $HGRCPATH
355 355
356 356 Remove commit with empty commit message, so as to not pollute further
357 357 tests.
358 358
359 359 $ hg --config extensions.strip= strip -q .
360 360
361 361 Revision with no copies (used to print a traceback):
362 362
363 363 $ hg tip -v --template '\n'
364 364
365 365
366 366 Compact style works:
367 367
368 368 $ hg log -Tcompact
369 369 8[tip] 95c24699272e 2020-01-01 10:01 +0000 test
370 370 third
371 371
372 372 7:-1 29114dbae42b 1970-01-12 13:46 +0000 user
373 373 second
374 374
375 375 6:5,4 d41e714fe50d 1970-01-18 08:40 +0000 person
376 376 merge
377 377
378 378 5:3 13207e5a10d9 1970-01-18 08:40 +0000 person
379 379 new head
380 380
381 381 4 bbe44766e73d 1970-01-17 04:53 +0000 person
382 382 new branch
383 383
384 384 3 10e46f2dcbf4 1970-01-16 01:06 +0000 person
385 385 no user, no domain
386 386
387 387 2 97054abb4ab8 1970-01-14 21:20 +0000 other
388 388 no person
389 389
390 390 1 b608e9d1a3f0 1970-01-13 17:33 +0000 other
391 391 other 1
392 392
393 393 0 1e4e1b8f71e0 1970-01-12 13:46 +0000 user
394 394 line 1
395 395
396 396
397 397 $ hg log -v --style compact
398 398 8[tip] 95c24699272e 2020-01-01 10:01 +0000 test
399 399 third
400 400
401 401 7:-1 29114dbae42b 1970-01-12 13:46 +0000 User Name <user@hostname>
402 402 second
403 403
404 404 6:5,4 d41e714fe50d 1970-01-18 08:40 +0000 person
405 405 merge
406 406
407 407 5:3 13207e5a10d9 1970-01-18 08:40 +0000 person
408 408 new head
409 409
410 410 4 bbe44766e73d 1970-01-17 04:53 +0000 person
411 411 new branch
412 412
413 413 3 10e46f2dcbf4 1970-01-16 01:06 +0000 person
414 414 no user, no domain
415 415
416 416 2 97054abb4ab8 1970-01-14 21:20 +0000 other@place
417 417 no person
418 418
419 419 1 b608e9d1a3f0 1970-01-13 17:33 +0000 A. N. Other <other@place>
420 420 other 1
421 421 other 2
422 422
423 423 other 3
424 424
425 425 0 1e4e1b8f71e0 1970-01-12 13:46 +0000 User Name <user@hostname>
426 426 line 1
427 427 line 2
428 428
429 429
430 430 $ hg log --debug --style compact
431 431 8[tip]:7,-1 95c24699272e 2020-01-01 10:01 +0000 test
432 432 third
433 433
434 434 7:-1,-1 29114dbae42b 1970-01-12 13:46 +0000 User Name <user@hostname>
435 435 second
436 436
437 437 6:5,4 d41e714fe50d 1970-01-18 08:40 +0000 person
438 438 merge
439 439
440 440 5:3,-1 13207e5a10d9 1970-01-18 08:40 +0000 person
441 441 new head
442 442
443 443 4:3,-1 bbe44766e73d 1970-01-17 04:53 +0000 person
444 444 new branch
445 445
446 446 3:2,-1 10e46f2dcbf4 1970-01-16 01:06 +0000 person
447 447 no user, no domain
448 448
449 449 2:1,-1 97054abb4ab8 1970-01-14 21:20 +0000 other@place
450 450 no person
451 451
452 452 1:0,-1 b608e9d1a3f0 1970-01-13 17:33 +0000 A. N. Other <other@place>
453 453 other 1
454 454 other 2
455 455
456 456 other 3
457 457
458 458 0:-1,-1 1e4e1b8f71e0 1970-01-12 13:46 +0000 User Name <user@hostname>
459 459 line 1
460 460 line 2
461 461
462 462
463 463 Test xml styles:
464 464
465 465 $ hg log --style xml -r 'not all()'
466 466 <?xml version="1.0"?>
467 467 <log>
468 468 </log>
469 469
470 470 $ hg log --style xml
471 471 <?xml version="1.0"?>
472 472 <log>
473 473 <logentry revision="8" node="95c24699272ef57d062b8bccc32c878bf841784a">
474 474 <tag>tip</tag>
475 475 <author email="test">test</author>
476 476 <date>2020-01-01T10:01:00+00:00</date>
477 477 <msg xml:space="preserve">third</msg>
478 478 </logentry>
479 479 <logentry revision="7" node="29114dbae42b9f078cf2714dbe3a86bba8ec7453">
480 480 <parent revision="-1" node="0000000000000000000000000000000000000000" />
481 481 <author email="user@hostname">User Name</author>
482 482 <date>1970-01-12T13:46:40+00:00</date>
483 483 <msg xml:space="preserve">second</msg>
484 484 </logentry>
485 485 <logentry revision="6" node="d41e714fe50d9e4a5f11b4d595d543481b5f980b">
486 486 <parent revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f" />
487 487 <parent revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74" />
488 488 <author email="person">person</author>
489 489 <date>1970-01-18T08:40:01+00:00</date>
490 490 <msg xml:space="preserve">merge</msg>
491 491 </logentry>
492 492 <logentry revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f">
493 493 <parent revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47" />
494 494 <author email="person">person</author>
495 495 <date>1970-01-18T08:40:00+00:00</date>
496 496 <msg xml:space="preserve">new head</msg>
497 497 </logentry>
498 498 <logentry revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74">
499 499 <branch>foo</branch>
500 500 <author email="person">person</author>
501 501 <date>1970-01-17T04:53:20+00:00</date>
502 502 <msg xml:space="preserve">new branch</msg>
503 503 </logentry>
504 504 <logentry revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47">
505 505 <author email="person">person</author>
506 506 <date>1970-01-16T01:06:40+00:00</date>
507 507 <msg xml:space="preserve">no user, no domain</msg>
508 508 </logentry>
509 509 <logentry revision="2" node="97054abb4ab824450e9164180baf491ae0078465">
510 510 <author email="other@place">other</author>
511 511 <date>1970-01-14T21:20:00+00:00</date>
512 512 <msg xml:space="preserve">no person</msg>
513 513 </logentry>
514 514 <logentry revision="1" node="b608e9d1a3f0273ccf70fb85fd6866b3482bf965">
515 515 <author email="other@place">A. N. Other</author>
516 516 <date>1970-01-13T17:33:20+00:00</date>
517 517 <msg xml:space="preserve">other 1
518 518 other 2
519 519
520 520 other 3</msg>
521 521 </logentry>
522 522 <logentry revision="0" node="1e4e1b8f71e05681d422154f5421e385fec3454f">
523 523 <author email="user@hostname">User Name</author>
524 524 <date>1970-01-12T13:46:40+00:00</date>
525 525 <msg xml:space="preserve">line 1
526 526 line 2</msg>
527 527 </logentry>
528 528 </log>
529 529
530 530 $ hg log -v --style xml
531 531 <?xml version="1.0"?>
532 532 <log>
533 533 <logentry revision="8" node="95c24699272ef57d062b8bccc32c878bf841784a">
534 534 <tag>tip</tag>
535 535 <author email="test">test</author>
536 536 <date>2020-01-01T10:01:00+00:00</date>
537 537 <msg xml:space="preserve">third</msg>
538 538 <paths>
539 539 <path action="A">fourth</path>
540 540 <path action="A">third</path>
541 541 <path action="R">second</path>
542 542 </paths>
543 543 <copies>
544 544 <copy source="second">fourth</copy>
545 545 </copies>
546 546 </logentry>
547 547 <logentry revision="7" node="29114dbae42b9f078cf2714dbe3a86bba8ec7453">
548 548 <parent revision="-1" node="0000000000000000000000000000000000000000" />
549 549 <author email="user@hostname">User Name</author>
550 550 <date>1970-01-12T13:46:40+00:00</date>
551 551 <msg xml:space="preserve">second</msg>
552 552 <paths>
553 553 <path action="A">second</path>
554 554 </paths>
555 555 </logentry>
556 556 <logentry revision="6" node="d41e714fe50d9e4a5f11b4d595d543481b5f980b">
557 557 <parent revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f" />
558 558 <parent revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74" />
559 559 <author email="person">person</author>
560 560 <date>1970-01-18T08:40:01+00:00</date>
561 561 <msg xml:space="preserve">merge</msg>
562 562 <paths>
563 563 </paths>
564 564 </logentry>
565 565 <logentry revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f">
566 566 <parent revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47" />
567 567 <author email="person">person</author>
568 568 <date>1970-01-18T08:40:00+00:00</date>
569 569 <msg xml:space="preserve">new head</msg>
570 570 <paths>
571 571 <path action="A">d</path>
572 572 </paths>
573 573 </logentry>
574 574 <logentry revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74">
575 575 <branch>foo</branch>
576 576 <author email="person">person</author>
577 577 <date>1970-01-17T04:53:20+00:00</date>
578 578 <msg xml:space="preserve">new branch</msg>
579 579 <paths>
580 580 </paths>
581 581 </logentry>
582 582 <logentry revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47">
583 583 <author email="person">person</author>
584 584 <date>1970-01-16T01:06:40+00:00</date>
585 585 <msg xml:space="preserve">no user, no domain</msg>
586 586 <paths>
587 587 <path action="M">c</path>
588 588 </paths>
589 589 </logentry>
590 590 <logentry revision="2" node="97054abb4ab824450e9164180baf491ae0078465">
591 591 <author email="other@place">other</author>
592 592 <date>1970-01-14T21:20:00+00:00</date>
593 593 <msg xml:space="preserve">no person</msg>
594 594 <paths>
595 595 <path action="A">c</path>
596 596 </paths>
597 597 </logentry>
598 598 <logentry revision="1" node="b608e9d1a3f0273ccf70fb85fd6866b3482bf965">
599 599 <author email="other@place">A. N. Other</author>
600 600 <date>1970-01-13T17:33:20+00:00</date>
601 601 <msg xml:space="preserve">other 1
602 602 other 2
603 603
604 604 other 3</msg>
605 605 <paths>
606 606 <path action="A">b</path>
607 607 </paths>
608 608 </logentry>
609 609 <logentry revision="0" node="1e4e1b8f71e05681d422154f5421e385fec3454f">
610 610 <author email="user@hostname">User Name</author>
611 611 <date>1970-01-12T13:46:40+00:00</date>
612 612 <msg xml:space="preserve">line 1
613 613 line 2</msg>
614 614 <paths>
615 615 <path action="A">a</path>
616 616 </paths>
617 617 </logentry>
618 618 </log>
619 619
620 620 $ hg log --debug --style xml
621 621 <?xml version="1.0"?>
622 622 <log>
623 623 <logentry revision="8" node="95c24699272ef57d062b8bccc32c878bf841784a">
624 624 <tag>tip</tag>
625 625 <parent revision="7" node="29114dbae42b9f078cf2714dbe3a86bba8ec7453" />
626 626 <parent revision="-1" node="0000000000000000000000000000000000000000" />
627 627 <author email="test">test</author>
628 628 <date>2020-01-01T10:01:00+00:00</date>
629 629 <msg xml:space="preserve">third</msg>
630 630 <paths>
631 631 <path action="A">fourth</path>
632 632 <path action="A">third</path>
633 633 <path action="R">second</path>
634 634 </paths>
635 635 <copies>
636 636 <copy source="second">fourth</copy>
637 637 </copies>
638 638 <extra key="branch">default</extra>
639 639 </logentry>
640 640 <logentry revision="7" node="29114dbae42b9f078cf2714dbe3a86bba8ec7453">
641 641 <parent revision="-1" node="0000000000000000000000000000000000000000" />
642 642 <parent revision="-1" node="0000000000000000000000000000000000000000" />
643 643 <author email="user@hostname">User Name</author>
644 644 <date>1970-01-12T13:46:40+00:00</date>
645 645 <msg xml:space="preserve">second</msg>
646 646 <paths>
647 647 <path action="A">second</path>
648 648 </paths>
649 649 <extra key="branch">default</extra>
650 650 </logentry>
651 651 <logentry revision="6" node="d41e714fe50d9e4a5f11b4d595d543481b5f980b">
652 652 <parent revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f" />
653 653 <parent revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74" />
654 654 <author email="person">person</author>
655 655 <date>1970-01-18T08:40:01+00:00</date>
656 656 <msg xml:space="preserve">merge</msg>
657 657 <paths>
658 658 </paths>
659 659 <extra key="branch">default</extra>
660 660 </logentry>
661 661 <logentry revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f">
662 662 <parent revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47" />
663 663 <parent revision="-1" node="0000000000000000000000000000000000000000" />
664 664 <author email="person">person</author>
665 665 <date>1970-01-18T08:40:00+00:00</date>
666 666 <msg xml:space="preserve">new head</msg>
667 667 <paths>
668 668 <path action="A">d</path>
669 669 </paths>
670 670 <extra key="branch">default</extra>
671 671 </logentry>
672 672 <logentry revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74">
673 673 <branch>foo</branch>
674 674 <parent revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47" />
675 675 <parent revision="-1" node="0000000000000000000000000000000000000000" />
676 676 <author email="person">person</author>
677 677 <date>1970-01-17T04:53:20+00:00</date>
678 678 <msg xml:space="preserve">new branch</msg>
679 679 <paths>
680 680 </paths>
681 681 <extra key="branch">foo</extra>
682 682 </logentry>
683 683 <logentry revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47">
684 684 <parent revision="2" node="97054abb4ab824450e9164180baf491ae0078465" />
685 685 <parent revision="-1" node="0000000000000000000000000000000000000000" />
686 686 <author email="person">person</author>
687 687 <date>1970-01-16T01:06:40+00:00</date>
688 688 <msg xml:space="preserve">no user, no domain</msg>
689 689 <paths>
690 690 <path action="M">c</path>
691 691 </paths>
692 692 <extra key="branch">default</extra>
693 693 </logentry>
694 694 <logentry revision="2" node="97054abb4ab824450e9164180baf491ae0078465">
695 695 <parent revision="1" node="b608e9d1a3f0273ccf70fb85fd6866b3482bf965" />
696 696 <parent revision="-1" node="0000000000000000000000000000000000000000" />
697 697 <author email="other@place">other</author>
698 698 <date>1970-01-14T21:20:00+00:00</date>
699 699 <msg xml:space="preserve">no person</msg>
700 700 <paths>
701 701 <path action="A">c</path>
702 702 </paths>
703 703 <extra key="branch">default</extra>
704 704 </logentry>
705 705 <logentry revision="1" node="b608e9d1a3f0273ccf70fb85fd6866b3482bf965">
706 706 <parent revision="0" node="1e4e1b8f71e05681d422154f5421e385fec3454f" />
707 707 <parent revision="-1" node="0000000000000000000000000000000000000000" />
708 708 <author email="other@place">A. N. Other</author>
709 709 <date>1970-01-13T17:33:20+00:00</date>
710 710 <msg xml:space="preserve">other 1
711 711 other 2
712 712
713 713 other 3</msg>
714 714 <paths>
715 715 <path action="A">b</path>
716 716 </paths>
717 717 <extra key="branch">default</extra>
718 718 </logentry>
719 719 <logentry revision="0" node="1e4e1b8f71e05681d422154f5421e385fec3454f">
720 720 <parent revision="-1" node="0000000000000000000000000000000000000000" />
721 721 <parent revision="-1" node="0000000000000000000000000000000000000000" />
722 722 <author email="user@hostname">User Name</author>
723 723 <date>1970-01-12T13:46:40+00:00</date>
724 724 <msg xml:space="preserve">line 1
725 725 line 2</msg>
726 726 <paths>
727 727 <path action="A">a</path>
728 728 </paths>
729 729 <extra key="branch">default</extra>
730 730 </logentry>
731 731 </log>
732 732
733 733
734 734 Test JSON style:
735 735
736 736 $ hg log -k nosuch -Tjson
737 737 []
738 738
739 739 $ hg log -qr . -Tjson
740 740 [
741 741 {
742 742 "rev": 8,
743 743 "node": "95c24699272ef57d062b8bccc32c878bf841784a"
744 744 }
745 745 ]
746 746
747 747 $ hg log -vpr . -Tjson --stat
748 748 [
749 749 {
750 750 "rev": 8,
751 751 "node": "95c24699272ef57d062b8bccc32c878bf841784a",
752 752 "branch": "default",
753 753 "phase": "draft",
754 754 "user": "test",
755 755 "date": [1577872860, 0],
756 756 "desc": "third",
757 757 "bookmarks": [],
758 758 "tags": ["tip"],
759 759 "parents": ["29114dbae42b9f078cf2714dbe3a86bba8ec7453"],
760 760 "files": ["fourth", "second", "third"],
761 761 "diffstat": " fourth | 1 +\n second | 1 -\n third | 1 +\n 3 files changed, 2 insertions(+), 1 deletions(-)\n",
762 762 "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"
763 763 }
764 764 ]
765 765
766 766 honor --git but not format-breaking diffopts
767 767 $ hg --config diff.noprefix=True log --git -vpr . -Tjson
768 768 [
769 769 {
770 770 "rev": 8,
771 771 "node": "95c24699272ef57d062b8bccc32c878bf841784a",
772 772 "branch": "default",
773 773 "phase": "draft",
774 774 "user": "test",
775 775 "date": [1577872860, 0],
776 776 "desc": "third",
777 777 "bookmarks": [],
778 778 "tags": ["tip"],
779 779 "parents": ["29114dbae42b9f078cf2714dbe3a86bba8ec7453"],
780 780 "files": ["fourth", "second", "third"],
781 781 "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"
782 782 }
783 783 ]
784 784
785 785 $ hg log -T json
786 786 [
787 787 {
788 788 "rev": 8,
789 789 "node": "95c24699272ef57d062b8bccc32c878bf841784a",
790 790 "branch": "default",
791 791 "phase": "draft",
792 792 "user": "test",
793 793 "date": [1577872860, 0],
794 794 "desc": "third",
795 795 "bookmarks": [],
796 796 "tags": ["tip"],
797 797 "parents": ["29114dbae42b9f078cf2714dbe3a86bba8ec7453"]
798 798 },
799 799 {
800 800 "rev": 7,
801 801 "node": "29114dbae42b9f078cf2714dbe3a86bba8ec7453",
802 802 "branch": "default",
803 803 "phase": "draft",
804 804 "user": "User Name <user@hostname>",
805 805 "date": [1000000, 0],
806 806 "desc": "second",
807 807 "bookmarks": [],
808 808 "tags": [],
809 809 "parents": ["0000000000000000000000000000000000000000"]
810 810 },
811 811 {
812 812 "rev": 6,
813 813 "node": "d41e714fe50d9e4a5f11b4d595d543481b5f980b",
814 814 "branch": "default",
815 815 "phase": "draft",
816 816 "user": "person",
817 817 "date": [1500001, 0],
818 818 "desc": "merge",
819 819 "bookmarks": [],
820 820 "tags": [],
821 821 "parents": ["13207e5a10d9fd28ec424934298e176197f2c67f", "bbe44766e73d5f11ed2177f1838de10c53ef3e74"]
822 822 },
823 823 {
824 824 "rev": 5,
825 825 "node": "13207e5a10d9fd28ec424934298e176197f2c67f",
826 826 "branch": "default",
827 827 "phase": "draft",
828 828 "user": "person",
829 829 "date": [1500000, 0],
830 830 "desc": "new head",
831 831 "bookmarks": [],
832 832 "tags": [],
833 833 "parents": ["10e46f2dcbf4823578cf180f33ecf0b957964c47"]
834 834 },
835 835 {
836 836 "rev": 4,
837 837 "node": "bbe44766e73d5f11ed2177f1838de10c53ef3e74",
838 838 "branch": "foo",
839 839 "phase": "draft",
840 840 "user": "person",
841 841 "date": [1400000, 0],
842 842 "desc": "new branch",
843 843 "bookmarks": [],
844 844 "tags": [],
845 845 "parents": ["10e46f2dcbf4823578cf180f33ecf0b957964c47"]
846 846 },
847 847 {
848 848 "rev": 3,
849 849 "node": "10e46f2dcbf4823578cf180f33ecf0b957964c47",
850 850 "branch": "default",
851 851 "phase": "draft",
852 852 "user": "person",
853 853 "date": [1300000, 0],
854 854 "desc": "no user, no domain",
855 855 "bookmarks": [],
856 856 "tags": [],
857 857 "parents": ["97054abb4ab824450e9164180baf491ae0078465"]
858 858 },
859 859 {
860 860 "rev": 2,
861 861 "node": "97054abb4ab824450e9164180baf491ae0078465",
862 862 "branch": "default",
863 863 "phase": "draft",
864 864 "user": "other@place",
865 865 "date": [1200000, 0],
866 866 "desc": "no person",
867 867 "bookmarks": [],
868 868 "tags": [],
869 869 "parents": ["b608e9d1a3f0273ccf70fb85fd6866b3482bf965"]
870 870 },
871 871 {
872 872 "rev": 1,
873 873 "node": "b608e9d1a3f0273ccf70fb85fd6866b3482bf965",
874 874 "branch": "default",
875 875 "phase": "draft",
876 876 "user": "A. N. Other <other@place>",
877 877 "date": [1100000, 0],
878 878 "desc": "other 1\nother 2\n\nother 3",
879 879 "bookmarks": [],
880 880 "tags": [],
881 881 "parents": ["1e4e1b8f71e05681d422154f5421e385fec3454f"]
882 882 },
883 883 {
884 884 "rev": 0,
885 885 "node": "1e4e1b8f71e05681d422154f5421e385fec3454f",
886 886 "branch": "default",
887 887 "phase": "draft",
888 888 "user": "User Name <user@hostname>",
889 889 "date": [1000000, 0],
890 890 "desc": "line 1\nline 2",
891 891 "bookmarks": [],
892 892 "tags": [],
893 893 "parents": ["0000000000000000000000000000000000000000"]
894 894 }
895 895 ]
896 896
897 897 $ hg heads -v -Tjson
898 898 [
899 899 {
900 900 "rev": 8,
901 901 "node": "95c24699272ef57d062b8bccc32c878bf841784a",
902 902 "branch": "default",
903 903 "phase": "draft",
904 904 "user": "test",
905 905 "date": [1577872860, 0],
906 906 "desc": "third",
907 907 "bookmarks": [],
908 908 "tags": ["tip"],
909 909 "parents": ["29114dbae42b9f078cf2714dbe3a86bba8ec7453"],
910 910 "files": ["fourth", "second", "third"]
911 911 },
912 912 {
913 913 "rev": 6,
914 914 "node": "d41e714fe50d9e4a5f11b4d595d543481b5f980b",
915 915 "branch": "default",
916 916 "phase": "draft",
917 917 "user": "person",
918 918 "date": [1500001, 0],
919 919 "desc": "merge",
920 920 "bookmarks": [],
921 921 "tags": [],
922 922 "parents": ["13207e5a10d9fd28ec424934298e176197f2c67f", "bbe44766e73d5f11ed2177f1838de10c53ef3e74"],
923 923 "files": []
924 924 },
925 925 {
926 926 "rev": 4,
927 927 "node": "bbe44766e73d5f11ed2177f1838de10c53ef3e74",
928 928 "branch": "foo",
929 929 "phase": "draft",
930 930 "user": "person",
931 931 "date": [1400000, 0],
932 932 "desc": "new branch",
933 933 "bookmarks": [],
934 934 "tags": [],
935 935 "parents": ["10e46f2dcbf4823578cf180f33ecf0b957964c47"],
936 936 "files": []
937 937 }
938 938 ]
939 939
940 940 $ hg log --debug -Tjson
941 941 [
942 942 {
943 943 "rev": 8,
944 944 "node": "95c24699272ef57d062b8bccc32c878bf841784a",
945 945 "branch": "default",
946 946 "phase": "draft",
947 947 "user": "test",
948 948 "date": [1577872860, 0],
949 949 "desc": "third",
950 950 "bookmarks": [],
951 951 "tags": ["tip"],
952 952 "parents": ["29114dbae42b9f078cf2714dbe3a86bba8ec7453"],
953 953 "manifest": "94961b75a2da554b4df6fb599e5bfc7d48de0c64",
954 954 "extra": {"branch": "default"},
955 955 "modified": [],
956 956 "added": ["fourth", "third"],
957 957 "removed": ["second"]
958 958 },
959 959 {
960 960 "rev": 7,
961 961 "node": "29114dbae42b9f078cf2714dbe3a86bba8ec7453",
962 962 "branch": "default",
963 963 "phase": "draft",
964 964 "user": "User Name <user@hostname>",
965 965 "date": [1000000, 0],
966 966 "desc": "second",
967 967 "bookmarks": [],
968 968 "tags": [],
969 969 "parents": ["0000000000000000000000000000000000000000"],
970 970 "manifest": "f2dbc354b94e5ec0b4f10680ee0cee816101d0bf",
971 971 "extra": {"branch": "default"},
972 972 "modified": [],
973 973 "added": ["second"],
974 974 "removed": []
975 975 },
976 976 {
977 977 "rev": 6,
978 978 "node": "d41e714fe50d9e4a5f11b4d595d543481b5f980b",
979 979 "branch": "default",
980 980 "phase": "draft",
981 981 "user": "person",
982 982 "date": [1500001, 0],
983 983 "desc": "merge",
984 984 "bookmarks": [],
985 985 "tags": [],
986 986 "parents": ["13207e5a10d9fd28ec424934298e176197f2c67f", "bbe44766e73d5f11ed2177f1838de10c53ef3e74"],
987 987 "manifest": "4dc3def4f9b4c6e8de820f6ee74737f91e96a216",
988 988 "extra": {"branch": "default"},
989 989 "modified": [],
990 990 "added": [],
991 991 "removed": []
992 992 },
993 993 {
994 994 "rev": 5,
995 995 "node": "13207e5a10d9fd28ec424934298e176197f2c67f",
996 996 "branch": "default",
997 997 "phase": "draft",
998 998 "user": "person",
999 999 "date": [1500000, 0],
1000 1000 "desc": "new head",
1001 1001 "bookmarks": [],
1002 1002 "tags": [],
1003 1003 "parents": ["10e46f2dcbf4823578cf180f33ecf0b957964c47"],
1004 1004 "manifest": "4dc3def4f9b4c6e8de820f6ee74737f91e96a216",
1005 1005 "extra": {"branch": "default"},
1006 1006 "modified": [],
1007 1007 "added": ["d"],
1008 1008 "removed": []
1009 1009 },
1010 1010 {
1011 1011 "rev": 4,
1012 1012 "node": "bbe44766e73d5f11ed2177f1838de10c53ef3e74",
1013 1013 "branch": "foo",
1014 1014 "phase": "draft",
1015 1015 "user": "person",
1016 1016 "date": [1400000, 0],
1017 1017 "desc": "new branch",
1018 1018 "bookmarks": [],
1019 1019 "tags": [],
1020 1020 "parents": ["10e46f2dcbf4823578cf180f33ecf0b957964c47"],
1021 1021 "manifest": "cb5a1327723bada42f117e4c55a303246eaf9ccc",
1022 1022 "extra": {"branch": "foo"},
1023 1023 "modified": [],
1024 1024 "added": [],
1025 1025 "removed": []
1026 1026 },
1027 1027 {
1028 1028 "rev": 3,
1029 1029 "node": "10e46f2dcbf4823578cf180f33ecf0b957964c47",
1030 1030 "branch": "default",
1031 1031 "phase": "draft",
1032 1032 "user": "person",
1033 1033 "date": [1300000, 0],
1034 1034 "desc": "no user, no domain",
1035 1035 "bookmarks": [],
1036 1036 "tags": [],
1037 1037 "parents": ["97054abb4ab824450e9164180baf491ae0078465"],
1038 1038 "manifest": "cb5a1327723bada42f117e4c55a303246eaf9ccc",
1039 1039 "extra": {"branch": "default"},
1040 1040 "modified": ["c"],
1041 1041 "added": [],
1042 1042 "removed": []
1043 1043 },
1044 1044 {
1045 1045 "rev": 2,
1046 1046 "node": "97054abb4ab824450e9164180baf491ae0078465",
1047 1047 "branch": "default",
1048 1048 "phase": "draft",
1049 1049 "user": "other@place",
1050 1050 "date": [1200000, 0],
1051 1051 "desc": "no person",
1052 1052 "bookmarks": [],
1053 1053 "tags": [],
1054 1054 "parents": ["b608e9d1a3f0273ccf70fb85fd6866b3482bf965"],
1055 1055 "manifest": "6e0e82995c35d0d57a52aca8da4e56139e06b4b1",
1056 1056 "extra": {"branch": "default"},
1057 1057 "modified": [],
1058 1058 "added": ["c"],
1059 1059 "removed": []
1060 1060 },
1061 1061 {
1062 1062 "rev": 1,
1063 1063 "node": "b608e9d1a3f0273ccf70fb85fd6866b3482bf965",
1064 1064 "branch": "default",
1065 1065 "phase": "draft",
1066 1066 "user": "A. N. Other <other@place>",
1067 1067 "date": [1100000, 0],
1068 1068 "desc": "other 1\nother 2\n\nother 3",
1069 1069 "bookmarks": [],
1070 1070 "tags": [],
1071 1071 "parents": ["1e4e1b8f71e05681d422154f5421e385fec3454f"],
1072 1072 "manifest": "4e8d705b1e53e3f9375e0e60dc7b525d8211fe55",
1073 1073 "extra": {"branch": "default"},
1074 1074 "modified": [],
1075 1075 "added": ["b"],
1076 1076 "removed": []
1077 1077 },
1078 1078 {
1079 1079 "rev": 0,
1080 1080 "node": "1e4e1b8f71e05681d422154f5421e385fec3454f",
1081 1081 "branch": "default",
1082 1082 "phase": "draft",
1083 1083 "user": "User Name <user@hostname>",
1084 1084 "date": [1000000, 0],
1085 1085 "desc": "line 1\nline 2",
1086 1086 "bookmarks": [],
1087 1087 "tags": [],
1088 1088 "parents": ["0000000000000000000000000000000000000000"],
1089 1089 "manifest": "a0c8bcbbb45c63b90b70ad007bf38961f64f2af0",
1090 1090 "extra": {"branch": "default"},
1091 1091 "modified": [],
1092 1092 "added": ["a"],
1093 1093 "removed": []
1094 1094 }
1095 1095 ]
1096 1096
1097 1097 Error if style not readable:
1098 1098
1099 1099 #if unix-permissions no-root
1100 1100 $ touch q
1101 1101 $ chmod 0 q
1102 1102 $ hg log --style ./q
1103 1103 abort: Permission denied: ./q
1104 1104 [255]
1105 1105 #endif
1106 1106
1107 1107 Error if no style:
1108 1108
1109 1109 $ hg log --style notexist
1110 1110 abort: style 'notexist' not found
1111 1111 (available styles: bisect, changelog, compact, default, phases, show, status, xml)
1112 1112 [255]
1113 1113
1114 1114 $ hg log -T list
1115 1115 available styles: bisect, changelog, compact, default, phases, show, status, xml
1116 1116 abort: specify a template
1117 1117 [255]
1118 1118
1119 1119 Error if style missing key:
1120 1120
1121 1121 $ echo 'q = q' > t
1122 1122 $ hg log --style ./t
1123 1123 abort: "changeset" not in template map
1124 1124 [255]
1125 1125
1126 1126 Error if style missing value:
1127 1127
1128 1128 $ echo 'changeset =' > t
1129 1129 $ hg log --style t
1130 1130 hg: parse error at t:1: missing value
1131 1131 [255]
1132 1132
1133 1133 Error if include fails:
1134 1134
1135 1135 $ echo 'changeset = q' >> t
1136 1136 #if unix-permissions no-root
1137 1137 $ hg log --style ./t
1138 1138 abort: template file ./q: Permission denied
1139 1139 [255]
1140 1140 $ rm -f q
1141 1141 #endif
1142 1142
1143 1143 Include works:
1144 1144
1145 1145 $ echo '{rev}' > q
1146 1146 $ hg log --style ./t
1147 1147 8
1148 1148 7
1149 1149 6
1150 1150 5
1151 1151 4
1152 1152 3
1153 1153 2
1154 1154 1
1155 1155 0
1156 1156
1157 1157 Check that recursive reference does not fall into RuntimeError (issue4758):
1158 1158
1159 1159 common mistake:
1160 1160
1161 1161 $ hg log -T '{changeset}\n'
1162 1162 abort: recursive reference 'changeset' in template
1163 1163 [255]
1164 1164
1165 1165 circular reference:
1166 1166
1167 1167 $ cat << EOF > issue4758
1168 1168 > changeset = '{foo}'
1169 1169 > foo = '{changeset}'
1170 1170 > EOF
1171 1171 $ hg log --style ./issue4758
1172 1172 abort: recursive reference 'foo' in template
1173 1173 [255]
1174 1174
1175 1175 buildmap() -> gettemplate(), where no thunk was made:
1176 1176
1177 1177 $ hg log -T '{files % changeset}\n'
1178 1178 abort: recursive reference 'changeset' in template
1179 1179 [255]
1180 1180
1181 1181 not a recursion if a keyword of the same name exists:
1182 1182
1183 1183 $ cat << EOF > issue4758
1184 1184 > changeset = '{tags % rev}'
1185 1185 > rev = '{rev} {tag}\n'
1186 1186 > EOF
1187 1187 $ hg log --style ./issue4758 -r tip
1188 1188 8 tip
1189 1189
1190 1190 Check that {phase} works correctly on parents:
1191 1191
1192 1192 $ cat << EOF > parentphase
1193 1193 > changeset_debug = '{rev} ({phase}):{parents}\n'
1194 1194 > parent = ' {rev} ({phase})'
1195 1195 > EOF
1196 1196 $ hg phase -r 5 --public
1197 1197 $ hg phase -r 7 --secret --force
1198 1198 $ hg log --debug -G --style ./parentphase
1199 1199 @ 8 (secret): 7 (secret) -1 (public)
1200 1200 |
1201 1201 o 7 (secret): -1 (public) -1 (public)
1202 1202
1203 1203 o 6 (draft): 5 (public) 4 (draft)
1204 1204 |\
1205 1205 | o 5 (public): 3 (public) -1 (public)
1206 1206 | |
1207 1207 o | 4 (draft): 3 (public) -1 (public)
1208 1208 |/
1209 1209 o 3 (public): 2 (public) -1 (public)
1210 1210 |
1211 1211 o 2 (public): 1 (public) -1 (public)
1212 1212 |
1213 1213 o 1 (public): 0 (public) -1 (public)
1214 1214 |
1215 1215 o 0 (public): -1 (public) -1 (public)
1216 1216
1217 1217
1218 1218 Missing non-standard names give no error (backward compatibility):
1219 1219
1220 1220 $ echo "changeset = '{c}'" > t
1221 1221 $ hg log --style ./t
1222 1222
1223 1223 Defining non-standard name works:
1224 1224
1225 1225 $ cat <<EOF > t
1226 1226 > changeset = '{c}'
1227 1227 > c = q
1228 1228 > EOF
1229 1229 $ hg log --style ./t
1230 1230 8
1231 1231 7
1232 1232 6
1233 1233 5
1234 1234 4
1235 1235 3
1236 1236 2
1237 1237 1
1238 1238 0
1239 1239
1240 1240 ui.style works:
1241 1241
1242 1242 $ echo '[ui]' > .hg/hgrc
1243 1243 $ echo 'style = t' >> .hg/hgrc
1244 1244 $ hg log
1245 1245 8
1246 1246 7
1247 1247 6
1248 1248 5
1249 1249 4
1250 1250 3
1251 1251 2
1252 1252 1
1253 1253 0
1254 1254
1255 1255
1256 1256 Issue338:
1257 1257
1258 1258 $ hg log --style=changelog > changelog
1259 1259
1260 1260 $ cat changelog
1261 1261 2020-01-01 test <test>
1262 1262
1263 1263 * fourth, second, third:
1264 1264 third
1265 1265 [95c24699272e] [tip]
1266 1266
1267 1267 1970-01-12 User Name <user@hostname>
1268 1268
1269 1269 * second:
1270 1270 second
1271 1271 [29114dbae42b]
1272 1272
1273 1273 1970-01-18 person <person>
1274 1274
1275 1275 * merge
1276 1276 [d41e714fe50d]
1277 1277
1278 1278 * d:
1279 1279 new head
1280 1280 [13207e5a10d9]
1281 1281
1282 1282 1970-01-17 person <person>
1283 1283
1284 1284 * new branch
1285 1285 [bbe44766e73d] <foo>
1286 1286
1287 1287 1970-01-16 person <person>
1288 1288
1289 1289 * c:
1290 1290 no user, no domain
1291 1291 [10e46f2dcbf4]
1292 1292
1293 1293 1970-01-14 other <other@place>
1294 1294
1295 1295 * c:
1296 1296 no person
1297 1297 [97054abb4ab8]
1298 1298
1299 1299 1970-01-13 A. N. Other <other@place>
1300 1300
1301 1301 * b:
1302 1302 other 1 other 2
1303 1303
1304 1304 other 3
1305 1305 [b608e9d1a3f0]
1306 1306
1307 1307 1970-01-12 User Name <user@hostname>
1308 1308
1309 1309 * a:
1310 1310 line 1 line 2
1311 1311 [1e4e1b8f71e0]
1312 1312
1313 1313
1314 1314 Issue2130: xml output for 'hg heads' is malformed
1315 1315
1316 1316 $ hg heads --style changelog
1317 1317 2020-01-01 test <test>
1318 1318
1319 1319 * fourth, second, third:
1320 1320 third
1321 1321 [95c24699272e] [tip]
1322 1322
1323 1323 1970-01-18 person <person>
1324 1324
1325 1325 * merge
1326 1326 [d41e714fe50d]
1327 1327
1328 1328 1970-01-17 person <person>
1329 1329
1330 1330 * new branch
1331 1331 [bbe44766e73d] <foo>
1332 1332
1333 1333
1334 1334 Keys work:
1335 1335
1336 1336 $ for key in author branch branches date desc file_adds file_dels file_mods \
1337 1337 > file_copies file_copies_switch files \
1338 1338 > manifest node parents rev tags diffstat extras \
1339 1339 > p1rev p2rev p1node p2node; do
1340 1340 > for mode in '' --verbose --debug; do
1341 1341 > hg log $mode --template "$key$mode: {$key}\n"
1342 1342 > done
1343 1343 > done
1344 1344 author: test
1345 1345 author: User Name <user@hostname>
1346 1346 author: person
1347 1347 author: person
1348 1348 author: person
1349 1349 author: person
1350 1350 author: other@place
1351 1351 author: A. N. Other <other@place>
1352 1352 author: User Name <user@hostname>
1353 1353 author--verbose: test
1354 1354 author--verbose: User Name <user@hostname>
1355 1355 author--verbose: person
1356 1356 author--verbose: person
1357 1357 author--verbose: person
1358 1358 author--verbose: person
1359 1359 author--verbose: other@place
1360 1360 author--verbose: A. N. Other <other@place>
1361 1361 author--verbose: User Name <user@hostname>
1362 1362 author--debug: test
1363 1363 author--debug: User Name <user@hostname>
1364 1364 author--debug: person
1365 1365 author--debug: person
1366 1366 author--debug: person
1367 1367 author--debug: person
1368 1368 author--debug: other@place
1369 1369 author--debug: A. N. Other <other@place>
1370 1370 author--debug: User Name <user@hostname>
1371 1371 branch: default
1372 1372 branch: default
1373 1373 branch: default
1374 1374 branch: default
1375 1375 branch: foo
1376 1376 branch: default
1377 1377 branch: default
1378 1378 branch: default
1379 1379 branch: default
1380 1380 branch--verbose: default
1381 1381 branch--verbose: default
1382 1382 branch--verbose: default
1383 1383 branch--verbose: default
1384 1384 branch--verbose: foo
1385 1385 branch--verbose: default
1386 1386 branch--verbose: default
1387 1387 branch--verbose: default
1388 1388 branch--verbose: default
1389 1389 branch--debug: default
1390 1390 branch--debug: default
1391 1391 branch--debug: default
1392 1392 branch--debug: default
1393 1393 branch--debug: foo
1394 1394 branch--debug: default
1395 1395 branch--debug: default
1396 1396 branch--debug: default
1397 1397 branch--debug: default
1398 1398 branches:
1399 1399 branches:
1400 1400 branches:
1401 1401 branches:
1402 1402 branches: foo
1403 1403 branches:
1404 1404 branches:
1405 1405 branches:
1406 1406 branches:
1407 1407 branches--verbose:
1408 1408 branches--verbose:
1409 1409 branches--verbose:
1410 1410 branches--verbose:
1411 1411 branches--verbose: foo
1412 1412 branches--verbose:
1413 1413 branches--verbose:
1414 1414 branches--verbose:
1415 1415 branches--verbose:
1416 1416 branches--debug:
1417 1417 branches--debug:
1418 1418 branches--debug:
1419 1419 branches--debug:
1420 1420 branches--debug: foo
1421 1421 branches--debug:
1422 1422 branches--debug:
1423 1423 branches--debug:
1424 1424 branches--debug:
1425 1425 date: 1577872860.00
1426 1426 date: 1000000.00
1427 1427 date: 1500001.00
1428 1428 date: 1500000.00
1429 1429 date: 1400000.00
1430 1430 date: 1300000.00
1431 1431 date: 1200000.00
1432 1432 date: 1100000.00
1433 1433 date: 1000000.00
1434 1434 date--verbose: 1577872860.00
1435 1435 date--verbose: 1000000.00
1436 1436 date--verbose: 1500001.00
1437 1437 date--verbose: 1500000.00
1438 1438 date--verbose: 1400000.00
1439 1439 date--verbose: 1300000.00
1440 1440 date--verbose: 1200000.00
1441 1441 date--verbose: 1100000.00
1442 1442 date--verbose: 1000000.00
1443 1443 date--debug: 1577872860.00
1444 1444 date--debug: 1000000.00
1445 1445 date--debug: 1500001.00
1446 1446 date--debug: 1500000.00
1447 1447 date--debug: 1400000.00
1448 1448 date--debug: 1300000.00
1449 1449 date--debug: 1200000.00
1450 1450 date--debug: 1100000.00
1451 1451 date--debug: 1000000.00
1452 1452 desc: third
1453 1453 desc: second
1454 1454 desc: merge
1455 1455 desc: new head
1456 1456 desc: new branch
1457 1457 desc: no user, no domain
1458 1458 desc: no person
1459 1459 desc: other 1
1460 1460 other 2
1461 1461
1462 1462 other 3
1463 1463 desc: line 1
1464 1464 line 2
1465 1465 desc--verbose: third
1466 1466 desc--verbose: second
1467 1467 desc--verbose: merge
1468 1468 desc--verbose: new head
1469 1469 desc--verbose: new branch
1470 1470 desc--verbose: no user, no domain
1471 1471 desc--verbose: no person
1472 1472 desc--verbose: other 1
1473 1473 other 2
1474 1474
1475 1475 other 3
1476 1476 desc--verbose: line 1
1477 1477 line 2
1478 1478 desc--debug: third
1479 1479 desc--debug: second
1480 1480 desc--debug: merge
1481 1481 desc--debug: new head
1482 1482 desc--debug: new branch
1483 1483 desc--debug: no user, no domain
1484 1484 desc--debug: no person
1485 1485 desc--debug: other 1
1486 1486 other 2
1487 1487
1488 1488 other 3
1489 1489 desc--debug: line 1
1490 1490 line 2
1491 1491 file_adds: fourth third
1492 1492 file_adds: second
1493 1493 file_adds:
1494 1494 file_adds: d
1495 1495 file_adds:
1496 1496 file_adds:
1497 1497 file_adds: c
1498 1498 file_adds: b
1499 1499 file_adds: a
1500 1500 file_adds--verbose: fourth third
1501 1501 file_adds--verbose: second
1502 1502 file_adds--verbose:
1503 1503 file_adds--verbose: d
1504 1504 file_adds--verbose:
1505 1505 file_adds--verbose:
1506 1506 file_adds--verbose: c
1507 1507 file_adds--verbose: b
1508 1508 file_adds--verbose: a
1509 1509 file_adds--debug: fourth third
1510 1510 file_adds--debug: second
1511 1511 file_adds--debug:
1512 1512 file_adds--debug: d
1513 1513 file_adds--debug:
1514 1514 file_adds--debug:
1515 1515 file_adds--debug: c
1516 1516 file_adds--debug: b
1517 1517 file_adds--debug: a
1518 1518 file_dels: second
1519 1519 file_dels:
1520 1520 file_dels:
1521 1521 file_dels:
1522 1522 file_dels:
1523 1523 file_dels:
1524 1524 file_dels:
1525 1525 file_dels:
1526 1526 file_dels:
1527 1527 file_dels--verbose: second
1528 1528 file_dels--verbose:
1529 1529 file_dels--verbose:
1530 1530 file_dels--verbose:
1531 1531 file_dels--verbose:
1532 1532 file_dels--verbose:
1533 1533 file_dels--verbose:
1534 1534 file_dels--verbose:
1535 1535 file_dels--verbose:
1536 1536 file_dels--debug: second
1537 1537 file_dels--debug:
1538 1538 file_dels--debug:
1539 1539 file_dels--debug:
1540 1540 file_dels--debug:
1541 1541 file_dels--debug:
1542 1542 file_dels--debug:
1543 1543 file_dels--debug:
1544 1544 file_dels--debug:
1545 1545 file_mods:
1546 1546 file_mods:
1547 1547 file_mods:
1548 1548 file_mods:
1549 1549 file_mods:
1550 1550 file_mods: c
1551 1551 file_mods:
1552 1552 file_mods:
1553 1553 file_mods:
1554 1554 file_mods--verbose:
1555 1555 file_mods--verbose:
1556 1556 file_mods--verbose:
1557 1557 file_mods--verbose:
1558 1558 file_mods--verbose:
1559 1559 file_mods--verbose: c
1560 1560 file_mods--verbose:
1561 1561 file_mods--verbose:
1562 1562 file_mods--verbose:
1563 1563 file_mods--debug:
1564 1564 file_mods--debug:
1565 1565 file_mods--debug:
1566 1566 file_mods--debug:
1567 1567 file_mods--debug:
1568 1568 file_mods--debug: c
1569 1569 file_mods--debug:
1570 1570 file_mods--debug:
1571 1571 file_mods--debug:
1572 1572 file_copies: fourth (second)
1573 1573 file_copies:
1574 1574 file_copies:
1575 1575 file_copies:
1576 1576 file_copies:
1577 1577 file_copies:
1578 1578 file_copies:
1579 1579 file_copies:
1580 1580 file_copies:
1581 1581 file_copies--verbose: fourth (second)
1582 1582 file_copies--verbose:
1583 1583 file_copies--verbose:
1584 1584 file_copies--verbose:
1585 1585 file_copies--verbose:
1586 1586 file_copies--verbose:
1587 1587 file_copies--verbose:
1588 1588 file_copies--verbose:
1589 1589 file_copies--verbose:
1590 1590 file_copies--debug: fourth (second)
1591 1591 file_copies--debug:
1592 1592 file_copies--debug:
1593 1593 file_copies--debug:
1594 1594 file_copies--debug:
1595 1595 file_copies--debug:
1596 1596 file_copies--debug:
1597 1597 file_copies--debug:
1598 1598 file_copies--debug:
1599 1599 file_copies_switch:
1600 1600 file_copies_switch:
1601 1601 file_copies_switch:
1602 1602 file_copies_switch:
1603 1603 file_copies_switch:
1604 1604 file_copies_switch:
1605 1605 file_copies_switch:
1606 1606 file_copies_switch:
1607 1607 file_copies_switch:
1608 1608 file_copies_switch--verbose:
1609 1609 file_copies_switch--verbose:
1610 1610 file_copies_switch--verbose:
1611 1611 file_copies_switch--verbose:
1612 1612 file_copies_switch--verbose:
1613 1613 file_copies_switch--verbose:
1614 1614 file_copies_switch--verbose:
1615 1615 file_copies_switch--verbose:
1616 1616 file_copies_switch--verbose:
1617 1617 file_copies_switch--debug:
1618 1618 file_copies_switch--debug:
1619 1619 file_copies_switch--debug:
1620 1620 file_copies_switch--debug:
1621 1621 file_copies_switch--debug:
1622 1622 file_copies_switch--debug:
1623 1623 file_copies_switch--debug:
1624 1624 file_copies_switch--debug:
1625 1625 file_copies_switch--debug:
1626 1626 files: fourth second third
1627 1627 files: second
1628 1628 files:
1629 1629 files: d
1630 1630 files:
1631 1631 files: c
1632 1632 files: c
1633 1633 files: b
1634 1634 files: a
1635 1635 files--verbose: fourth second third
1636 1636 files--verbose: second
1637 1637 files--verbose:
1638 1638 files--verbose: d
1639 1639 files--verbose:
1640 1640 files--verbose: c
1641 1641 files--verbose: c
1642 1642 files--verbose: b
1643 1643 files--verbose: a
1644 1644 files--debug: fourth second third
1645 1645 files--debug: second
1646 1646 files--debug:
1647 1647 files--debug: d
1648 1648 files--debug:
1649 1649 files--debug: c
1650 1650 files--debug: c
1651 1651 files--debug: b
1652 1652 files--debug: a
1653 1653 manifest: 6:94961b75a2da
1654 1654 manifest: 5:f2dbc354b94e
1655 1655 manifest: 4:4dc3def4f9b4
1656 1656 manifest: 4:4dc3def4f9b4
1657 1657 manifest: 3:cb5a1327723b
1658 1658 manifest: 3:cb5a1327723b
1659 1659 manifest: 2:6e0e82995c35
1660 1660 manifest: 1:4e8d705b1e53
1661 1661 manifest: 0:a0c8bcbbb45c
1662 1662 manifest--verbose: 6:94961b75a2da
1663 1663 manifest--verbose: 5:f2dbc354b94e
1664 1664 manifest--verbose: 4:4dc3def4f9b4
1665 1665 manifest--verbose: 4:4dc3def4f9b4
1666 1666 manifest--verbose: 3:cb5a1327723b
1667 1667 manifest--verbose: 3:cb5a1327723b
1668 1668 manifest--verbose: 2:6e0e82995c35
1669 1669 manifest--verbose: 1:4e8d705b1e53
1670 1670 manifest--verbose: 0:a0c8bcbbb45c
1671 1671 manifest--debug: 6:94961b75a2da554b4df6fb599e5bfc7d48de0c64
1672 1672 manifest--debug: 5:f2dbc354b94e5ec0b4f10680ee0cee816101d0bf
1673 1673 manifest--debug: 4:4dc3def4f9b4c6e8de820f6ee74737f91e96a216
1674 1674 manifest--debug: 4:4dc3def4f9b4c6e8de820f6ee74737f91e96a216
1675 1675 manifest--debug: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc
1676 1676 manifest--debug: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc
1677 1677 manifest--debug: 2:6e0e82995c35d0d57a52aca8da4e56139e06b4b1
1678 1678 manifest--debug: 1:4e8d705b1e53e3f9375e0e60dc7b525d8211fe55
1679 1679 manifest--debug: 0:a0c8bcbbb45c63b90b70ad007bf38961f64f2af0
1680 1680 node: 95c24699272ef57d062b8bccc32c878bf841784a
1681 1681 node: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1682 1682 node: d41e714fe50d9e4a5f11b4d595d543481b5f980b
1683 1683 node: 13207e5a10d9fd28ec424934298e176197f2c67f
1684 1684 node: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1685 1685 node: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1686 1686 node: 97054abb4ab824450e9164180baf491ae0078465
1687 1687 node: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1688 1688 node: 1e4e1b8f71e05681d422154f5421e385fec3454f
1689 1689 node--verbose: 95c24699272ef57d062b8bccc32c878bf841784a
1690 1690 node--verbose: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1691 1691 node--verbose: d41e714fe50d9e4a5f11b4d595d543481b5f980b
1692 1692 node--verbose: 13207e5a10d9fd28ec424934298e176197f2c67f
1693 1693 node--verbose: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1694 1694 node--verbose: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1695 1695 node--verbose: 97054abb4ab824450e9164180baf491ae0078465
1696 1696 node--verbose: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1697 1697 node--verbose: 1e4e1b8f71e05681d422154f5421e385fec3454f
1698 1698 node--debug: 95c24699272ef57d062b8bccc32c878bf841784a
1699 1699 node--debug: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1700 1700 node--debug: d41e714fe50d9e4a5f11b4d595d543481b5f980b
1701 1701 node--debug: 13207e5a10d9fd28ec424934298e176197f2c67f
1702 1702 node--debug: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1703 1703 node--debug: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1704 1704 node--debug: 97054abb4ab824450e9164180baf491ae0078465
1705 1705 node--debug: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1706 1706 node--debug: 1e4e1b8f71e05681d422154f5421e385fec3454f
1707 1707 parents:
1708 1708 parents: -1:000000000000
1709 1709 parents: 5:13207e5a10d9 4:bbe44766e73d
1710 1710 parents: 3:10e46f2dcbf4
1711 1711 parents:
1712 1712 parents:
1713 1713 parents:
1714 1714 parents:
1715 1715 parents:
1716 1716 parents--verbose:
1717 1717 parents--verbose: -1:000000000000
1718 1718 parents--verbose: 5:13207e5a10d9 4:bbe44766e73d
1719 1719 parents--verbose: 3:10e46f2dcbf4
1720 1720 parents--verbose:
1721 1721 parents--verbose:
1722 1722 parents--verbose:
1723 1723 parents--verbose:
1724 1724 parents--verbose:
1725 1725 parents--debug: 7:29114dbae42b9f078cf2714dbe3a86bba8ec7453 -1:0000000000000000000000000000000000000000
1726 1726 parents--debug: -1:0000000000000000000000000000000000000000 -1:0000000000000000000000000000000000000000
1727 1727 parents--debug: 5:13207e5a10d9fd28ec424934298e176197f2c67f 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
1728 1728 parents--debug: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47 -1:0000000000000000000000000000000000000000
1729 1729 parents--debug: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47 -1:0000000000000000000000000000000000000000
1730 1730 parents--debug: 2:97054abb4ab824450e9164180baf491ae0078465 -1:0000000000000000000000000000000000000000
1731 1731 parents--debug: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965 -1:0000000000000000000000000000000000000000
1732 1732 parents--debug: 0:1e4e1b8f71e05681d422154f5421e385fec3454f -1:0000000000000000000000000000000000000000
1733 1733 parents--debug: -1:0000000000000000000000000000000000000000 -1:0000000000000000000000000000000000000000
1734 1734 rev: 8
1735 1735 rev: 7
1736 1736 rev: 6
1737 1737 rev: 5
1738 1738 rev: 4
1739 1739 rev: 3
1740 1740 rev: 2
1741 1741 rev: 1
1742 1742 rev: 0
1743 1743 rev--verbose: 8
1744 1744 rev--verbose: 7
1745 1745 rev--verbose: 6
1746 1746 rev--verbose: 5
1747 1747 rev--verbose: 4
1748 1748 rev--verbose: 3
1749 1749 rev--verbose: 2
1750 1750 rev--verbose: 1
1751 1751 rev--verbose: 0
1752 1752 rev--debug: 8
1753 1753 rev--debug: 7
1754 1754 rev--debug: 6
1755 1755 rev--debug: 5
1756 1756 rev--debug: 4
1757 1757 rev--debug: 3
1758 1758 rev--debug: 2
1759 1759 rev--debug: 1
1760 1760 rev--debug: 0
1761 1761 tags: tip
1762 1762 tags:
1763 1763 tags:
1764 1764 tags:
1765 1765 tags:
1766 1766 tags:
1767 1767 tags:
1768 1768 tags:
1769 1769 tags:
1770 1770 tags--verbose: tip
1771 1771 tags--verbose:
1772 1772 tags--verbose:
1773 1773 tags--verbose:
1774 1774 tags--verbose:
1775 1775 tags--verbose:
1776 1776 tags--verbose:
1777 1777 tags--verbose:
1778 1778 tags--verbose:
1779 1779 tags--debug: tip
1780 1780 tags--debug:
1781 1781 tags--debug:
1782 1782 tags--debug:
1783 1783 tags--debug:
1784 1784 tags--debug:
1785 1785 tags--debug:
1786 1786 tags--debug:
1787 1787 tags--debug:
1788 1788 diffstat: 3: +2/-1
1789 1789 diffstat: 1: +1/-0
1790 1790 diffstat: 0: +0/-0
1791 1791 diffstat: 1: +1/-0
1792 1792 diffstat: 0: +0/-0
1793 1793 diffstat: 1: +1/-0
1794 1794 diffstat: 1: +4/-0
1795 1795 diffstat: 1: +2/-0
1796 1796 diffstat: 1: +1/-0
1797 1797 diffstat--verbose: 3: +2/-1
1798 1798 diffstat--verbose: 1: +1/-0
1799 1799 diffstat--verbose: 0: +0/-0
1800 1800 diffstat--verbose: 1: +1/-0
1801 1801 diffstat--verbose: 0: +0/-0
1802 1802 diffstat--verbose: 1: +1/-0
1803 1803 diffstat--verbose: 1: +4/-0
1804 1804 diffstat--verbose: 1: +2/-0
1805 1805 diffstat--verbose: 1: +1/-0
1806 1806 diffstat--debug: 3: +2/-1
1807 1807 diffstat--debug: 1: +1/-0
1808 1808 diffstat--debug: 0: +0/-0
1809 1809 diffstat--debug: 1: +1/-0
1810 1810 diffstat--debug: 0: +0/-0
1811 1811 diffstat--debug: 1: +1/-0
1812 1812 diffstat--debug: 1: +4/-0
1813 1813 diffstat--debug: 1: +2/-0
1814 1814 diffstat--debug: 1: +1/-0
1815 1815 extras: branch=default
1816 1816 extras: branch=default
1817 1817 extras: branch=default
1818 1818 extras: branch=default
1819 1819 extras: branch=foo
1820 1820 extras: branch=default
1821 1821 extras: branch=default
1822 1822 extras: branch=default
1823 1823 extras: branch=default
1824 1824 extras--verbose: branch=default
1825 1825 extras--verbose: branch=default
1826 1826 extras--verbose: branch=default
1827 1827 extras--verbose: branch=default
1828 1828 extras--verbose: branch=foo
1829 1829 extras--verbose: branch=default
1830 1830 extras--verbose: branch=default
1831 1831 extras--verbose: branch=default
1832 1832 extras--verbose: branch=default
1833 1833 extras--debug: branch=default
1834 1834 extras--debug: branch=default
1835 1835 extras--debug: branch=default
1836 1836 extras--debug: branch=default
1837 1837 extras--debug: branch=foo
1838 1838 extras--debug: branch=default
1839 1839 extras--debug: branch=default
1840 1840 extras--debug: branch=default
1841 1841 extras--debug: branch=default
1842 1842 p1rev: 7
1843 1843 p1rev: -1
1844 1844 p1rev: 5
1845 1845 p1rev: 3
1846 1846 p1rev: 3
1847 1847 p1rev: 2
1848 1848 p1rev: 1
1849 1849 p1rev: 0
1850 1850 p1rev: -1
1851 1851 p1rev--verbose: 7
1852 1852 p1rev--verbose: -1
1853 1853 p1rev--verbose: 5
1854 1854 p1rev--verbose: 3
1855 1855 p1rev--verbose: 3
1856 1856 p1rev--verbose: 2
1857 1857 p1rev--verbose: 1
1858 1858 p1rev--verbose: 0
1859 1859 p1rev--verbose: -1
1860 1860 p1rev--debug: 7
1861 1861 p1rev--debug: -1
1862 1862 p1rev--debug: 5
1863 1863 p1rev--debug: 3
1864 1864 p1rev--debug: 3
1865 1865 p1rev--debug: 2
1866 1866 p1rev--debug: 1
1867 1867 p1rev--debug: 0
1868 1868 p1rev--debug: -1
1869 1869 p2rev: -1
1870 1870 p2rev: -1
1871 1871 p2rev: 4
1872 1872 p2rev: -1
1873 1873 p2rev: -1
1874 1874 p2rev: -1
1875 1875 p2rev: -1
1876 1876 p2rev: -1
1877 1877 p2rev: -1
1878 1878 p2rev--verbose: -1
1879 1879 p2rev--verbose: -1
1880 1880 p2rev--verbose: 4
1881 1881 p2rev--verbose: -1
1882 1882 p2rev--verbose: -1
1883 1883 p2rev--verbose: -1
1884 1884 p2rev--verbose: -1
1885 1885 p2rev--verbose: -1
1886 1886 p2rev--verbose: -1
1887 1887 p2rev--debug: -1
1888 1888 p2rev--debug: -1
1889 1889 p2rev--debug: 4
1890 1890 p2rev--debug: -1
1891 1891 p2rev--debug: -1
1892 1892 p2rev--debug: -1
1893 1893 p2rev--debug: -1
1894 1894 p2rev--debug: -1
1895 1895 p2rev--debug: -1
1896 1896 p1node: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1897 1897 p1node: 0000000000000000000000000000000000000000
1898 1898 p1node: 13207e5a10d9fd28ec424934298e176197f2c67f
1899 1899 p1node: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1900 1900 p1node: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1901 1901 p1node: 97054abb4ab824450e9164180baf491ae0078465
1902 1902 p1node: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1903 1903 p1node: 1e4e1b8f71e05681d422154f5421e385fec3454f
1904 1904 p1node: 0000000000000000000000000000000000000000
1905 1905 p1node--verbose: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1906 1906 p1node--verbose: 0000000000000000000000000000000000000000
1907 1907 p1node--verbose: 13207e5a10d9fd28ec424934298e176197f2c67f
1908 1908 p1node--verbose: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1909 1909 p1node--verbose: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1910 1910 p1node--verbose: 97054abb4ab824450e9164180baf491ae0078465
1911 1911 p1node--verbose: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1912 1912 p1node--verbose: 1e4e1b8f71e05681d422154f5421e385fec3454f
1913 1913 p1node--verbose: 0000000000000000000000000000000000000000
1914 1914 p1node--debug: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1915 1915 p1node--debug: 0000000000000000000000000000000000000000
1916 1916 p1node--debug: 13207e5a10d9fd28ec424934298e176197f2c67f
1917 1917 p1node--debug: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1918 1918 p1node--debug: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1919 1919 p1node--debug: 97054abb4ab824450e9164180baf491ae0078465
1920 1920 p1node--debug: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1921 1921 p1node--debug: 1e4e1b8f71e05681d422154f5421e385fec3454f
1922 1922 p1node--debug: 0000000000000000000000000000000000000000
1923 1923 p2node: 0000000000000000000000000000000000000000
1924 1924 p2node: 0000000000000000000000000000000000000000
1925 1925 p2node: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1926 1926 p2node: 0000000000000000000000000000000000000000
1927 1927 p2node: 0000000000000000000000000000000000000000
1928 1928 p2node: 0000000000000000000000000000000000000000
1929 1929 p2node: 0000000000000000000000000000000000000000
1930 1930 p2node: 0000000000000000000000000000000000000000
1931 1931 p2node: 0000000000000000000000000000000000000000
1932 1932 p2node--verbose: 0000000000000000000000000000000000000000
1933 1933 p2node--verbose: 0000000000000000000000000000000000000000
1934 1934 p2node--verbose: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1935 1935 p2node--verbose: 0000000000000000000000000000000000000000
1936 1936 p2node--verbose: 0000000000000000000000000000000000000000
1937 1937 p2node--verbose: 0000000000000000000000000000000000000000
1938 1938 p2node--verbose: 0000000000000000000000000000000000000000
1939 1939 p2node--verbose: 0000000000000000000000000000000000000000
1940 1940 p2node--verbose: 0000000000000000000000000000000000000000
1941 1941 p2node--debug: 0000000000000000000000000000000000000000
1942 1942 p2node--debug: 0000000000000000000000000000000000000000
1943 1943 p2node--debug: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1944 1944 p2node--debug: 0000000000000000000000000000000000000000
1945 1945 p2node--debug: 0000000000000000000000000000000000000000
1946 1946 p2node--debug: 0000000000000000000000000000000000000000
1947 1947 p2node--debug: 0000000000000000000000000000000000000000
1948 1948 p2node--debug: 0000000000000000000000000000000000000000
1949 1949 p2node--debug: 0000000000000000000000000000000000000000
1950 1950
1951 1951 Filters work:
1952 1952
1953 1953 $ hg log --template '{author|domain}\n'
1954 1954
1955 1955 hostname
1956 1956
1957 1957
1958 1958
1959 1959
1960 1960 place
1961 1961 place
1962 1962 hostname
1963 1963
1964 1964 $ hg log --template '{author|person}\n'
1965 1965 test
1966 1966 User Name
1967 1967 person
1968 1968 person
1969 1969 person
1970 1970 person
1971 1971 other
1972 1972 A. N. Other
1973 1973 User Name
1974 1974
1975 1975 $ hg log --template '{author|user}\n'
1976 1976 test
1977 1977 user
1978 1978 person
1979 1979 person
1980 1980 person
1981 1981 person
1982 1982 other
1983 1983 other
1984 1984 user
1985 1985
1986 1986 $ hg log --template '{date|date}\n'
1987 1987 Wed Jan 01 10:01:00 2020 +0000
1988 1988 Mon Jan 12 13:46:40 1970 +0000
1989 1989 Sun Jan 18 08:40:01 1970 +0000
1990 1990 Sun Jan 18 08:40:00 1970 +0000
1991 1991 Sat Jan 17 04:53:20 1970 +0000
1992 1992 Fri Jan 16 01:06:40 1970 +0000
1993 1993 Wed Jan 14 21:20:00 1970 +0000
1994 1994 Tue Jan 13 17:33:20 1970 +0000
1995 1995 Mon Jan 12 13:46:40 1970 +0000
1996 1996
1997 1997 $ hg log --template '{date|isodate}\n'
1998 1998 2020-01-01 10:01 +0000
1999 1999 1970-01-12 13:46 +0000
2000 2000 1970-01-18 08:40 +0000
2001 2001 1970-01-18 08:40 +0000
2002 2002 1970-01-17 04:53 +0000
2003 2003 1970-01-16 01:06 +0000
2004 2004 1970-01-14 21:20 +0000
2005 2005 1970-01-13 17:33 +0000
2006 2006 1970-01-12 13:46 +0000
2007 2007
2008 2008 $ hg log --template '{date|isodatesec}\n'
2009 2009 2020-01-01 10:01:00 +0000
2010 2010 1970-01-12 13:46:40 +0000
2011 2011 1970-01-18 08:40:01 +0000
2012 2012 1970-01-18 08:40:00 +0000
2013 2013 1970-01-17 04:53:20 +0000
2014 2014 1970-01-16 01:06:40 +0000
2015 2015 1970-01-14 21:20:00 +0000
2016 2016 1970-01-13 17:33:20 +0000
2017 2017 1970-01-12 13:46:40 +0000
2018 2018
2019 2019 $ hg log --template '{date|rfc822date}\n'
2020 2020 Wed, 01 Jan 2020 10:01:00 +0000
2021 2021 Mon, 12 Jan 1970 13:46:40 +0000
2022 2022 Sun, 18 Jan 1970 08:40:01 +0000
2023 2023 Sun, 18 Jan 1970 08:40:00 +0000
2024 2024 Sat, 17 Jan 1970 04:53:20 +0000
2025 2025 Fri, 16 Jan 1970 01:06:40 +0000
2026 2026 Wed, 14 Jan 1970 21:20:00 +0000
2027 2027 Tue, 13 Jan 1970 17:33:20 +0000
2028 2028 Mon, 12 Jan 1970 13:46:40 +0000
2029 2029
2030 2030 $ hg log --template '{desc|firstline}\n'
2031 2031 third
2032 2032 second
2033 2033 merge
2034 2034 new head
2035 2035 new branch
2036 2036 no user, no domain
2037 2037 no person
2038 2038 other 1
2039 2039 line 1
2040 2040
2041 2041 $ hg log --template '{node|short}\n'
2042 2042 95c24699272e
2043 2043 29114dbae42b
2044 2044 d41e714fe50d
2045 2045 13207e5a10d9
2046 2046 bbe44766e73d
2047 2047 10e46f2dcbf4
2048 2048 97054abb4ab8
2049 2049 b608e9d1a3f0
2050 2050 1e4e1b8f71e0
2051 2051
2052 2052 $ hg log --template '<changeset author="{author|xmlescape}"/>\n'
2053 2053 <changeset author="test"/>
2054 2054 <changeset author="User Name &lt;user@hostname&gt;"/>
2055 2055 <changeset author="person"/>
2056 2056 <changeset author="person"/>
2057 2057 <changeset author="person"/>
2058 2058 <changeset author="person"/>
2059 2059 <changeset author="other@place"/>
2060 2060 <changeset author="A. N. Other &lt;other@place&gt;"/>
2061 2061 <changeset author="User Name &lt;user@hostname&gt;"/>
2062 2062
2063 2063 $ hg log --template '{rev}: {children}\n'
2064 2064 8:
2065 2065 7: 8:95c24699272e
2066 2066 6:
2067 2067 5: 6:d41e714fe50d
2068 2068 4: 6:d41e714fe50d
2069 2069 3: 4:bbe44766e73d 5:13207e5a10d9
2070 2070 2: 3:10e46f2dcbf4
2071 2071 1: 2:97054abb4ab8
2072 2072 0: 1:b608e9d1a3f0
2073 2073
2074 2074 Formatnode filter works:
2075 2075
2076 2076 $ hg -q log -r 0 --template '{node|formatnode}\n'
2077 2077 1e4e1b8f71e0
2078 2078
2079 2079 $ hg log -r 0 --template '{node|formatnode}\n'
2080 2080 1e4e1b8f71e0
2081 2081
2082 2082 $ hg -v log -r 0 --template '{node|formatnode}\n'
2083 2083 1e4e1b8f71e0
2084 2084
2085 2085 $ hg --debug log -r 0 --template '{node|formatnode}\n'
2086 2086 1e4e1b8f71e05681d422154f5421e385fec3454f
2087 2087
2088 2088 Age filter:
2089 2089
2090 2090 $ hg init unstable-hash
2091 2091 $ cd unstable-hash
2092 2092 $ hg log --template '{date|age}\n' > /dev/null || exit 1
2093 2093
2094 2094 >>> from datetime import datetime, timedelta
2095 2095 >>> fp = open('a', 'w')
2096 2096 >>> n = datetime.now() + timedelta(366 * 7)
2097 2097 >>> fp.write('%d-%d-%d 00:00' % (n.year, n.month, n.day))
2098 2098 >>> fp.close()
2099 2099 $ hg add a
2100 2100 $ hg commit -m future -d "`cat a`"
2101 2101
2102 2102 $ hg log -l1 --template '{date|age}\n'
2103 2103 7 years from now
2104 2104
2105 2105 $ cd ..
2106 2106 $ rm -rf unstable-hash
2107 2107
2108 2108 Add a dummy commit to make up for the instability of the above:
2109 2109
2110 2110 $ echo a > a
2111 2111 $ hg add a
2112 2112 $ hg ci -m future
2113 2113
2114 2114 Count filter:
2115 2115
2116 2116 $ hg log -l1 --template '{node|count} {node|short|count}\n'
2117 2117 40 12
2118 2118
2119 2119 $ hg log -l1 --template '{revset("null^")|count} {revset(".")|count} {revset("0::3")|count}\n'
2120 2120 0 1 4
2121 2121
2122 2122 $ hg log -G --template '{rev}: children: {children|count}, \
2123 2123 > tags: {tags|count}, file_adds: {file_adds|count}, \
2124 2124 > ancestors: {revset("ancestors(%s)", rev)|count}'
2125 2125 @ 9: children: 0, tags: 1, file_adds: 1, ancestors: 3
2126 2126 |
2127 2127 o 8: children: 1, tags: 0, file_adds: 2, ancestors: 2
2128 2128 |
2129 2129 o 7: children: 1, tags: 0, file_adds: 1, ancestors: 1
2130 2130
2131 2131 o 6: children: 0, tags: 0, file_adds: 0, ancestors: 7
2132 2132 |\
2133 2133 | o 5: children: 1, tags: 0, file_adds: 1, ancestors: 5
2134 2134 | |
2135 2135 o | 4: children: 1, tags: 0, file_adds: 0, ancestors: 5
2136 2136 |/
2137 2137 o 3: children: 2, tags: 0, file_adds: 0, ancestors: 4
2138 2138 |
2139 2139 o 2: children: 1, tags: 0, file_adds: 1, ancestors: 3
2140 2140 |
2141 2141 o 1: children: 1, tags: 0, file_adds: 1, ancestors: 2
2142 2142 |
2143 2143 o 0: children: 1, tags: 0, file_adds: 1, ancestors: 1
2144 2144
2145 2145
2146 2146 Upper/lower filters:
2147 2147
2148 2148 $ hg log -r0 --template '{branch|upper}\n'
2149 2149 DEFAULT
2150 2150 $ hg log -r0 --template '{author|lower}\n'
2151 2151 user name <user@hostname>
2152 2152 $ hg log -r0 --template '{date|upper}\n'
2153 2153 abort: template filter 'upper' is not compatible with keyword 'date'
2154 2154 [255]
2155 2155
2156 2156 Add a commit that does all possible modifications at once
2157 2157
2158 2158 $ echo modify >> third
2159 2159 $ touch b
2160 2160 $ hg add b
2161 2161 $ hg mv fourth fifth
2162 2162 $ hg rm a
2163 2163 $ hg ci -m "Modify, add, remove, rename"
2164 2164
2165 2165 Check the status template
2166 2166
2167 2167 $ cat <<EOF >> $HGRCPATH
2168 2168 > [extensions]
2169 2169 > color=
2170 2170 > EOF
2171 2171
2172 2172 $ hg log -T status -r 10
2173 2173 changeset: 10:0f9759ec227a
2174 2174 tag: tip
2175 2175 user: test
2176 2176 date: Thu Jan 01 00:00:00 1970 +0000
2177 2177 summary: Modify, add, remove, rename
2178 2178 files:
2179 2179 M third
2180 2180 A b
2181 2181 A fifth
2182 2182 R a
2183 2183 R fourth
2184 2184
2185 2185 $ hg log -T status -C -r 10
2186 2186 changeset: 10:0f9759ec227a
2187 2187 tag: tip
2188 2188 user: test
2189 2189 date: Thu Jan 01 00:00:00 1970 +0000
2190 2190 summary: Modify, add, remove, rename
2191 2191 files:
2192 2192 M third
2193 2193 A b
2194 2194 A fifth
2195 2195 fourth
2196 2196 R a
2197 2197 R fourth
2198 2198
2199 2199 $ hg log -T status -C -r 10 -v
2200 2200 changeset: 10:0f9759ec227a
2201 2201 tag: tip
2202 2202 user: test
2203 2203 date: Thu Jan 01 00:00:00 1970 +0000
2204 2204 description:
2205 2205 Modify, add, remove, rename
2206 2206
2207 2207 files:
2208 2208 M third
2209 2209 A b
2210 2210 A fifth
2211 2211 fourth
2212 2212 R a
2213 2213 R fourth
2214 2214
2215 2215 $ hg log -T status -C -r 10 --debug
2216 2216 changeset: 10:0f9759ec227a4859c2014a345cd8a859022b7c6c
2217 2217 tag: tip
2218 2218 phase: secret
2219 2219 parent: 9:bf9dfba36635106d6a73ccc01e28b762da60e066
2220 2220 parent: -1:0000000000000000000000000000000000000000
2221 2221 manifest: 8:89dd546f2de0a9d6d664f58d86097eb97baba567
2222 2222 user: test
2223 2223 date: Thu Jan 01 00:00:00 1970 +0000
2224 2224 extra: branch=default
2225 2225 description:
2226 2226 Modify, add, remove, rename
2227 2227
2228 2228 files:
2229 2229 M third
2230 2230 A b
2231 2231 A fifth
2232 2232 fourth
2233 2233 R a
2234 2234 R fourth
2235 2235
2236 2236 $ hg log -T status -C -r 10 --quiet
2237 2237 10:0f9759ec227a
2238 2238 $ hg --color=debug log -T status -r 10
2239 2239 [log.changeset changeset.secret|changeset: 10:0f9759ec227a]
2240 2240 [log.tag|tag: tip]
2241 2241 [log.user|user: test]
2242 2242 [log.date|date: Thu Jan 01 00:00:00 1970 +0000]
2243 2243 [log.summary|summary: Modify, add, remove, rename]
2244 2244 [ui.note log.files|files:]
2245 2245 [status.modified|M third]
2246 2246 [status.added|A b]
2247 2247 [status.added|A fifth]
2248 2248 [status.removed|R a]
2249 2249 [status.removed|R fourth]
2250 2250
2251 2251 $ hg --color=debug log -T status -C -r 10
2252 2252 [log.changeset changeset.secret|changeset: 10:0f9759ec227a]
2253 2253 [log.tag|tag: tip]
2254 2254 [log.user|user: test]
2255 2255 [log.date|date: Thu Jan 01 00:00:00 1970 +0000]
2256 2256 [log.summary|summary: Modify, add, remove, rename]
2257 2257 [ui.note log.files|files:]
2258 2258 [status.modified|M third]
2259 2259 [status.added|A b]
2260 2260 [status.added|A fifth]
2261 2261 [status.copied| fourth]
2262 2262 [status.removed|R a]
2263 2263 [status.removed|R fourth]
2264 2264
2265 2265 $ hg --color=debug log -T status -C -r 10 -v
2266 2266 [log.changeset changeset.secret|changeset: 10:0f9759ec227a]
2267 2267 [log.tag|tag: tip]
2268 2268 [log.user|user: test]
2269 2269 [log.date|date: Thu Jan 01 00:00:00 1970 +0000]
2270 2270 [ui.note log.description|description:]
2271 2271 [ui.note log.description|Modify, add, remove, rename]
2272 2272
2273 2273 [ui.note log.files|files:]
2274 2274 [status.modified|M third]
2275 2275 [status.added|A b]
2276 2276 [status.added|A fifth]
2277 2277 [status.copied| fourth]
2278 2278 [status.removed|R a]
2279 2279 [status.removed|R fourth]
2280 2280
2281 2281 $ hg --color=debug log -T status -C -r 10 --debug
2282 2282 [log.changeset changeset.secret|changeset: 10:0f9759ec227a4859c2014a345cd8a859022b7c6c]
2283 2283 [log.tag|tag: tip]
2284 2284 [log.phase|phase: secret]
2285 2285 [log.parent changeset.secret|parent: 9:bf9dfba36635106d6a73ccc01e28b762da60e066]
2286 2286 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2287 2287 [ui.debug log.manifest|manifest: 8:89dd546f2de0a9d6d664f58d86097eb97baba567]
2288 2288 [log.user|user: test]
2289 2289 [log.date|date: Thu Jan 01 00:00:00 1970 +0000]
2290 2290 [ui.debug log.extra|extra: branch=default]
2291 2291 [ui.note log.description|description:]
2292 2292 [ui.note log.description|Modify, add, remove, rename]
2293 2293
2294 2294 [ui.note log.files|files:]
2295 2295 [status.modified|M third]
2296 2296 [status.added|A b]
2297 2297 [status.added|A fifth]
2298 2298 [status.copied| fourth]
2299 2299 [status.removed|R a]
2300 2300 [status.removed|R fourth]
2301 2301
2302 2302 $ hg --color=debug log -T status -C -r 10 --quiet
2303 2303 [log.node|10:0f9759ec227a]
2304 2304
2305 2305 Check the bisect template
2306 2306
2307 2307 $ hg bisect -g 1
2308 2308 $ hg bisect -b 3 --noupdate
2309 2309 Testing changeset 2:97054abb4ab8 (2 changesets remaining, ~1 tests)
2310 2310 $ hg log -T bisect -r 0:4
2311 2311 changeset: 0:1e4e1b8f71e0
2312 2312 bisect: good (implicit)
2313 2313 user: User Name <user@hostname>
2314 2314 date: Mon Jan 12 13:46:40 1970 +0000
2315 2315 summary: line 1
2316 2316
2317 2317 changeset: 1:b608e9d1a3f0
2318 2318 bisect: good
2319 2319 user: A. N. Other <other@place>
2320 2320 date: Tue Jan 13 17:33:20 1970 +0000
2321 2321 summary: other 1
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 summary: no person
2328 2328
2329 2329 changeset: 3:10e46f2dcbf4
2330 2330 bisect: bad
2331 2331 user: person
2332 2332 date: Fri Jan 16 01:06:40 1970 +0000
2333 2333 summary: no user, no domain
2334 2334
2335 2335 changeset: 4:bbe44766e73d
2336 2336 bisect: bad (implicit)
2337 2337 branch: foo
2338 2338 user: person
2339 2339 date: Sat Jan 17 04:53:20 1970 +0000
2340 2340 summary: new branch
2341 2341
2342 2342 $ hg log --debug -T bisect -r 0:4
2343 2343 changeset: 0:1e4e1b8f71e05681d422154f5421e385fec3454f
2344 2344 bisect: good (implicit)
2345 2345 phase: public
2346 2346 parent: -1:0000000000000000000000000000000000000000
2347 2347 parent: -1:0000000000000000000000000000000000000000
2348 2348 manifest: 0:a0c8bcbbb45c63b90b70ad007bf38961f64f2af0
2349 2349 user: User Name <user@hostname>
2350 2350 date: Mon Jan 12 13:46:40 1970 +0000
2351 2351 files+: a
2352 2352 extra: branch=default
2353 2353 description:
2354 2354 line 1
2355 2355 line 2
2356 2356
2357 2357
2358 2358 changeset: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965
2359 2359 bisect: good
2360 2360 phase: public
2361 2361 parent: 0:1e4e1b8f71e05681d422154f5421e385fec3454f
2362 2362 parent: -1:0000000000000000000000000000000000000000
2363 2363 manifest: 1:4e8d705b1e53e3f9375e0e60dc7b525d8211fe55
2364 2364 user: A. N. Other <other@place>
2365 2365 date: Tue Jan 13 17:33:20 1970 +0000
2366 2366 files+: b
2367 2367 extra: branch=default
2368 2368 description:
2369 2369 other 1
2370 2370 other 2
2371 2371
2372 2372 other 3
2373 2373
2374 2374
2375 2375 changeset: 2:97054abb4ab824450e9164180baf491ae0078465
2376 2376 bisect: untested
2377 2377 phase: public
2378 2378 parent: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965
2379 2379 parent: -1:0000000000000000000000000000000000000000
2380 2380 manifest: 2:6e0e82995c35d0d57a52aca8da4e56139e06b4b1
2381 2381 user: other@place
2382 2382 date: Wed Jan 14 21:20:00 1970 +0000
2383 2383 files+: c
2384 2384 extra: branch=default
2385 2385 description:
2386 2386 no person
2387 2387
2388 2388
2389 2389 changeset: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47
2390 2390 bisect: bad
2391 2391 phase: public
2392 2392 parent: 2:97054abb4ab824450e9164180baf491ae0078465
2393 2393 parent: -1:0000000000000000000000000000000000000000
2394 2394 manifest: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc
2395 2395 user: person
2396 2396 date: Fri Jan 16 01:06:40 1970 +0000
2397 2397 files: c
2398 2398 extra: branch=default
2399 2399 description:
2400 2400 no user, no domain
2401 2401
2402 2402
2403 2403 changeset: 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
2404 2404 bisect: bad (implicit)
2405 2405 branch: foo
2406 2406 phase: draft
2407 2407 parent: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47
2408 2408 parent: -1:0000000000000000000000000000000000000000
2409 2409 manifest: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc
2410 2410 user: person
2411 2411 date: Sat Jan 17 04:53:20 1970 +0000
2412 2412 extra: branch=foo
2413 2413 description:
2414 2414 new branch
2415 2415
2416 2416
2417 2417 $ hg log -v -T bisect -r 0:4
2418 2418 changeset: 0:1e4e1b8f71e0
2419 2419 bisect: good (implicit)
2420 2420 user: User Name <user@hostname>
2421 2421 date: Mon Jan 12 13:46:40 1970 +0000
2422 2422 files: a
2423 2423 description:
2424 2424 line 1
2425 2425 line 2
2426 2426
2427 2427
2428 2428 changeset: 1:b608e9d1a3f0
2429 2429 bisect: good
2430 2430 user: A. N. Other <other@place>
2431 2431 date: Tue Jan 13 17:33:20 1970 +0000
2432 2432 files: b
2433 2433 description:
2434 2434 other 1
2435 2435 other 2
2436 2436
2437 2437 other 3
2438 2438
2439 2439
2440 2440 changeset: 2:97054abb4ab8
2441 2441 bisect: untested
2442 2442 user: other@place
2443 2443 date: Wed Jan 14 21:20:00 1970 +0000
2444 2444 files: c
2445 2445 description:
2446 2446 no person
2447 2447
2448 2448
2449 2449 changeset: 3:10e46f2dcbf4
2450 2450 bisect: bad
2451 2451 user: person
2452 2452 date: Fri Jan 16 01:06:40 1970 +0000
2453 2453 files: c
2454 2454 description:
2455 2455 no user, no domain
2456 2456
2457 2457
2458 2458 changeset: 4:bbe44766e73d
2459 2459 bisect: bad (implicit)
2460 2460 branch: foo
2461 2461 user: person
2462 2462 date: Sat Jan 17 04:53:20 1970 +0000
2463 2463 description:
2464 2464 new branch
2465 2465
2466 2466
2467 2467 $ hg --color=debug log -T bisect -r 0:4
2468 2468 [log.changeset changeset.public|changeset: 0:1e4e1b8f71e0]
2469 2469 [log.bisect bisect.good|bisect: good (implicit)]
2470 2470 [log.user|user: User Name <user@hostname>]
2471 2471 [log.date|date: Mon Jan 12 13:46:40 1970 +0000]
2472 2472 [log.summary|summary: line 1]
2473 2473
2474 2474 [log.changeset changeset.public|changeset: 1:b608e9d1a3f0]
2475 2475 [log.bisect bisect.good|bisect: good]
2476 2476 [log.user|user: A. N. Other <other@place>]
2477 2477 [log.date|date: Tue Jan 13 17:33:20 1970 +0000]
2478 2478 [log.summary|summary: other 1]
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 [log.summary|summary: no person]
2485 2485
2486 2486 [log.changeset changeset.public|changeset: 3:10e46f2dcbf4]
2487 2487 [log.bisect bisect.bad|bisect: bad]
2488 2488 [log.user|user: person]
2489 2489 [log.date|date: Fri Jan 16 01:06:40 1970 +0000]
2490 2490 [log.summary|summary: no user, no domain]
2491 2491
2492 2492 [log.changeset changeset.draft|changeset: 4:bbe44766e73d]
2493 2493 [log.bisect bisect.bad|bisect: bad (implicit)]
2494 2494 [log.branch|branch: foo]
2495 2495 [log.user|user: person]
2496 2496 [log.date|date: Sat Jan 17 04:53:20 1970 +0000]
2497 2497 [log.summary|summary: new branch]
2498 2498
2499 2499 $ hg --color=debug log --debug -T bisect -r 0:4
2500 2500 [log.changeset changeset.public|changeset: 0:1e4e1b8f71e05681d422154f5421e385fec3454f]
2501 2501 [log.bisect bisect.good|bisect: good (implicit)]
2502 2502 [log.phase|phase: public]
2503 2503 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2504 2504 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2505 2505 [ui.debug log.manifest|manifest: 0:a0c8bcbbb45c63b90b70ad007bf38961f64f2af0]
2506 2506 [log.user|user: User Name <user@hostname>]
2507 2507 [log.date|date: Mon Jan 12 13:46:40 1970 +0000]
2508 2508 [ui.debug log.files|files+: a]
2509 2509 [ui.debug log.extra|extra: branch=default]
2510 2510 [ui.note log.description|description:]
2511 2511 [ui.note log.description|line 1
2512 2512 line 2]
2513 2513
2514 2514
2515 2515 [log.changeset changeset.public|changeset: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965]
2516 2516 [log.bisect bisect.good|bisect: good]
2517 2517 [log.phase|phase: public]
2518 2518 [log.parent changeset.public|parent: 0:1e4e1b8f71e05681d422154f5421e385fec3454f]
2519 2519 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2520 2520 [ui.debug log.manifest|manifest: 1:4e8d705b1e53e3f9375e0e60dc7b525d8211fe55]
2521 2521 [log.user|user: A. N. Other <other@place>]
2522 2522 [log.date|date: Tue Jan 13 17:33:20 1970 +0000]
2523 2523 [ui.debug log.files|files+: b]
2524 2524 [ui.debug log.extra|extra: branch=default]
2525 2525 [ui.note log.description|description:]
2526 2526 [ui.note log.description|other 1
2527 2527 other 2
2528 2528
2529 2529 other 3]
2530 2530
2531 2531
2532 2532 [log.changeset changeset.public|changeset: 2:97054abb4ab824450e9164180baf491ae0078465]
2533 2533 [log.bisect bisect.untested|bisect: untested]
2534 2534 [log.phase|phase: public]
2535 2535 [log.parent changeset.public|parent: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965]
2536 2536 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2537 2537 [ui.debug log.manifest|manifest: 2:6e0e82995c35d0d57a52aca8da4e56139e06b4b1]
2538 2538 [log.user|user: other@place]
2539 2539 [log.date|date: Wed Jan 14 21:20:00 1970 +0000]
2540 2540 [ui.debug log.files|files+: c]
2541 2541 [ui.debug log.extra|extra: branch=default]
2542 2542 [ui.note log.description|description:]
2543 2543 [ui.note log.description|no person]
2544 2544
2545 2545
2546 2546 [log.changeset changeset.public|changeset: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47]
2547 2547 [log.bisect bisect.bad|bisect: bad]
2548 2548 [log.phase|phase: public]
2549 2549 [log.parent changeset.public|parent: 2:97054abb4ab824450e9164180baf491ae0078465]
2550 2550 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2551 2551 [ui.debug log.manifest|manifest: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc]
2552 2552 [log.user|user: person]
2553 2553 [log.date|date: Fri Jan 16 01:06:40 1970 +0000]
2554 2554 [ui.debug log.files|files: c]
2555 2555 [ui.debug log.extra|extra: branch=default]
2556 2556 [ui.note log.description|description:]
2557 2557 [ui.note log.description|no user, no domain]
2558 2558
2559 2559
2560 2560 [log.changeset changeset.draft|changeset: 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74]
2561 2561 [log.bisect bisect.bad|bisect: bad (implicit)]
2562 2562 [log.branch|branch: foo]
2563 2563 [log.phase|phase: draft]
2564 2564 [log.parent changeset.public|parent: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47]
2565 2565 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2566 2566 [ui.debug log.manifest|manifest: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc]
2567 2567 [log.user|user: person]
2568 2568 [log.date|date: Sat Jan 17 04:53:20 1970 +0000]
2569 2569 [ui.debug log.extra|extra: branch=foo]
2570 2570 [ui.note log.description|description:]
2571 2571 [ui.note log.description|new branch]
2572 2572
2573 2573
2574 2574 $ hg --color=debug log -v -T bisect -r 0:4
2575 2575 [log.changeset changeset.public|changeset: 0:1e4e1b8f71e0]
2576 2576 [log.bisect bisect.good|bisect: good (implicit)]
2577 2577 [log.user|user: User Name <user@hostname>]
2578 2578 [log.date|date: Mon Jan 12 13:46:40 1970 +0000]
2579 2579 [ui.note log.files|files: a]
2580 2580 [ui.note log.description|description:]
2581 2581 [ui.note log.description|line 1
2582 2582 line 2]
2583 2583
2584 2584
2585 2585 [log.changeset changeset.public|changeset: 1:b608e9d1a3f0]
2586 2586 [log.bisect bisect.good|bisect: good]
2587 2587 [log.user|user: A. N. Other <other@place>]
2588 2588 [log.date|date: Tue Jan 13 17:33:20 1970 +0000]
2589 2589 [ui.note log.files|files: b]
2590 2590 [ui.note log.description|description:]
2591 2591 [ui.note log.description|other 1
2592 2592 other 2
2593 2593
2594 2594 other 3]
2595 2595
2596 2596
2597 2597 [log.changeset changeset.public|changeset: 2:97054abb4ab8]
2598 2598 [log.bisect bisect.untested|bisect: untested]
2599 2599 [log.user|user: other@place]
2600 2600 [log.date|date: Wed Jan 14 21:20:00 1970 +0000]
2601 2601 [ui.note log.files|files: c]
2602 2602 [ui.note log.description|description:]
2603 2603 [ui.note log.description|no person]
2604 2604
2605 2605
2606 2606 [log.changeset changeset.public|changeset: 3:10e46f2dcbf4]
2607 2607 [log.bisect bisect.bad|bisect: bad]
2608 2608 [log.user|user: person]
2609 2609 [log.date|date: Fri Jan 16 01:06:40 1970 +0000]
2610 2610 [ui.note log.files|files: c]
2611 2611 [ui.note log.description|description:]
2612 2612 [ui.note log.description|no user, no domain]
2613 2613
2614 2614
2615 2615 [log.changeset changeset.draft|changeset: 4:bbe44766e73d]
2616 2616 [log.bisect bisect.bad|bisect: bad (implicit)]
2617 2617 [log.branch|branch: foo]
2618 2618 [log.user|user: person]
2619 2619 [log.date|date: Sat Jan 17 04:53:20 1970 +0000]
2620 2620 [ui.note log.description|description:]
2621 2621 [ui.note log.description|new branch]
2622 2622
2623 2623
2624 2624 $ hg bisect --reset
2625 2625
2626 2626 Error on syntax:
2627 2627
2628 2628 $ echo 'x = "f' >> t
2629 2629 $ hg log
2630 2630 hg: parse error at t:3: unmatched quotes
2631 2631 [255]
2632 2632
2633 2633 $ hg log -T '{date'
2634 2634 hg: parse error at 1: unterminated template expansion
2635 2635 [255]
2636 2636
2637 2637 Behind the scenes, this will throw TypeError
2638 2638
2639 2639 $ hg log -l 3 --template '{date|obfuscate}\n'
2640 2640 abort: template filter 'obfuscate' is not compatible with keyword 'date'
2641 2641 [255]
2642 2642
2643 2643 Behind the scenes, this will throw a ValueError
2644 2644
2645 2645 $ hg log -l 3 --template 'line: {desc|shortdate}\n'
2646 2646 abort: template filter 'shortdate' is not compatible with keyword 'desc'
2647 2647 [255]
2648 2648
2649 2649 Behind the scenes, this will throw AttributeError
2650 2650
2651 2651 $ hg log -l 3 --template 'line: {date|escape}\n'
2652 2652 abort: template filter 'escape' is not compatible with keyword 'date'
2653 2653 [255]
2654 2654
2655 2655 $ hg log -l 3 --template 'line: {extras|localdate}\n'
2656 2656 hg: parse error: localdate expects a date information
2657 2657 [255]
2658 2658
2659 2659 Behind the scenes, this will throw ValueError
2660 2660
2661 2661 $ hg tip --template '{author|email|date}\n'
2662 2662 hg: parse error: date expects a date information
2663 2663 [255]
2664 2664
2665 2665 Error in nested template:
2666 2666
2667 2667 $ hg log -T '{"date'
2668 2668 hg: parse error at 2: unterminated string
2669 2669 [255]
2670 2670
2671 2671 $ hg log -T '{"foo{date|=}"}'
2672 2672 hg: parse error at 11: syntax error
2673 2673 [255]
2674 2674
2675 2675 Thrown an error if a template function doesn't exist
2676 2676
2677 2677 $ hg tip --template '{foo()}\n'
2678 2678 hg: parse error: unknown function 'foo'
2679 2679 [255]
2680 2680
2681 2681 Pass generator object created by template function to filter
2682 2682
2683 2683 $ hg log -l 1 --template '{if(author, author)|user}\n'
2684 2684 test
2685 2685
2686 Test index keyword:
2687
2688 $ hg log -l 2 -T '{index + 10}{files % " {index}:{file}"}\n'
2689 10 0:a 1:b 2:fifth 3:fourth 4:third
2690 11 0:a
2691
2692 $ hg branches -T '{index} {branch}\n'
2693 0 default
2694 1 foo
2695
2686 2696 Test diff function:
2687 2697
2688 2698 $ hg diff -c 8
2689 2699 diff -r 29114dbae42b -r 95c24699272e fourth
2690 2700 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2691 2701 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
2692 2702 @@ -0,0 +1,1 @@
2693 2703 +second
2694 2704 diff -r 29114dbae42b -r 95c24699272e second
2695 2705 --- a/second Mon Jan 12 13:46:40 1970 +0000
2696 2706 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
2697 2707 @@ -1,1 +0,0 @@
2698 2708 -second
2699 2709 diff -r 29114dbae42b -r 95c24699272e third
2700 2710 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2701 2711 +++ b/third Wed Jan 01 10:01:00 2020 +0000
2702 2712 @@ -0,0 +1,1 @@
2703 2713 +third
2704 2714
2705 2715 $ hg log -r 8 -T "{diff()}"
2706 2716 diff -r 29114dbae42b -r 95c24699272e fourth
2707 2717 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2708 2718 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
2709 2719 @@ -0,0 +1,1 @@
2710 2720 +second
2711 2721 diff -r 29114dbae42b -r 95c24699272e second
2712 2722 --- a/second Mon Jan 12 13:46:40 1970 +0000
2713 2723 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
2714 2724 @@ -1,1 +0,0 @@
2715 2725 -second
2716 2726 diff -r 29114dbae42b -r 95c24699272e third
2717 2727 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2718 2728 +++ b/third Wed Jan 01 10:01:00 2020 +0000
2719 2729 @@ -0,0 +1,1 @@
2720 2730 +third
2721 2731
2722 2732 $ hg log -r 8 -T "{diff('glob:f*')}"
2723 2733 diff -r 29114dbae42b -r 95c24699272e fourth
2724 2734 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2725 2735 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
2726 2736 @@ -0,0 +1,1 @@
2727 2737 +second
2728 2738
2729 2739 $ hg log -r 8 -T "{diff('', 'glob:f*')}"
2730 2740 diff -r 29114dbae42b -r 95c24699272e second
2731 2741 --- a/second Mon Jan 12 13:46:40 1970 +0000
2732 2742 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
2733 2743 @@ -1,1 +0,0 @@
2734 2744 -second
2735 2745 diff -r 29114dbae42b -r 95c24699272e third
2736 2746 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2737 2747 +++ b/third Wed Jan 01 10:01:00 2020 +0000
2738 2748 @@ -0,0 +1,1 @@
2739 2749 +third
2740 2750
2741 2751 $ hg log -r 8 -T "{diff('FOURTH'|lower)}"
2742 2752 diff -r 29114dbae42b -r 95c24699272e fourth
2743 2753 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2744 2754 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
2745 2755 @@ -0,0 +1,1 @@
2746 2756 +second
2747 2757
2748 2758 $ cd ..
2749 2759
2750 2760
2751 2761 latesttag:
2752 2762
2753 2763 $ hg init latesttag
2754 2764 $ cd latesttag
2755 2765
2756 2766 $ echo a > file
2757 2767 $ hg ci -Am a -d '0 0'
2758 2768 adding file
2759 2769
2760 2770 $ echo b >> file
2761 2771 $ hg ci -m b -d '1 0'
2762 2772
2763 2773 $ echo c >> head1
2764 2774 $ hg ci -Am h1c -d '2 0'
2765 2775 adding head1
2766 2776
2767 2777 $ hg update -q 1
2768 2778 $ echo d >> head2
2769 2779 $ hg ci -Am h2d -d '3 0'
2770 2780 adding head2
2771 2781 created new head
2772 2782
2773 2783 $ echo e >> head2
2774 2784 $ hg ci -m h2e -d '4 0'
2775 2785
2776 2786 $ hg merge -q
2777 2787 $ hg ci -m merge -d '5 -3600'
2778 2788
2779 2789 No tag set:
2780 2790
2781 2791 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
2782 2792 5: null+5
2783 2793 4: null+4
2784 2794 3: null+3
2785 2795 2: null+3
2786 2796 1: null+2
2787 2797 0: null+1
2788 2798
2789 2799 One common tag: longest path wins:
2790 2800
2791 2801 $ hg tag -r 1 -m t1 -d '6 0' t1
2792 2802 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
2793 2803 6: t1+4
2794 2804 5: t1+3
2795 2805 4: t1+2
2796 2806 3: t1+1
2797 2807 2: t1+1
2798 2808 1: t1+0
2799 2809 0: null+1
2800 2810
2801 2811 One ancestor tag: more recent wins:
2802 2812
2803 2813 $ hg tag -r 2 -m t2 -d '7 0' t2
2804 2814 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
2805 2815 7: t2+3
2806 2816 6: t2+2
2807 2817 5: t2+1
2808 2818 4: t1+2
2809 2819 3: t1+1
2810 2820 2: t2+0
2811 2821 1: t1+0
2812 2822 0: null+1
2813 2823
2814 2824 Two branch tags: more recent wins:
2815 2825
2816 2826 $ hg tag -r 3 -m t3 -d '8 0' t3
2817 2827 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
2818 2828 8: t3+5
2819 2829 7: t3+4
2820 2830 6: t3+3
2821 2831 5: t3+2
2822 2832 4: t3+1
2823 2833 3: t3+0
2824 2834 2: t2+0
2825 2835 1: t1+0
2826 2836 0: null+1
2827 2837
2828 2838 Merged tag overrides:
2829 2839
2830 2840 $ hg tag -r 5 -m t5 -d '9 0' t5
2831 2841 $ hg tag -r 3 -m at3 -d '10 0' at3
2832 2842 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
2833 2843 10: t5+5
2834 2844 9: t5+4
2835 2845 8: t5+3
2836 2846 7: t5+2
2837 2847 6: t5+1
2838 2848 5: t5+0
2839 2849 4: at3:t3+1
2840 2850 3: at3:t3+0
2841 2851 2: t2+0
2842 2852 1: t1+0
2843 2853 0: null+1
2844 2854
2845 2855 $ hg log --template "{rev}: {latesttag % '{tag}+{distance},{changes} '}\n"
2846 2856 10: t5+5,5
2847 2857 9: t5+4,4
2848 2858 8: t5+3,3
2849 2859 7: t5+2,2
2850 2860 6: t5+1,1
2851 2861 5: t5+0,0
2852 2862 4: at3+1,1 t3+1,1
2853 2863 3: at3+0,0 t3+0,0
2854 2864 2: t2+0,0
2855 2865 1: t1+0,0
2856 2866 0: null+1,1
2857 2867
2858 2868 $ hg log --template "{rev}: {latesttag('re:^t[13]$') % '{tag}, C: {changes}, D: {distance}'}\n"
2859 2869 10: t3, C: 8, D: 7
2860 2870 9: t3, C: 7, D: 6
2861 2871 8: t3, C: 6, D: 5
2862 2872 7: t3, C: 5, D: 4
2863 2873 6: t3, C: 4, D: 3
2864 2874 5: t3, C: 3, D: 2
2865 2875 4: t3, C: 1, D: 1
2866 2876 3: t3, C: 0, D: 0
2867 2877 2: t1, C: 1, D: 1
2868 2878 1: t1, C: 0, D: 0
2869 2879 0: null, C: 1, D: 1
2870 2880
2871 2881 $ cd ..
2872 2882
2873 2883
2874 2884 Style path expansion: issue1948 - ui.style option doesn't work on OSX
2875 2885 if it is a relative path
2876 2886
2877 2887 $ mkdir -p home/styles
2878 2888
2879 2889 $ cat > home/styles/teststyle <<EOF
2880 2890 > changeset = 'test {rev}:{node|short}\n'
2881 2891 > EOF
2882 2892
2883 2893 $ HOME=`pwd`/home; export HOME
2884 2894
2885 2895 $ cat > latesttag/.hg/hgrc <<EOF
2886 2896 > [ui]
2887 2897 > style = ~/styles/teststyle
2888 2898 > EOF
2889 2899
2890 2900 $ hg -R latesttag tip
2891 2901 test 10:9b4a630e5f5f
2892 2902
2893 2903 Test recursive showlist template (issue1989):
2894 2904
2895 2905 $ cat > style1989 <<EOF
2896 2906 > changeset = '{file_mods}{manifest}{extras}'
2897 2907 > file_mod = 'M|{author|person}\n'
2898 2908 > manifest = '{rev},{author}\n'
2899 2909 > extra = '{key}: {author}\n'
2900 2910 > EOF
2901 2911
2902 2912 $ hg -R latesttag log -r tip --style=style1989
2903 2913 M|test
2904 2914 10,test
2905 2915 branch: test
2906 2916
2907 2917 Test new-style inline templating:
2908 2918
2909 2919 $ hg log -R latesttag -r tip --template 'modified files: {file_mods % " {file}\n"}\n'
2910 2920 modified files: .hgtags
2911 2921
2912 2922
2913 2923 $ hg log -R latesttag -r tip -T '{rev % "a"}\n'
2914 2924 hg: parse error: keyword 'rev' is not iterable
2915 2925 [255]
2916 2926 $ hg log -R latesttag -r tip -T '{get(extras, "unknown") % "a"}\n'
2917 2927 hg: parse error: None is not iterable
2918 2928 [255]
2919 2929
2920 2930 Test the sub function of templating for expansion:
2921 2931
2922 2932 $ hg log -R latesttag -r 10 --template '{sub("[0-9]", "x", "{rev}")}\n'
2923 2933 xx
2924 2934
2925 2935 $ hg log -R latesttag -r 10 -T '{sub("[", "x", rev)}\n'
2926 2936 hg: parse error: sub got an invalid pattern: [
2927 2937 [255]
2928 2938 $ hg log -R latesttag -r 10 -T '{sub("[0-9]", r"\1", rev)}\n'
2929 2939 hg: parse error: sub got an invalid replacement: \1
2930 2940 [255]
2931 2941
2932 2942 Test the strip function with chars specified:
2933 2943
2934 2944 $ hg log -R latesttag --template '{desc}\n'
2935 2945 at3
2936 2946 t5
2937 2947 t3
2938 2948 t2
2939 2949 t1
2940 2950 merge
2941 2951 h2e
2942 2952 h2d
2943 2953 h1c
2944 2954 b
2945 2955 a
2946 2956
2947 2957 $ hg log -R latesttag --template '{strip(desc, "te")}\n'
2948 2958 at3
2949 2959 5
2950 2960 3
2951 2961 2
2952 2962 1
2953 2963 merg
2954 2964 h2
2955 2965 h2d
2956 2966 h1c
2957 2967 b
2958 2968 a
2959 2969
2960 2970 Test date format:
2961 2971
2962 2972 $ hg log -R latesttag --template 'date: {date(date, "%y %m %d %S %z")}\n'
2963 2973 date: 70 01 01 10 +0000
2964 2974 date: 70 01 01 09 +0000
2965 2975 date: 70 01 01 08 +0000
2966 2976 date: 70 01 01 07 +0000
2967 2977 date: 70 01 01 06 +0000
2968 2978 date: 70 01 01 05 +0100
2969 2979 date: 70 01 01 04 +0000
2970 2980 date: 70 01 01 03 +0000
2971 2981 date: 70 01 01 02 +0000
2972 2982 date: 70 01 01 01 +0000
2973 2983 date: 70 01 01 00 +0000
2974 2984
2975 2985 Test invalid date:
2976 2986
2977 2987 $ hg log -R latesttag -T '{date(rev)}\n'
2978 2988 hg: parse error: date expects a date information
2979 2989 [255]
2980 2990
2981 2991 Test integer literal:
2982 2992
2983 2993 $ hg debugtemplate -v '{(0)}\n'
2984 2994 (template
2985 2995 (group
2986 2996 ('integer', '0'))
2987 2997 ('string', '\n'))
2988 2998 0
2989 2999 $ hg debugtemplate -v '{(123)}\n'
2990 3000 (template
2991 3001 (group
2992 3002 ('integer', '123'))
2993 3003 ('string', '\n'))
2994 3004 123
2995 3005 $ hg debugtemplate -v '{(-4)}\n'
2996 3006 (template
2997 3007 (group
2998 3008 (negate
2999 3009 ('integer', '4')))
3000 3010 ('string', '\n'))
3001 3011 -4
3002 3012 $ hg debugtemplate '{(-)}\n'
3003 3013 hg: parse error at 3: not a prefix: )
3004 3014 [255]
3005 3015 $ hg debugtemplate '{(-a)}\n'
3006 3016 hg: parse error: negation needs an integer argument
3007 3017 [255]
3008 3018
3009 3019 top-level integer literal is interpreted as symbol (i.e. variable name):
3010 3020
3011 3021 $ hg debugtemplate -D 1=one -v '{1}\n'
3012 3022 (template
3013 3023 ('integer', '1')
3014 3024 ('string', '\n'))
3015 3025 one
3016 3026 $ hg debugtemplate -D 1=one -v '{if("t", "{1}")}\n'
3017 3027 (template
3018 3028 (func
3019 3029 ('symbol', 'if')
3020 3030 (list
3021 3031 ('string', 't')
3022 3032 (template
3023 3033 ('integer', '1'))))
3024 3034 ('string', '\n'))
3025 3035 one
3026 3036 $ hg debugtemplate -D 1=one -v '{1|stringify}\n'
3027 3037 (template
3028 3038 (|
3029 3039 ('integer', '1')
3030 3040 ('symbol', 'stringify'))
3031 3041 ('string', '\n'))
3032 3042 one
3033 3043
3034 3044 unless explicit symbol is expected:
3035 3045
3036 3046 $ hg log -Ra -r0 -T '{desc|1}\n'
3037 3047 hg: parse error: expected a symbol, got 'integer'
3038 3048 [255]
3039 3049 $ hg log -Ra -r0 -T '{1()}\n'
3040 3050 hg: parse error: expected a symbol, got 'integer'
3041 3051 [255]
3042 3052
3043 3053 Test string literal:
3044 3054
3045 3055 $ hg debugtemplate -Ra -r0 -v '{"string with no template fragment"}\n'
3046 3056 (template
3047 3057 ('string', 'string with no template fragment')
3048 3058 ('string', '\n'))
3049 3059 string with no template fragment
3050 3060 $ hg debugtemplate -Ra -r0 -v '{"template: {rev}"}\n'
3051 3061 (template
3052 3062 (template
3053 3063 ('string', 'template: ')
3054 3064 ('symbol', 'rev'))
3055 3065 ('string', '\n'))
3056 3066 template: 0
3057 3067 $ hg debugtemplate -Ra -r0 -v '{r"rawstring: {rev}"}\n'
3058 3068 (template
3059 3069 ('string', 'rawstring: {rev}')
3060 3070 ('string', '\n'))
3061 3071 rawstring: {rev}
3062 3072 $ hg debugtemplate -Ra -r0 -v '{files % r"rawstring: {file}"}\n'
3063 3073 (template
3064 3074 (%
3065 3075 ('symbol', 'files')
3066 3076 ('string', 'rawstring: {file}'))
3067 3077 ('string', '\n'))
3068 3078 rawstring: {file}
3069 3079
3070 3080 Test string escaping:
3071 3081
3072 3082 $ hg log -R latesttag -r 0 --template '>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
3073 3083 >
3074 3084 <>\n<[>
3075 3085 <>\n<]>
3076 3086 <>\n<
3077 3087
3078 3088 $ hg log -R latesttag -r 0 \
3079 3089 > --config ui.logtemplate='>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
3080 3090 >
3081 3091 <>\n<[>
3082 3092 <>\n<]>
3083 3093 <>\n<
3084 3094
3085 3095 $ hg log -R latesttag -r 0 -T esc \
3086 3096 > --config templates.esc='>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
3087 3097 >
3088 3098 <>\n<[>
3089 3099 <>\n<]>
3090 3100 <>\n<
3091 3101
3092 3102 $ cat <<'EOF' > esctmpl
3093 3103 > changeset = '>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
3094 3104 > EOF
3095 3105 $ hg log -R latesttag -r 0 --style ./esctmpl
3096 3106 >
3097 3107 <>\n<[>
3098 3108 <>\n<]>
3099 3109 <>\n<
3100 3110
3101 3111 Test string escaping of quotes:
3102 3112
3103 3113 $ hg log -Ra -r0 -T '{"\""}\n'
3104 3114 "
3105 3115 $ hg log -Ra -r0 -T '{"\\\""}\n'
3106 3116 \"
3107 3117 $ hg log -Ra -r0 -T '{r"\""}\n'
3108 3118 \"
3109 3119 $ hg log -Ra -r0 -T '{r"\\\""}\n'
3110 3120 \\\"
3111 3121
3112 3122
3113 3123 $ hg log -Ra -r0 -T '{"\""}\n'
3114 3124 "
3115 3125 $ hg log -Ra -r0 -T '{"\\\""}\n'
3116 3126 \"
3117 3127 $ hg log -Ra -r0 -T '{r"\""}\n'
3118 3128 \"
3119 3129 $ hg log -Ra -r0 -T '{r"\\\""}\n'
3120 3130 \\\"
3121 3131
3122 3132 Test exception in quoted template. single backslash before quotation mark is
3123 3133 stripped before parsing:
3124 3134
3125 3135 $ cat <<'EOF' > escquotetmpl
3126 3136 > changeset = "\" \\" \\\" \\\\" {files % \"{file}\"}\n"
3127 3137 > EOF
3128 3138 $ cd latesttag
3129 3139 $ hg log -r 2 --style ../escquotetmpl
3130 3140 " \" \" \\" head1
3131 3141
3132 3142 $ hg log -r 2 -T esc --config templates.esc='"{\"valid\"}\n"'
3133 3143 valid
3134 3144 $ hg log -r 2 -T esc --config templates.esc="'"'{\'"'"'valid\'"'"'}\n'"'"
3135 3145 valid
3136 3146
3137 3147 Test compatibility with 2.9.2-3.4 of escaped quoted strings in nested
3138 3148 _evalifliteral() templates (issue4733):
3139 3149
3140 3150 $ hg log -r 2 -T '{if(rev, "\"{rev}")}\n'
3141 3151 "2
3142 3152 $ hg log -r 2 -T '{if(rev, "{if(rev, \"\\\"{rev}\")}")}\n'
3143 3153 "2
3144 3154 $ hg log -r 2 -T '{if(rev, "{if(rev, \"{if(rev, \\\"\\\\\\\"{rev}\\\")}\")}")}\n'
3145 3155 "2
3146 3156
3147 3157 $ hg log -r 2 -T '{if(rev, "\\\"")}\n'
3148 3158 \"
3149 3159 $ hg log -r 2 -T '{if(rev, "{if(rev, \"\\\\\\\"\")}")}\n'
3150 3160 \"
3151 3161 $ hg log -r 2 -T '{if(rev, "{if(rev, \"{if(rev, \\\"\\\\\\\\\\\\\\\"\\\")}\")}")}\n'
3152 3162 \"
3153 3163
3154 3164 $ hg log -r 2 -T '{if(rev, r"\\\"")}\n'
3155 3165 \\\"
3156 3166 $ hg log -r 2 -T '{if(rev, "{if(rev, r\"\\\\\\\"\")}")}\n'
3157 3167 \\\"
3158 3168 $ hg log -r 2 -T '{if(rev, "{if(rev, \"{if(rev, r\\\"\\\\\\\\\\\\\\\"\\\")}\")}")}\n'
3159 3169 \\\"
3160 3170
3161 3171 escaped single quotes and errors:
3162 3172
3163 3173 $ hg log -r 2 -T "{if(rev, '{if(rev, \'foo\')}')}"'\n'
3164 3174 foo
3165 3175 $ hg log -r 2 -T "{if(rev, '{if(rev, r\'foo\')}')}"'\n'
3166 3176 foo
3167 3177 $ hg log -r 2 -T '{if(rev, "{if(rev, \")}")}\n'
3168 3178 hg: parse error at 21: unterminated string
3169 3179 [255]
3170 3180 $ hg log -r 2 -T '{if(rev, \"\\"")}\n'
3171 3181 hg: parse error: trailing \ in string
3172 3182 [255]
3173 3183 $ hg log -r 2 -T '{if(rev, r\"\\"")}\n'
3174 3184 hg: parse error: trailing \ in string
3175 3185 [255]
3176 3186
3177 3187 $ cd ..
3178 3188
3179 3189 Test leading backslashes:
3180 3190
3181 3191 $ cd latesttag
3182 3192 $ hg log -r 2 -T '\{rev} {files % "\{file}"}\n'
3183 3193 {rev} {file}
3184 3194 $ hg log -r 2 -T '\\{rev} {files % "\\{file}"}\n'
3185 3195 \2 \head1
3186 3196 $ hg log -r 2 -T '\\\{rev} {files % "\\\{file}"}\n'
3187 3197 \{rev} \{file}
3188 3198 $ cd ..
3189 3199
3190 3200 Test leading backslashes in "if" expression (issue4714):
3191 3201
3192 3202 $ cd latesttag
3193 3203 $ hg log -r 2 -T '{if("1", "\{rev}")} {if("1", r"\{rev}")}\n'
3194 3204 {rev} \{rev}
3195 3205 $ hg log -r 2 -T '{if("1", "\\{rev}")} {if("1", r"\\{rev}")}\n'
3196 3206 \2 \\{rev}
3197 3207 $ hg log -r 2 -T '{if("1", "\\\{rev}")} {if("1", r"\\\{rev}")}\n'
3198 3208 \{rev} \\\{rev}
3199 3209 $ cd ..
3200 3210
3201 3211 "string-escape"-ed "\x5c\x786e" becomes r"\x6e" (once) or r"n" (twice)
3202 3212
3203 3213 $ hg log -R a -r 0 --template '{if("1", "\x5c\x786e", "NG")}\n'
3204 3214 \x6e
3205 3215 $ hg log -R a -r 0 --template '{if("1", r"\x5c\x786e", "NG")}\n'
3206 3216 \x5c\x786e
3207 3217 $ hg log -R a -r 0 --template '{if("", "NG", "\x5c\x786e")}\n'
3208 3218 \x6e
3209 3219 $ hg log -R a -r 0 --template '{if("", "NG", r"\x5c\x786e")}\n'
3210 3220 \x5c\x786e
3211 3221
3212 3222 $ hg log -R a -r 2 --template '{ifeq("no perso\x6e", desc, "\x5c\x786e", "NG")}\n'
3213 3223 \x6e
3214 3224 $ hg log -R a -r 2 --template '{ifeq(r"no perso\x6e", desc, "NG", r"\x5c\x786e")}\n'
3215 3225 \x5c\x786e
3216 3226 $ hg log -R a -r 2 --template '{ifeq(desc, "no perso\x6e", "\x5c\x786e", "NG")}\n'
3217 3227 \x6e
3218 3228 $ hg log -R a -r 2 --template '{ifeq(desc, r"no perso\x6e", "NG", r"\x5c\x786e")}\n'
3219 3229 \x5c\x786e
3220 3230
3221 3231 $ hg log -R a -r 8 --template '{join(files, "\n")}\n'
3222 3232 fourth
3223 3233 second
3224 3234 third
3225 3235 $ hg log -R a -r 8 --template '{join(files, r"\n")}\n'
3226 3236 fourth\nsecond\nthird
3227 3237
3228 3238 $ hg log -R a -r 2 --template '{rstdoc("1st\n\n2nd", "htm\x6c")}'
3229 3239 <p>
3230 3240 1st
3231 3241 </p>
3232 3242 <p>
3233 3243 2nd
3234 3244 </p>
3235 3245 $ hg log -R a -r 2 --template '{rstdoc(r"1st\n\n2nd", "html")}'
3236 3246 <p>
3237 3247 1st\n\n2nd
3238 3248 </p>
3239 3249 $ hg log -R a -r 2 --template '{rstdoc("1st\n\n2nd", r"htm\x6c")}'
3240 3250 1st
3241 3251
3242 3252 2nd
3243 3253
3244 3254 $ hg log -R a -r 2 --template '{strip(desc, "\x6e")}\n'
3245 3255 o perso
3246 3256 $ hg log -R a -r 2 --template '{strip(desc, r"\x6e")}\n'
3247 3257 no person
3248 3258 $ hg log -R a -r 2 --template '{strip("no perso\x6e", "\x6e")}\n'
3249 3259 o perso
3250 3260 $ hg log -R a -r 2 --template '{strip(r"no perso\x6e", r"\x6e")}\n'
3251 3261 no perso
3252 3262
3253 3263 $ hg log -R a -r 2 --template '{sub("\\x6e", "\x2d", desc)}\n'
3254 3264 -o perso-
3255 3265 $ hg log -R a -r 2 --template '{sub(r"\\x6e", "-", desc)}\n'
3256 3266 no person
3257 3267 $ hg log -R a -r 2 --template '{sub("n", r"\x2d", desc)}\n'
3258 3268 \x2do perso\x2d
3259 3269 $ hg log -R a -r 2 --template '{sub("n", "\x2d", "no perso\x6e")}\n'
3260 3270 -o perso-
3261 3271 $ hg log -R a -r 2 --template '{sub("n", r"\x2d", r"no perso\x6e")}\n'
3262 3272 \x2do perso\x6e
3263 3273
3264 3274 $ hg log -R a -r 8 --template '{files % "{file}\n"}'
3265 3275 fourth
3266 3276 second
3267 3277 third
3268 3278
3269 3279 Test string escaping in nested expression:
3270 3280
3271 3281 $ hg log -R a -r 8 --template '{ifeq(r"\x6e", if("1", "\x5c\x786e"), join(files, "\x5c\x786e"))}\n'
3272 3282 fourth\x6esecond\x6ethird
3273 3283 $ hg log -R a -r 8 --template '{ifeq(if("1", r"\x6e"), "\x5c\x786e", join(files, "\x5c\x786e"))}\n'
3274 3284 fourth\x6esecond\x6ethird
3275 3285
3276 3286 $ hg log -R a -r 8 --template '{join(files, ifeq(branch, "default", "\x5c\x786e"))}\n'
3277 3287 fourth\x6esecond\x6ethird
3278 3288 $ hg log -R a -r 8 --template '{join(files, ifeq(branch, "default", r"\x5c\x786e"))}\n'
3279 3289 fourth\x5c\x786esecond\x5c\x786ethird
3280 3290
3281 3291 $ hg log -R a -r 3:4 --template '{rev}:{sub(if("1", "\x6e"), ifeq(branch, "foo", r"\x5c\x786e", "\x5c\x786e"), desc)}\n'
3282 3292 3:\x6eo user, \x6eo domai\x6e
3283 3293 4:\x5c\x786eew bra\x5c\x786ech
3284 3294
3285 3295 Test quotes in nested expression are evaluated just like a $(command)
3286 3296 substitution in POSIX shells:
3287 3297
3288 3298 $ hg log -R a -r 8 -T '{"{"{rev}:{node|short}"}"}\n'
3289 3299 8:95c24699272e
3290 3300 $ hg log -R a -r 8 -T '{"{"\{{rev}} \"{node|short}\""}"}\n'
3291 3301 {8} "95c24699272e"
3292 3302
3293 3303 Test recursive evaluation:
3294 3304
3295 3305 $ hg init r
3296 3306 $ cd r
3297 3307 $ echo a > a
3298 3308 $ hg ci -Am '{rev}'
3299 3309 adding a
3300 3310 $ hg log -r 0 --template '{if(rev, desc)}\n'
3301 3311 {rev}
3302 3312 $ hg log -r 0 --template '{if(rev, "{author} {rev}")}\n'
3303 3313 test 0
3304 3314
3305 3315 $ hg branch -q 'text.{rev}'
3306 3316 $ echo aa >> aa
3307 3317 $ hg ci -u '{node|short}' -m 'desc to be wrapped desc to be wrapped'
3308 3318
3309 3319 $ hg log -l1 --template '{fill(desc, "20", author, branch)}'
3310 3320 {node|short}desc to
3311 3321 text.{rev}be wrapped
3312 3322 text.{rev}desc to be
3313 3323 text.{rev}wrapped (no-eol)
3314 3324 $ hg log -l1 --template '{fill(desc, "20", "{node|short}:", "text.{rev}:")}'
3315 3325 bcc7ff960b8e:desc to
3316 3326 text.1:be wrapped
3317 3327 text.1:desc to be
3318 3328 text.1:wrapped (no-eol)
3319 3329 $ hg log -l1 -T '{fill(desc, date, "", "")}\n'
3320 3330 hg: parse error: fill expects an integer width
3321 3331 [255]
3322 3332
3323 3333 $ COLUMNS=25 hg log -l1 --template '{fill(desc, termwidth, "{node|short}:", "termwidth.{rev}:")}'
3324 3334 bcc7ff960b8e:desc to be
3325 3335 termwidth.1:wrapped desc
3326 3336 termwidth.1:to be wrapped (no-eol)
3327 3337
3328 3338 $ hg log -l 1 --template '{sub(r"[0-9]", "-", author)}'
3329 3339 {node|short} (no-eol)
3330 3340 $ hg log -l 1 --template '{sub(r"[0-9]", "-", "{node|short}")}'
3331 3341 bcc-ff---b-e (no-eol)
3332 3342
3333 3343 $ cat >> .hg/hgrc <<EOF
3334 3344 > [extensions]
3335 3345 > color=
3336 3346 > [color]
3337 3347 > mode=ansi
3338 3348 > text.{rev} = red
3339 3349 > text.1 = green
3340 3350 > EOF
3341 3351 $ hg log --color=always -l 1 --template '{label(branch, "text\n")}'
3342 3352 \x1b[0;31mtext\x1b[0m (esc)
3343 3353 $ hg log --color=always -l 1 --template '{label("text.{rev}", "text\n")}'
3344 3354 \x1b[0;32mtext\x1b[0m (esc)
3345 3355
3346 3356 color effect can be specified without quoting:
3347 3357
3348 3358 $ hg log --color=always -l 1 --template '{label(red, "text\n")}'
3349 3359 \x1b[0;31mtext\x1b[0m (esc)
3350 3360
3351 3361 color effects can be nested (issue5413)
3352 3362
3353 3363 $ hg debugtemplate --color=always \
3354 3364 > '{label(red, "red{label(magenta, "ma{label(cyan, "cyan")}{label(yellow, "yellow")}genta")}")}\n'
3355 3365 \x1b[0;31mred\x1b[0;35mma\x1b[0;36mcyan\x1b[0m\x1b[0;31m\x1b[0;35m\x1b[0;33myellow\x1b[0m\x1b[0;31m\x1b[0;35mgenta\x1b[0m (esc)
3356 3366
3357 3367 pad() should interact well with color codes (issue5416)
3358 3368
3359 3369 $ hg debugtemplate --color=always \
3360 3370 > '{pad(label(red, "red"), 5, label(cyan, "-"))}\n'
3361 3371 \x1b[0;31mred\x1b[0m\x1b[0;36m-\x1b[0m\x1b[0;36m-\x1b[0m (esc)
3362 3372
3363 3373 label should be no-op if color is disabled:
3364 3374
3365 3375 $ hg log --color=never -l 1 --template '{label(red, "text\n")}'
3366 3376 text
3367 3377 $ hg log --config extensions.color=! -l 1 --template '{label(red, "text\n")}'
3368 3378 text
3369 3379
3370 3380 Test branches inside if statement:
3371 3381
3372 3382 $ hg log -r 0 --template '{if(branches, "yes", "no")}\n'
3373 3383 no
3374 3384
3375 3385 Test get function:
3376 3386
3377 3387 $ hg log -r 0 --template '{get(extras, "branch")}\n'
3378 3388 default
3379 3389 $ hg log -r 0 --template '{get(extras, "br{"anch"}")}\n'
3380 3390 default
3381 3391 $ hg log -r 0 --template '{get(files, "should_fail")}\n'
3382 3392 hg: parse error: get() expects a dict as first argument
3383 3393 [255]
3384 3394
3385 3395 Test localdate(date, tz) function:
3386 3396
3387 3397 $ TZ=JST-09 hg log -r0 -T '{date|localdate|isodate}\n'
3388 3398 1970-01-01 09:00 +0900
3389 3399 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "UTC")|isodate}\n'
3390 3400 1970-01-01 00:00 +0000
3391 3401 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "blahUTC")|isodate}\n'
3392 3402 hg: parse error: localdate expects a timezone
3393 3403 [255]
3394 3404 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "+0200")|isodate}\n'
3395 3405 1970-01-01 02:00 +0200
3396 3406 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "0")|isodate}\n'
3397 3407 1970-01-01 00:00 +0000
3398 3408 $ TZ=JST-09 hg log -r0 -T '{localdate(date, 0)|isodate}\n'
3399 3409 1970-01-01 00:00 +0000
3400 3410 $ hg log -r0 -T '{localdate(date, "invalid")|isodate}\n'
3401 3411 hg: parse error: localdate expects a timezone
3402 3412 [255]
3403 3413 $ hg log -r0 -T '{localdate(date, date)|isodate}\n'
3404 3414 hg: parse error: localdate expects a timezone
3405 3415 [255]
3406 3416
3407 3417 Test shortest(node) function:
3408 3418
3409 3419 $ echo b > b
3410 3420 $ hg ci -qAm b
3411 3421 $ hg log --template '{shortest(node)}\n'
3412 3422 e777
3413 3423 bcc7
3414 3424 f776
3415 3425 $ hg log --template '{shortest(node, 10)}\n'
3416 3426 e777603221
3417 3427 bcc7ff960b
3418 3428 f7769ec2ab
3419 3429 $ hg log --template '{node|shortest}\n' -l1
3420 3430 e777
3421 3431
3422 3432 $ hg log -r 0 -T '{shortest(node, "1{"0"}")}\n'
3423 3433 f7769ec2ab
3424 3434 $ hg log -r 0 -T '{shortest(node, "not an int")}\n'
3425 3435 hg: parse error: shortest() expects an integer minlength
3426 3436 [255]
3427 3437
3428 3438 $ cd ..
3429 3439
3430 3440 Test shortest(node) with the repo having short hash collision:
3431 3441
3432 3442 $ hg init hashcollision
3433 3443 $ cd hashcollision
3434 3444 $ cat <<EOF >> .hg/hgrc
3435 3445 > [experimental]
3436 3446 > evolution = createmarkers
3437 3447 > EOF
3438 3448 $ echo 0 > a
3439 3449 $ hg ci -qAm 0
3440 3450 $ for i in 17 129 248 242 480 580 617 1057 2857 4025; do
3441 3451 > hg up -q 0
3442 3452 > echo $i > a
3443 3453 > hg ci -qm $i
3444 3454 > done
3445 3455 $ hg up -q null
3446 3456 $ hg log -r0: -T '{rev}:{node}\n'
3447 3457 0:b4e73ffab476aa0ee32ed81ca51e07169844bc6a
3448 3458 1:11424df6dc1dd4ea255eae2b58eaca7831973bbc
3449 3459 2:11407b3f1b9c3e76a79c1ec5373924df096f0499
3450 3460 3:11dd92fe0f39dfdaacdaa5f3997edc533875cfc4
3451 3461 4:10776689e627b465361ad5c296a20a487e153ca4
3452 3462 5:a00be79088084cb3aff086ab799f8790e01a976b
3453 3463 6:a0b0acd79b4498d0052993d35a6a748dd51d13e6
3454 3464 7:a0457b3450b8e1b778f1163b31a435802987fe5d
3455 3465 8:c56256a09cd28e5764f32e8e2810d0f01e2e357a
3456 3466 9:c5623987d205cd6d9d8389bfc40fff9dbb670b48
3457 3467 10:c562ddd9c94164376c20b86b0b4991636a3bf84f
3458 3468 $ hg debugobsolete a00be79088084cb3aff086ab799f8790e01a976b
3459 3469 $ hg debugobsolete c5623987d205cd6d9d8389bfc40fff9dbb670b48
3460 3470 $ hg debugobsolete c562ddd9c94164376c20b86b0b4991636a3bf84f
3461 3471
3462 3472 nodes starting with '11' (we don't have the revision number '11' though)
3463 3473
3464 3474 $ hg log -r 1:3 -T '{rev}:{shortest(node, 0)}\n'
3465 3475 1:1142
3466 3476 2:1140
3467 3477 3:11d
3468 3478
3469 3479 '5:a00' is hidden, but still we have two nodes starting with 'a0'
3470 3480
3471 3481 $ hg log -r 6:7 -T '{rev}:{shortest(node, 0)}\n'
3472 3482 6:a0b
3473 3483 7:a04
3474 3484
3475 3485 node '10' conflicts with the revision number '10' even if it is hidden
3476 3486 (we could exclude hidden revision numbers, but currently we don't)
3477 3487
3478 3488 $ hg log -r 4 -T '{rev}:{shortest(node, 0)}\n'
3479 3489 4:107
3480 3490 $ hg log -r 4 -T '{rev}:{shortest(node, 0)}\n' --hidden
3481 3491 4:107
3482 3492
3483 3493 node 'c562' should be unique if the other 'c562' nodes are hidden
3484 3494 (but we don't try the slow path to filter out hidden nodes for now)
3485 3495
3486 3496 $ hg log -r 8 -T '{rev}:{node|shortest}\n'
3487 3497 8:c5625
3488 3498 $ hg log -r 8:10 -T '{rev}:{node|shortest}\n' --hidden
3489 3499 8:c5625
3490 3500 9:c5623
3491 3501 10:c562d
3492 3502
3493 3503 $ cd ..
3494 3504
3495 3505 Test pad function
3496 3506
3497 3507 $ cd r
3498 3508
3499 3509 $ hg log --template '{pad(rev, 20)} {author|user}\n'
3500 3510 2 test
3501 3511 1 {node|short}
3502 3512 0 test
3503 3513
3504 3514 $ hg log --template '{pad(rev, 20, " ", True)} {author|user}\n'
3505 3515 2 test
3506 3516 1 {node|short}
3507 3517 0 test
3508 3518
3509 3519 $ hg log --template '{pad(rev, 20, "-", False)} {author|user}\n'
3510 3520 2------------------- test
3511 3521 1------------------- {node|short}
3512 3522 0------------------- test
3513 3523
3514 3524 Test template string in pad function
3515 3525
3516 3526 $ hg log -r 0 -T '{pad("\{{rev}}", 10)} {author|user}\n'
3517 3527 {0} test
3518 3528
3519 3529 $ hg log -r 0 -T '{pad(r"\{rev}", 10)} {author|user}\n'
3520 3530 \{rev} test
3521 3531
3522 3532 Test width argument passed to pad function
3523 3533
3524 3534 $ hg log -r 0 -T '{pad(rev, "1{"0"}")} {author|user}\n'
3525 3535 0 test
3526 3536 $ hg log -r 0 -T '{pad(rev, "not an int")}\n'
3527 3537 hg: parse error: pad() expects an integer width
3528 3538 [255]
3529 3539
3530 3540 Test invalid fillchar passed to pad function
3531 3541
3532 3542 $ hg log -r 0 -T '{pad(rev, 10, "")}\n'
3533 3543 hg: parse error: pad() expects a single fill character
3534 3544 [255]
3535 3545 $ hg log -r 0 -T '{pad(rev, 10, "--")}\n'
3536 3546 hg: parse error: pad() expects a single fill character
3537 3547 [255]
3538 3548
3539 3549 Test boolean argument passed to pad function
3540 3550
3541 3551 no crash
3542 3552
3543 3553 $ hg log -r 0 -T '{pad(rev, 10, "-", "f{"oo"}")}\n'
3544 3554 ---------0
3545 3555
3546 3556 string/literal
3547 3557
3548 3558 $ hg log -r 0 -T '{pad(rev, 10, "-", "false")}\n'
3549 3559 ---------0
3550 3560 $ hg log -r 0 -T '{pad(rev, 10, "-", false)}\n'
3551 3561 0---------
3552 3562 $ hg log -r 0 -T '{pad(rev, 10, "-", "")}\n'
3553 3563 0---------
3554 3564
3555 3565 unknown keyword is evaluated to ''
3556 3566
3557 3567 $ hg log -r 0 -T '{pad(rev, 10, "-", unknownkeyword)}\n'
3558 3568 0---------
3559 3569
3560 3570 Test separate function
3561 3571
3562 3572 $ hg log -r 0 -T '{separate("-", "", "a", "b", "", "", "c", "")}\n'
3563 3573 a-b-c
3564 3574 $ hg log -r 0 -T '{separate(" ", "{rev}:{node|short}", author|user, branch)}\n'
3565 3575 0:f7769ec2ab97 test default
3566 3576 $ hg log -r 0 --color=always -T '{separate(" ", "a", label(red, "b"), "c", label(red, ""), "d")}\n'
3567 3577 a \x1b[0;31mb\x1b[0m c d (esc)
3568 3578
3569 3579 Test boolean expression/literal passed to if function
3570 3580
3571 3581 $ hg log -r 0 -T '{if(rev, "rev 0 is True")}\n'
3572 3582 rev 0 is True
3573 3583 $ hg log -r 0 -T '{if(0, "literal 0 is True as well")}\n'
3574 3584 literal 0 is True as well
3575 3585 $ hg log -r 0 -T '{if("", "", "empty string is False")}\n'
3576 3586 empty string is False
3577 3587 $ hg log -r 0 -T '{if(revset(r"0 - 0"), "", "empty list is False")}\n'
3578 3588 empty list is False
3579 3589 $ hg log -r 0 -T '{if(true, "true is True")}\n'
3580 3590 true is True
3581 3591 $ hg log -r 0 -T '{if(false, "", "false is False")}\n'
3582 3592 false is False
3583 3593 $ hg log -r 0 -T '{if("false", "non-empty string is True")}\n'
3584 3594 non-empty string is True
3585 3595
3586 3596 Test ifcontains function
3587 3597
3588 3598 $ hg log --template '{rev} {ifcontains(rev, "2 two 0", "is in the string", "is not")}\n'
3589 3599 2 is in the string
3590 3600 1 is not
3591 3601 0 is in the string
3592 3602
3593 3603 $ hg log -T '{rev} {ifcontains(rev, "2 two{" 0"}", "is in the string", "is not")}\n'
3594 3604 2 is in the string
3595 3605 1 is not
3596 3606 0 is in the string
3597 3607
3598 3608 $ hg log --template '{rev} {ifcontains("a", file_adds, "added a", "did not add a")}\n'
3599 3609 2 did not add a
3600 3610 1 did not add a
3601 3611 0 added a
3602 3612
3603 3613 $ hg log --debug -T '{rev}{ifcontains(1, parents, " is parent of 1")}\n'
3604 3614 2 is parent of 1
3605 3615 1
3606 3616 0
3607 3617
3608 3618 Test revset function
3609 3619
3610 3620 $ hg log --template '{rev} {ifcontains(rev, revset("."), "current rev", "not current rev")}\n'
3611 3621 2 current rev
3612 3622 1 not current rev
3613 3623 0 not current rev
3614 3624
3615 3625 $ hg log --template '{rev} {ifcontains(rev, revset(". + .^"), "match rev", "not match rev")}\n'
3616 3626 2 match rev
3617 3627 1 match rev
3618 3628 0 not match rev
3619 3629
3620 3630 $ hg log --template '{rev} Parents: {revset("parents(%s)", rev)}\n'
3621 3631 2 Parents: 1
3622 3632 1 Parents: 0
3623 3633 0 Parents:
3624 3634
3625 3635 $ cat >> .hg/hgrc <<EOF
3626 3636 > [revsetalias]
3627 3637 > myparents(\$1) = parents(\$1)
3628 3638 > EOF
3629 3639 $ hg log --template '{rev} Parents: {revset("myparents(%s)", rev)}\n'
3630 3640 2 Parents: 1
3631 3641 1 Parents: 0
3632 3642 0 Parents:
3633 3643
3634 3644 $ hg log --template 'Rev: {rev}\n{revset("::%s", rev) % "Ancestor: {revision}\n"}\n'
3635 3645 Rev: 2
3636 3646 Ancestor: 0
3637 3647 Ancestor: 1
3638 3648 Ancestor: 2
3639 3649
3640 3650 Rev: 1
3641 3651 Ancestor: 0
3642 3652 Ancestor: 1
3643 3653
3644 3654 Rev: 0
3645 3655 Ancestor: 0
3646 3656
3647 3657 $ hg log --template '{revset("TIP"|lower)}\n' -l1
3648 3658 2
3649 3659
3650 3660 $ hg log -T '{revset("%s", "t{"ip"}")}\n' -l1
3651 3661 2
3652 3662
3653 3663 a list template is evaluated for each item of revset/parents
3654 3664
3655 3665 $ hg log -T '{rev} p: {revset("p1(%s)", rev) % "{rev}:{node|short}"}\n'
3656 3666 2 p: 1:bcc7ff960b8e
3657 3667 1 p: 0:f7769ec2ab97
3658 3668 0 p:
3659 3669
3660 3670 $ hg log --debug -T '{rev} p:{parents % " {rev}:{node|short}"}\n'
3661 3671 2 p: 1:bcc7ff960b8e -1:000000000000
3662 3672 1 p: 0:f7769ec2ab97 -1:000000000000
3663 3673 0 p: -1:000000000000 -1:000000000000
3664 3674
3665 3675 therefore, 'revcache' should be recreated for each rev
3666 3676
3667 3677 $ hg log -T '{rev} {file_adds}\np {revset("p1(%s)", rev) % "{file_adds}"}\n'
3668 3678 2 aa b
3669 3679 p
3670 3680 1
3671 3681 p a
3672 3682 0 a
3673 3683 p
3674 3684
3675 3685 $ hg log --debug -T '{rev} {file_adds}\np {parents % "{file_adds}"}\n'
3676 3686 2 aa b
3677 3687 p
3678 3688 1
3679 3689 p a
3680 3690 0 a
3681 3691 p
3682 3692
3683 3693 a revset item must be evaluated as an integer revision, not an offset from tip
3684 3694
3685 3695 $ hg log -l 1 -T '{revset("null") % "{rev}:{node|short}"}\n'
3686 3696 -1:000000000000
3687 3697 $ hg log -l 1 -T '{revset("%s", "null") % "{rev}:{node|short}"}\n'
3688 3698 -1:000000000000
3689 3699
3690 3700 join() should pick '{rev}' from revset items:
3691 3701
3692 3702 $ hg log -R ../a -T '{join(revset("parents(%d)", rev), ", ")}\n' -r6
3693 3703 4, 5
3694 3704
3695 3705 on the other hand, parents are formatted as '{rev}:{node|formatnode}' by
3696 3706 default. join() should agree with the default formatting:
3697 3707
3698 3708 $ hg log -R ../a -T '{join(parents, ", ")}\n' -r6
3699 3709 5:13207e5a10d9, 4:bbe44766e73d
3700 3710
3701 3711 $ hg log -R ../a -T '{join(parents, ",\n")}\n' -r6 --debug
3702 3712 5:13207e5a10d9fd28ec424934298e176197f2c67f,
3703 3713 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
3704 3714
3705 3715 Test files function
3706 3716
3707 3717 $ hg log -T "{rev}\n{join(files('*'), '\n')}\n"
3708 3718 2
3709 3719 a
3710 3720 aa
3711 3721 b
3712 3722 1
3713 3723 a
3714 3724 0
3715 3725 a
3716 3726
3717 3727 $ hg log -T "{rev}\n{join(files('aa'), '\n')}\n"
3718 3728 2
3719 3729 aa
3720 3730 1
3721 3731
3722 3732 0
3723 3733
3724 3734
3725 3735 Test relpath function
3726 3736
3727 3737 $ hg log -r0 -T '{files % "{file|relpath}\n"}'
3728 3738 a
3729 3739 $ cd ..
3730 3740 $ hg log -R r -r0 -T '{files % "{file|relpath}\n"}'
3731 3741 r/a
3732 3742 $ cd r
3733 3743
3734 3744 Test active bookmark templating
3735 3745
3736 3746 $ hg book foo
3737 3747 $ hg book bar
3738 3748 $ hg log --template "{rev} {bookmarks % '{bookmark}{ifeq(bookmark, active, \"*\")} '}\n"
3739 3749 2 bar* foo
3740 3750 1
3741 3751 0
3742 3752 $ hg log --template "{rev} {activebookmark}\n"
3743 3753 2 bar
3744 3754 1
3745 3755 0
3746 3756 $ hg bookmarks --inactive bar
3747 3757 $ hg log --template "{rev} {activebookmark}\n"
3748 3758 2
3749 3759 1
3750 3760 0
3751 3761 $ hg book -r1 baz
3752 3762 $ hg log --template "{rev} {join(bookmarks, ' ')}\n"
3753 3763 2 bar foo
3754 3764 1 baz
3755 3765 0
3756 3766 $ hg log --template "{rev} {ifcontains('foo', bookmarks, 't', 'f')}\n"
3757 3767 2 t
3758 3768 1 f
3759 3769 0 f
3760 3770
3761 3771 Test namespaces dict
3762 3772
3763 3773 $ hg log -T '{rev}{namespaces % " {namespace}={join(names, ",")}"}\n'
3764 3774 2 bookmarks=bar,foo tags=tip branches=text.{rev}
3765 3775 1 bookmarks=baz tags= branches=text.{rev}
3766 3776 0 bookmarks= tags= branches=default
3767 3777 $ hg log -r2 -T '{namespaces % "{namespace}: {names}\n"}'
3768 3778 bookmarks: bar foo
3769 3779 tags: tip
3770 3780 branches: text.{rev}
3771 3781 $ hg log -r2 -T '{namespaces % "{namespace}:\n{names % " {name}\n"}"}'
3772 3782 bookmarks:
3773 3783 bar
3774 3784 foo
3775 3785 tags:
3776 3786 tip
3777 3787 branches:
3778 3788 text.{rev}
3779 3789 $ hg log -r2 -T '{get(namespaces, "bookmarks") % "{name}\n"}'
3780 3790 bar
3781 3791 foo
3782 3792
3783 3793 Test stringify on sub expressions
3784 3794
3785 3795 $ cd ..
3786 3796 $ hg log -R a -r 8 --template '{join(files, if("1", if("1", ", ")))}\n'
3787 3797 fourth, second, third
3788 3798 $ hg log -R a -r 8 --template '{strip(if("1", if("1", "-abc-")), if("1", if("1", "-")))}\n'
3789 3799 abc
3790 3800
3791 3801 Test splitlines
3792 3802
3793 3803 $ hg log -Gv -R a --template "{splitlines(desc) % 'foo {line}\n'}"
3794 3804 @ foo Modify, add, remove, rename
3795 3805 |
3796 3806 o foo future
3797 3807 |
3798 3808 o foo third
3799 3809 |
3800 3810 o foo second
3801 3811
3802 3812 o foo merge
3803 3813 |\
3804 3814 | o foo new head
3805 3815 | |
3806 3816 o | foo new branch
3807 3817 |/
3808 3818 o foo no user, no domain
3809 3819 |
3810 3820 o foo no person
3811 3821 |
3812 3822 o foo other 1
3813 3823 | foo other 2
3814 3824 | foo
3815 3825 | foo other 3
3816 3826 o foo line 1
3817 3827 foo line 2
3818 3828
3819 3829 Test startswith
3820 3830 $ hg log -Gv -R a --template "{startswith(desc)}"
3821 3831 hg: parse error: startswith expects two arguments
3822 3832 [255]
3823 3833
3824 3834 $ hg log -Gv -R a --template "{startswith('line', desc)}"
3825 3835 @
3826 3836 |
3827 3837 o
3828 3838 |
3829 3839 o
3830 3840 |
3831 3841 o
3832 3842
3833 3843 o
3834 3844 |\
3835 3845 | o
3836 3846 | |
3837 3847 o |
3838 3848 |/
3839 3849 o
3840 3850 |
3841 3851 o
3842 3852 |
3843 3853 o
3844 3854 |
3845 3855 o line 1
3846 3856 line 2
3847 3857
3848 3858 Test bad template with better error message
3849 3859
3850 3860 $ hg log -Gv -R a --template '{desc|user()}'
3851 3861 hg: parse error: expected a symbol, got 'func'
3852 3862 [255]
3853 3863
3854 3864 Test word function (including index out of bounds graceful failure)
3855 3865
3856 3866 $ hg log -Gv -R a --template "{word('1', desc)}"
3857 3867 @ add,
3858 3868 |
3859 3869 o
3860 3870 |
3861 3871 o
3862 3872 |
3863 3873 o
3864 3874
3865 3875 o
3866 3876 |\
3867 3877 | o head
3868 3878 | |
3869 3879 o | branch
3870 3880 |/
3871 3881 o user,
3872 3882 |
3873 3883 o person
3874 3884 |
3875 3885 o 1
3876 3886 |
3877 3887 o 1
3878 3888
3879 3889
3880 3890 Test word third parameter used as splitter
3881 3891
3882 3892 $ hg log -Gv -R a --template "{word('0', desc, 'o')}"
3883 3893 @ M
3884 3894 |
3885 3895 o future
3886 3896 |
3887 3897 o third
3888 3898 |
3889 3899 o sec
3890 3900
3891 3901 o merge
3892 3902 |\
3893 3903 | o new head
3894 3904 | |
3895 3905 o | new branch
3896 3906 |/
3897 3907 o n
3898 3908 |
3899 3909 o n
3900 3910 |
3901 3911 o
3902 3912 |
3903 3913 o line 1
3904 3914 line 2
3905 3915
3906 3916 Test word error messages for not enough and too many arguments
3907 3917
3908 3918 $ hg log -Gv -R a --template "{word('0')}"
3909 3919 hg: parse error: word expects two or three arguments, got 1
3910 3920 [255]
3911 3921
3912 3922 $ hg log -Gv -R a --template "{word('0', desc, 'o', 'h', 'b', 'o', 'y')}"
3913 3923 hg: parse error: word expects two or three arguments, got 7
3914 3924 [255]
3915 3925
3916 3926 Test word for integer literal
3917 3927
3918 3928 $ hg log -R a --template "{word(2, desc)}\n" -r0
3919 3929 line
3920 3930
3921 3931 Test word for invalid numbers
3922 3932
3923 3933 $ hg log -Gv -R a --template "{word('a', desc)}"
3924 3934 hg: parse error: word expects an integer index
3925 3935 [255]
3926 3936
3927 3937 Test word for out of range
3928 3938
3929 3939 $ hg log -R a --template "{word(10000, desc)}"
3930 3940 $ hg log -R a --template "{word(-10000, desc)}"
3931 3941
3932 3942 Test indent and not adding to empty lines
3933 3943
3934 3944 $ hg log -T "-----\n{indent(desc, '>> ', ' > ')}\n" -r 0:1 -R a
3935 3945 -----
3936 3946 > line 1
3937 3947 >> line 2
3938 3948 -----
3939 3949 > other 1
3940 3950 >> other 2
3941 3951
3942 3952 >> other 3
3943 3953
3944 3954 Test with non-strings like dates
3945 3955
3946 3956 $ hg log -T "{indent(date, ' ')}\n" -r 2:3 -R a
3947 3957 1200000.00
3948 3958 1300000.00
3949 3959
3950 3960 Test broken string escapes:
3951 3961
3952 3962 $ hg log -T "bogus\\" -R a
3953 3963 hg: parse error: trailing \ in string
3954 3964 [255]
3955 3965 $ hg log -T "\\xy" -R a
3956 3966 hg: parse error: invalid \x escape
3957 3967 [255]
3958 3968
3959 3969 json filter should escape HTML tags so that the output can be embedded in hgweb:
3960 3970
3961 3971 $ hg log -T "{'<foo@example.org>'|json}\n" -R a -l1
3962 3972 "\u003cfoo@example.org\u003e"
3963 3973
3964 3974 Templater supports aliases of symbol and func() styles:
3965 3975
3966 3976 $ hg clone -q a aliases
3967 3977 $ cd aliases
3968 3978 $ cat <<EOF >> .hg/hgrc
3969 3979 > [templatealias]
3970 3980 > r = rev
3971 3981 > rn = "{r}:{node|short}"
3972 3982 > status(c, files) = files % "{c} {file}\n"
3973 3983 > utcdate(d) = localdate(d, "UTC")
3974 3984 > EOF
3975 3985
3976 3986 $ hg debugtemplate -vr0 '{rn} {utcdate(date)|isodate}\n'
3977 3987 (template
3978 3988 ('symbol', 'rn')
3979 3989 ('string', ' ')
3980 3990 (|
3981 3991 (func
3982 3992 ('symbol', 'utcdate')
3983 3993 ('symbol', 'date'))
3984 3994 ('symbol', 'isodate'))
3985 3995 ('string', '\n'))
3986 3996 * expanded:
3987 3997 (template
3988 3998 (template
3989 3999 ('symbol', 'rev')
3990 4000 ('string', ':')
3991 4001 (|
3992 4002 ('symbol', 'node')
3993 4003 ('symbol', 'short')))
3994 4004 ('string', ' ')
3995 4005 (|
3996 4006 (func
3997 4007 ('symbol', 'localdate')
3998 4008 (list
3999 4009 ('symbol', 'date')
4000 4010 ('string', 'UTC')))
4001 4011 ('symbol', 'isodate'))
4002 4012 ('string', '\n'))
4003 4013 0:1e4e1b8f71e0 1970-01-12 13:46 +0000
4004 4014
4005 4015 $ hg debugtemplate -vr0 '{status("A", file_adds)}'
4006 4016 (template
4007 4017 (func
4008 4018 ('symbol', 'status')
4009 4019 (list
4010 4020 ('string', 'A')
4011 4021 ('symbol', 'file_adds'))))
4012 4022 * expanded:
4013 4023 (template
4014 4024 (%
4015 4025 ('symbol', 'file_adds')
4016 4026 (template
4017 4027 ('string', 'A')
4018 4028 ('string', ' ')
4019 4029 ('symbol', 'file')
4020 4030 ('string', '\n'))))
4021 4031 A a
4022 4032
4023 4033 A unary function alias can be called as a filter:
4024 4034
4025 4035 $ hg debugtemplate -vr0 '{date|utcdate|isodate}\n'
4026 4036 (template
4027 4037 (|
4028 4038 (|
4029 4039 ('symbol', 'date')
4030 4040 ('symbol', 'utcdate'))
4031 4041 ('symbol', 'isodate'))
4032 4042 ('string', '\n'))
4033 4043 * expanded:
4034 4044 (template
4035 4045 (|
4036 4046 (func
4037 4047 ('symbol', 'localdate')
4038 4048 (list
4039 4049 ('symbol', 'date')
4040 4050 ('string', 'UTC')))
4041 4051 ('symbol', 'isodate'))
4042 4052 ('string', '\n'))
4043 4053 1970-01-12 13:46 +0000
4044 4054
4045 4055 Aliases should be applied only to command arguments and templates in hgrc.
4046 4056 Otherwise, our stock styles and web templates could be corrupted:
4047 4057
4048 4058 $ hg log -r0 -T '{rn} {utcdate(date)|isodate}\n'
4049 4059 0:1e4e1b8f71e0 1970-01-12 13:46 +0000
4050 4060
4051 4061 $ hg log -r0 --config ui.logtemplate='"{rn} {utcdate(date)|isodate}\n"'
4052 4062 0:1e4e1b8f71e0 1970-01-12 13:46 +0000
4053 4063
4054 4064 $ cat <<EOF > tmpl
4055 4065 > changeset = 'nothing expanded:{rn}\n'
4056 4066 > EOF
4057 4067 $ hg log -r0 --style ./tmpl
4058 4068 nothing expanded:
4059 4069
4060 4070 Aliases in formatter:
4061 4071
4062 4072 $ hg branches -T '{pad(branch, 7)} {rn}\n'
4063 4073 default 6:d41e714fe50d
4064 4074 foo 4:bbe44766e73d
4065 4075
4066 4076 Aliases should honor HGPLAIN:
4067 4077
4068 4078 $ HGPLAIN= hg log -r0 -T 'nothing expanded:{rn}\n'
4069 4079 nothing expanded:
4070 4080 $ HGPLAINEXCEPT=templatealias hg log -r0 -T '{rn}\n'
4071 4081 0:1e4e1b8f71e0
4072 4082
4073 4083 Unparsable alias:
4074 4084
4075 4085 $ hg debugtemplate --config templatealias.bad='x(' -v '{bad}'
4076 4086 (template
4077 4087 ('symbol', 'bad'))
4078 4088 abort: bad definition of template alias "bad": at 2: not a prefix: end
4079 4089 [255]
4080 4090 $ hg log --config templatealias.bad='x(' -T '{bad}'
4081 4091 abort: bad definition of template alias "bad": at 2: not a prefix: end
4082 4092 [255]
4083 4093
4084 4094 $ cd ..
4085 4095
4086 4096 Set up repository for non-ascii encoding tests:
4087 4097
4088 4098 $ hg init nonascii
4089 4099 $ cd nonascii
4090 4100 $ python <<EOF
4091 4101 > open('latin1', 'w').write('\xe9')
4092 4102 > open('utf-8', 'w').write('\xc3\xa9')
4093 4103 > EOF
4094 4104 $ HGENCODING=utf-8 hg branch -q `cat utf-8`
4095 4105 $ HGENCODING=utf-8 hg ci -qAm "non-ascii branch: `cat utf-8`" utf-8
4096 4106
4097 4107 json filter should try round-trip conversion to utf-8:
4098 4108
4099 4109 $ HGENCODING=ascii hg log -T "{branch|json}\n" -r0
4100 4110 "\u00e9"
4101 4111 $ HGENCODING=ascii hg log -T "{desc|json}\n" -r0
4102 4112 "non-ascii branch: \u00e9"
4103 4113
4104 4114 json filter takes input as utf-8b:
4105 4115
4106 4116 $ HGENCODING=ascii hg log -T "{'`cat utf-8`'|json}\n" -l1
4107 4117 "\u00e9"
4108 4118 $ HGENCODING=ascii hg log -T "{'`cat latin1`'|json}\n" -l1
4109 4119 "\udce9"
4110 4120
4111 4121 utf8 filter:
4112 4122
4113 4123 $ HGENCODING=ascii hg log -T "round-trip: {branch|utf8|hex}\n" -r0
4114 4124 round-trip: c3a9
4115 4125 $ HGENCODING=latin1 hg log -T "decoded: {'`cat latin1`'|utf8|hex}\n" -l1
4116 4126 decoded: c3a9
4117 4127 $ HGENCODING=ascii hg log -T "replaced: {'`cat latin1`'|utf8|hex}\n" -l1
4118 4128 abort: decoding near * (glob)
4119 4129 [255]
4120 4130 $ hg log -T "invalid type: {rev|utf8}\n" -r0
4121 4131 abort: template filter 'utf8' is not compatible with keyword 'rev'
4122 4132 [255]
4123 4133
4124 4134 pad width:
4125 4135
4126 4136 $ HGENCODING=utf-8 hg debugtemplate "{pad('`cat utf-8`', 2, '-')}\n"
4127 4137 \xc3\xa9- (esc)
4128 4138
4129 4139 $ cd ..
4130 4140
4131 4141 Test that template function in extension is registered as expected
4132 4142
4133 4143 $ cd a
4134 4144
4135 4145 $ cat <<EOF > $TESTTMP/customfunc.py
4136 4146 > from mercurial import registrar
4137 4147 >
4138 4148 > templatefunc = registrar.templatefunc()
4139 4149 >
4140 4150 > @templatefunc('custom()')
4141 4151 > def custom(context, mapping, args):
4142 4152 > return 'custom'
4143 4153 > EOF
4144 4154 $ cat <<EOF > .hg/hgrc
4145 4155 > [extensions]
4146 4156 > customfunc = $TESTTMP/customfunc.py
4147 4157 > EOF
4148 4158
4149 4159 $ hg log -r . -T "{custom()}\n" --config customfunc.enabled=true
4150 4160 custom
4151 4161
4152 4162 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now