##// END OF EJS Templates
log: add a "graphwidth" template variable...
Danny Hooper -
r33860:6f6c8788 default
parent child Browse files
Show More
@@ -1,3862 +1,3866 b''
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 11 import itertools
12 12 import os
13 13 import re
14 14 import tempfile
15 15
16 16 from .i18n import _
17 17 from .node import (
18 18 hex,
19 19 nullid,
20 20 nullrev,
21 21 short,
22 22 )
23 23
24 24 from . import (
25 25 bookmarks,
26 26 changelog,
27 27 copies,
28 28 crecord as crecordmod,
29 29 dirstateguard,
30 30 encoding,
31 31 error,
32 32 formatter,
33 33 graphmod,
34 34 match as matchmod,
35 35 obsolete,
36 36 patch,
37 37 pathutil,
38 38 phases,
39 39 pycompat,
40 40 registrar,
41 41 revlog,
42 42 revset,
43 43 scmutil,
44 44 smartset,
45 45 templatekw,
46 46 templater,
47 47 util,
48 48 vfs as vfsmod,
49 49 )
50 50 stringio = util.stringio
51 51
52 52 # templates of common command options
53 53
54 54 dryrunopts = [
55 55 ('n', 'dry-run', None,
56 56 _('do not perform actions, just print output')),
57 57 ]
58 58
59 59 remoteopts = [
60 60 ('e', 'ssh', '',
61 61 _('specify ssh command to use'), _('CMD')),
62 62 ('', 'remotecmd', '',
63 63 _('specify hg command to run on the remote side'), _('CMD')),
64 64 ('', 'insecure', None,
65 65 _('do not verify server certificate (ignoring web.cacerts config)')),
66 66 ]
67 67
68 68 walkopts = [
69 69 ('I', 'include', [],
70 70 _('include names matching the given patterns'), _('PATTERN')),
71 71 ('X', 'exclude', [],
72 72 _('exclude names matching the given patterns'), _('PATTERN')),
73 73 ]
74 74
75 75 commitopts = [
76 76 ('m', 'message', '',
77 77 _('use text as commit message'), _('TEXT')),
78 78 ('l', 'logfile', '',
79 79 _('read commit message from file'), _('FILE')),
80 80 ]
81 81
82 82 commitopts2 = [
83 83 ('d', 'date', '',
84 84 _('record the specified date as commit date'), _('DATE')),
85 85 ('u', 'user', '',
86 86 _('record the specified user as committer'), _('USER')),
87 87 ]
88 88
89 89 # hidden for now
90 90 formatteropts = [
91 91 ('T', 'template', '',
92 92 _('display with template (EXPERIMENTAL)'), _('TEMPLATE')),
93 93 ]
94 94
95 95 templateopts = [
96 96 ('', 'style', '',
97 97 _('display using template map file (DEPRECATED)'), _('STYLE')),
98 98 ('T', 'template', '',
99 99 _('display with template'), _('TEMPLATE')),
100 100 ]
101 101
102 102 logopts = [
103 103 ('p', 'patch', None, _('show patch')),
104 104 ('g', 'git', None, _('use git extended diff format')),
105 105 ('l', 'limit', '',
106 106 _('limit number of changes displayed'), _('NUM')),
107 107 ('M', 'no-merges', None, _('do not show merges')),
108 108 ('', 'stat', None, _('output diffstat-style summary of changes')),
109 109 ('G', 'graph', None, _("show the revision DAG")),
110 110 ] + templateopts
111 111
112 112 diffopts = [
113 113 ('a', 'text', None, _('treat all files as text')),
114 114 ('g', 'git', None, _('use git extended diff format')),
115 115 ('', 'binary', None, _('generate binary diffs in git mode (default)')),
116 116 ('', 'nodates', None, _('omit dates from diff headers'))
117 117 ]
118 118
119 119 diffwsopts = [
120 120 ('w', 'ignore-all-space', None,
121 121 _('ignore white space when comparing lines')),
122 122 ('b', 'ignore-space-change', None,
123 123 _('ignore changes in the amount of white space')),
124 124 ('B', 'ignore-blank-lines', None,
125 125 _('ignore changes whose lines are all blank')),
126 126 ]
127 127
128 128 diffopts2 = [
129 129 ('', 'noprefix', None, _('omit a/ and b/ prefixes from filenames')),
130 130 ('p', 'show-function', None, _('show which function each change is in')),
131 131 ('', 'reverse', None, _('produce a diff that undoes the changes')),
132 132 ] + diffwsopts + [
133 133 ('U', 'unified', '',
134 134 _('number of lines of context to show'), _('NUM')),
135 135 ('', 'stat', None, _('output diffstat-style summary of changes')),
136 136 ('', 'root', '', _('produce diffs relative to subdirectory'), _('DIR')),
137 137 ]
138 138
139 139 mergetoolopts = [
140 140 ('t', 'tool', '', _('specify merge tool')),
141 141 ]
142 142
143 143 similarityopts = [
144 144 ('s', 'similarity', '',
145 145 _('guess renamed files by similarity (0<=s<=100)'), _('SIMILARITY'))
146 146 ]
147 147
148 148 subrepoopts = [
149 149 ('S', 'subrepos', None,
150 150 _('recurse into subrepositories'))
151 151 ]
152 152
153 153 debugrevlogopts = [
154 154 ('c', 'changelog', False, _('open changelog')),
155 155 ('m', 'manifest', False, _('open manifest')),
156 156 ('', 'dir', '', _('open directory manifest')),
157 157 ]
158 158
159 159 # special string such that everything below this line will be ingored in the
160 160 # editor text
161 161 _linebelow = "^HG: ------------------------ >8 ------------------------$"
162 162
163 163 def ishunk(x):
164 164 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
165 165 return isinstance(x, hunkclasses)
166 166
167 167 def newandmodified(chunks, originalchunks):
168 168 newlyaddedandmodifiedfiles = set()
169 169 for chunk in chunks:
170 170 if ishunk(chunk) and chunk.header.isnewfile() and chunk not in \
171 171 originalchunks:
172 172 newlyaddedandmodifiedfiles.add(chunk.header.filename())
173 173 return newlyaddedandmodifiedfiles
174 174
175 175 def parsealiases(cmd):
176 176 return cmd.lstrip("^").split("|")
177 177
178 178 def setupwrapcolorwrite(ui):
179 179 # wrap ui.write so diff output can be labeled/colorized
180 180 def wrapwrite(orig, *args, **kw):
181 181 label = kw.pop('label', '')
182 182 for chunk, l in patch.difflabel(lambda: args):
183 183 orig(chunk, label=label + l)
184 184
185 185 oldwrite = ui.write
186 186 def wrap(*args, **kwargs):
187 187 return wrapwrite(oldwrite, *args, **kwargs)
188 188 setattr(ui, 'write', wrap)
189 189 return oldwrite
190 190
191 191 def filterchunks(ui, originalhunks, usecurses, testfile, operation=None):
192 192 if usecurses:
193 193 if testfile:
194 194 recordfn = crecordmod.testdecorator(testfile,
195 195 crecordmod.testchunkselector)
196 196 else:
197 197 recordfn = crecordmod.chunkselector
198 198
199 199 return crecordmod.filterpatch(ui, originalhunks, recordfn, operation)
200 200
201 201 else:
202 202 return patch.filterpatch(ui, originalhunks, operation)
203 203
204 204 def recordfilter(ui, originalhunks, operation=None):
205 205 """ Prompts the user to filter the originalhunks and return a list of
206 206 selected hunks.
207 207 *operation* is used for to build ui messages to indicate the user what
208 208 kind of filtering they are doing: reverting, committing, shelving, etc.
209 209 (see patch.filterpatch).
210 210 """
211 211 usecurses = crecordmod.checkcurses(ui)
212 212 testfile = ui.config('experimental', 'crecordtest')
213 213 oldwrite = setupwrapcolorwrite(ui)
214 214 try:
215 215 newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
216 216 testfile, operation)
217 217 finally:
218 218 ui.write = oldwrite
219 219 return newchunks, newopts
220 220
221 221 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
222 222 filterfn, *pats, **opts):
223 223 from . import merge as mergemod
224 224 opts = pycompat.byteskwargs(opts)
225 225 if not ui.interactive():
226 226 if cmdsuggest:
227 227 msg = _('running non-interactively, use %s instead') % cmdsuggest
228 228 else:
229 229 msg = _('running non-interactively')
230 230 raise error.Abort(msg)
231 231
232 232 # make sure username is set before going interactive
233 233 if not opts.get('user'):
234 234 ui.username() # raise exception, username not provided
235 235
236 236 def recordfunc(ui, repo, message, match, opts):
237 237 """This is generic record driver.
238 238
239 239 Its job is to interactively filter local changes, and
240 240 accordingly prepare working directory into a state in which the
241 241 job can be delegated to a non-interactive commit command such as
242 242 'commit' or 'qrefresh'.
243 243
244 244 After the actual job is done by non-interactive command, the
245 245 working directory is restored to its original state.
246 246
247 247 In the end we'll record interesting changes, and everything else
248 248 will be left in place, so the user can continue working.
249 249 """
250 250
251 251 checkunfinished(repo, commit=True)
252 252 wctx = repo[None]
253 253 merge = len(wctx.parents()) > 1
254 254 if merge:
255 255 raise error.Abort(_('cannot partially commit a merge '
256 256 '(use "hg commit" instead)'))
257 257
258 258 def fail(f, msg):
259 259 raise error.Abort('%s: %s' % (f, msg))
260 260
261 261 force = opts.get('force')
262 262 if not force:
263 263 vdirs = []
264 264 match.explicitdir = vdirs.append
265 265 match.bad = fail
266 266
267 267 status = repo.status(match=match)
268 268 if not force:
269 269 repo.checkcommitpatterns(wctx, vdirs, match, status, fail)
270 270 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
271 271 diffopts.nodates = True
272 272 diffopts.git = True
273 273 diffopts.showfunc = True
274 274 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
275 275 originalchunks = patch.parsepatch(originaldiff)
276 276
277 277 # 1. filter patch, since we are intending to apply subset of it
278 278 try:
279 279 chunks, newopts = filterfn(ui, originalchunks)
280 280 except patch.PatchError as err:
281 281 raise error.Abort(_('error parsing patch: %s') % err)
282 282 opts.update(newopts)
283 283
284 284 # We need to keep a backup of files that have been newly added and
285 285 # modified during the recording process because there is a previous
286 286 # version without the edit in the workdir
287 287 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
288 288 contenders = set()
289 289 for h in chunks:
290 290 try:
291 291 contenders.update(set(h.files()))
292 292 except AttributeError:
293 293 pass
294 294
295 295 changed = status.modified + status.added + status.removed
296 296 newfiles = [f for f in changed if f in contenders]
297 297 if not newfiles:
298 298 ui.status(_('no changes to record\n'))
299 299 return 0
300 300
301 301 modified = set(status.modified)
302 302
303 303 # 2. backup changed files, so we can restore them in the end
304 304
305 305 if backupall:
306 306 tobackup = changed
307 307 else:
308 308 tobackup = [f for f in newfiles if f in modified or f in \
309 309 newlyaddedandmodifiedfiles]
310 310 backups = {}
311 311 if tobackup:
312 312 backupdir = repo.vfs.join('record-backups')
313 313 try:
314 314 os.mkdir(backupdir)
315 315 except OSError as err:
316 316 if err.errno != errno.EEXIST:
317 317 raise
318 318 try:
319 319 # backup continues
320 320 for f in tobackup:
321 321 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
322 322 dir=backupdir)
323 323 os.close(fd)
324 324 ui.debug('backup %r as %r\n' % (f, tmpname))
325 325 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
326 326 backups[f] = tmpname
327 327
328 328 fp = stringio()
329 329 for c in chunks:
330 330 fname = c.filename()
331 331 if fname in backups:
332 332 c.write(fp)
333 333 dopatch = fp.tell()
334 334 fp.seek(0)
335 335
336 336 # 2.5 optionally review / modify patch in text editor
337 337 if opts.get('review', False):
338 338 patchtext = (crecordmod.diffhelptext
339 339 + crecordmod.patchhelptext
340 340 + fp.read())
341 341 reviewedpatch = ui.edit(patchtext, "",
342 342 extra={"suffix": ".diff"},
343 343 repopath=repo.path)
344 344 fp.truncate(0)
345 345 fp.write(reviewedpatch)
346 346 fp.seek(0)
347 347
348 348 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
349 349 # 3a. apply filtered patch to clean repo (clean)
350 350 if backups:
351 351 # Equivalent to hg.revert
352 352 m = scmutil.matchfiles(repo, backups.keys())
353 353 mergemod.update(repo, repo.dirstate.p1(),
354 354 False, True, matcher=m)
355 355
356 356 # 3b. (apply)
357 357 if dopatch:
358 358 try:
359 359 ui.debug('applying patch\n')
360 360 ui.debug(fp.getvalue())
361 361 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
362 362 except patch.PatchError as err:
363 363 raise error.Abort(str(err))
364 364 del fp
365 365
366 366 # 4. We prepared working directory according to filtered
367 367 # patch. Now is the time to delegate the job to
368 368 # commit/qrefresh or the like!
369 369
370 370 # Make all of the pathnames absolute.
371 371 newfiles = [repo.wjoin(nf) for nf in newfiles]
372 372 return commitfunc(ui, repo, *newfiles, **opts)
373 373 finally:
374 374 # 5. finally restore backed-up files
375 375 try:
376 376 dirstate = repo.dirstate
377 377 for realname, tmpname in backups.iteritems():
378 378 ui.debug('restoring %r to %r\n' % (tmpname, realname))
379 379
380 380 if dirstate[realname] == 'n':
381 381 # without normallookup, restoring timestamp
382 382 # may cause partially committed files
383 383 # to be treated as unmodified
384 384 dirstate.normallookup(realname)
385 385
386 386 # copystat=True here and above are a hack to trick any
387 387 # editors that have f open that we haven't modified them.
388 388 #
389 389 # Also note that this racy as an editor could notice the
390 390 # file's mtime before we've finished writing it.
391 391 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
392 392 os.unlink(tmpname)
393 393 if tobackup:
394 394 os.rmdir(backupdir)
395 395 except OSError:
396 396 pass
397 397
398 398 def recordinwlock(ui, repo, message, match, opts):
399 399 with repo.wlock():
400 400 return recordfunc(ui, repo, message, match, opts)
401 401
402 402 return commit(ui, repo, recordinwlock, pats, opts)
403 403
404 404 def tersestatus(root, statlist, status, ignorefn, ignore):
405 405 """
406 406 Returns a list of statuses with directory collapsed if all the files in the
407 407 directory has the same status.
408 408 """
409 409
410 410 def numfiles(dirname):
411 411 """
412 412 Calculates the number of tracked files in a given directory which also
413 413 includes files which were removed or deleted. Considers ignored files
414 414 if ignore argument is True or 'i' is present in status argument.
415 415 """
416 416 if lencache.get(dirname):
417 417 return lencache[dirname]
418 418 if 'i' in status or ignore:
419 419 def match(localpath):
420 420 absolutepath = os.path.join(root, localpath)
421 421 if os.path.isdir(absolutepath) and isemptydir(absolutepath):
422 422 return True
423 423 return False
424 424 else:
425 425 def match(localpath):
426 426 # there can be directory whose all the files are ignored and
427 427 # hence the drectory should also be ignored while counting
428 428 # number of files or subdirs in it's parent directory. This
429 429 # checks the same.
430 430 # XXX: We need a better logic here.
431 431 if os.path.isdir(os.path.join(root, localpath)):
432 432 return isignoreddir(localpath)
433 433 else:
434 434 # XXX: there can be files which have the ignored pattern but
435 435 # are not ignored. That leads to bug in counting number of
436 436 # tracked files in the directory.
437 437 return ignorefn(localpath)
438 438 lendir = 0
439 439 abspath = os.path.join(root, dirname)
440 440 # There might be cases when a directory does not exists as the whole
441 441 # directory can be removed and/or deleted.
442 442 try:
443 443 for f in os.listdir(abspath):
444 444 localpath = os.path.join(dirname, f)
445 445 if not match(localpath):
446 446 lendir += 1
447 447 except OSError:
448 448 pass
449 449 lendir += len(absentdir.get(dirname, []))
450 450 lencache[dirname] = lendir
451 451 return lendir
452 452
453 453 def isemptydir(abspath):
454 454 """
455 455 Check whether a directory is empty or not, i.e. there is no files in the
456 456 directory and all its subdirectories.
457 457 """
458 458 for f in os.listdir(abspath):
459 459 fullpath = os.path.join(abspath, f)
460 460 if os.path.isdir(fullpath):
461 461 # recursion here
462 462 ret = isemptydir(fullpath)
463 463 if not ret:
464 464 return False
465 465 else:
466 466 return False
467 467 return True
468 468
469 469 def isignoreddir(localpath):
470 470 """Return True if `localpath` directory is ignored or contains only
471 471 ignored files and should hence be considered ignored.
472 472 """
473 473 dirpath = os.path.join(root, localpath)
474 474 if ignorefn(dirpath):
475 475 return True
476 476 for f in os.listdir(dirpath):
477 477 filepath = os.path.join(dirpath, f)
478 478 if os.path.isdir(filepath):
479 479 # recursion here
480 480 ret = isignoreddir(os.path.join(localpath, f))
481 481 if not ret:
482 482 return False
483 483 else:
484 484 if not ignorefn(os.path.join(localpath, f)):
485 485 return False
486 486 return True
487 487
488 488 def absentones(removedfiles, missingfiles):
489 489 """
490 490 Returns a dictionary of directories with files in it which are either
491 491 removed or missing (deleted) in them.
492 492 """
493 493 absentdir = {}
494 494 absentfiles = removedfiles + missingfiles
495 495 while absentfiles:
496 496 f = absentfiles.pop()
497 497 par = os.path.dirname(f)
498 498 if par == '':
499 499 continue
500 500 # we need to store files rather than number of files as some files
501 501 # or subdirectories in a directory can be counted twice. This is
502 502 # also we have used sets here.
503 503 try:
504 504 absentdir[par].add(f)
505 505 except KeyError:
506 506 absentdir[par] = set([f])
507 507 absentfiles.append(par)
508 508 return absentdir
509 509
510 510 indexes = {'m': 0, 'a': 1, 'r': 2, 'd': 3, 'u': 4, 'i': 5, 'c': 6}
511 511 # get a dictonary of directories and files which are missing as os.listdir()
512 512 # won't be able to list them.
513 513 absentdir = absentones(statlist[2], statlist[3])
514 514 finalrs = [[]] * len(indexes)
515 515 didsomethingchanged = False
516 516 # dictionary to store number of files and subdir in a directory so that we
517 517 # don't compute that again.
518 518 lencache = {}
519 519
520 520 for st in pycompat.bytestr(status):
521 521
522 522 try:
523 523 ind = indexes[st]
524 524 except KeyError:
525 525 # TODO: Need a better error message here
526 526 raise error.Abort("'%s' not recognized" % st)
527 527
528 528 sfiles = statlist[ind]
529 529 if not sfiles:
530 530 continue
531 531 pardict = {}
532 532 for a in sfiles:
533 533 par = os.path.dirname(a)
534 534 pardict.setdefault(par, []).append(a)
535 535
536 536 rs = []
537 537 newls = []
538 538 for par, files in pardict.iteritems():
539 539 lenpar = numfiles(par)
540 540 if lenpar == len(files):
541 541 newls.append(par)
542 542
543 543 if not newls:
544 544 continue
545 545
546 546 while newls:
547 547 newel = newls.pop()
548 548 if newel == '':
549 549 continue
550 550 parn = os.path.dirname(newel)
551 551 pardict[newel] = []
552 552 # Adding pycompat.ossep as newel is a directory.
553 553 pardict.setdefault(parn, []).append(newel + pycompat.ossep)
554 554 lenpar = numfiles(parn)
555 555 if lenpar == len(pardict[parn]):
556 556 newls.append(parn)
557 557
558 558 # dict.values() for Py3 compatibility
559 559 for files in pardict.values():
560 560 rs.extend(files)
561 561
562 562 rs.sort()
563 563 finalrs[ind] = rs
564 564 didsomethingchanged = True
565 565
566 566 # If nothing is changed, make sure the order of files is preserved.
567 567 if not didsomethingchanged:
568 568 return statlist
569 569
570 570 for x in xrange(len(indexes)):
571 571 if not finalrs[x]:
572 572 finalrs[x] = statlist[x]
573 573
574 574 return finalrs
575 575
576 576 def _commentlines(raw):
577 577 '''Surround lineswith a comment char and a new line'''
578 578 lines = raw.splitlines()
579 579 commentedlines = ['# %s' % line for line in lines]
580 580 return '\n'.join(commentedlines) + '\n'
581 581
582 582 def _conflictsmsg(repo):
583 583 # avoid merge cycle
584 584 from . import merge as mergemod
585 585 mergestate = mergemod.mergestate.read(repo)
586 586 if not mergestate.active():
587 587 return
588 588
589 589 m = scmutil.match(repo[None])
590 590 unresolvedlist = [f for f in mergestate if m(f) and mergestate[f] == 'u']
591 591 if unresolvedlist:
592 592 mergeliststr = '\n'.join(
593 593 [' %s' % os.path.relpath(
594 594 os.path.join(repo.root, path),
595 595 pycompat.getcwd()) for path in unresolvedlist])
596 596 msg = _('''Unresolved merge conflicts:
597 597
598 598 %s
599 599
600 600 To mark files as resolved: hg resolve --mark FILE''') % mergeliststr
601 601 else:
602 602 msg = _('No unresolved merge conflicts.')
603 603
604 604 return _commentlines(msg)
605 605
606 606 def _helpmessage(continuecmd, abortcmd):
607 607 msg = _('To continue: %s\n'
608 608 'To abort: %s') % (continuecmd, abortcmd)
609 609 return _commentlines(msg)
610 610
611 611 def _rebasemsg():
612 612 return _helpmessage('hg rebase --continue', 'hg rebase --abort')
613 613
614 614 def _histeditmsg():
615 615 return _helpmessage('hg histedit --continue', 'hg histedit --abort')
616 616
617 617 def _unshelvemsg():
618 618 return _helpmessage('hg unshelve --continue', 'hg unshelve --abort')
619 619
620 620 def _updatecleanmsg(dest=None):
621 621 warning = _('warning: this will discard uncommitted changes')
622 622 return 'hg update --clean %s (%s)' % (dest or '.', warning)
623 623
624 624 def _graftmsg():
625 625 # tweakdefaults requires `update` to have a rev hence the `.`
626 626 return _helpmessage('hg graft --continue', _updatecleanmsg())
627 627
628 628 def _mergemsg():
629 629 # tweakdefaults requires `update` to have a rev hence the `.`
630 630 return _helpmessage('hg commit', _updatecleanmsg())
631 631
632 632 def _bisectmsg():
633 633 msg = _('To mark the changeset good: hg bisect --good\n'
634 634 'To mark the changeset bad: hg bisect --bad\n'
635 635 'To abort: hg bisect --reset\n')
636 636 return _commentlines(msg)
637 637
638 638 def fileexistspredicate(filename):
639 639 return lambda repo: repo.vfs.exists(filename)
640 640
641 641 def _mergepredicate(repo):
642 642 return len(repo[None].parents()) > 1
643 643
644 644 STATES = (
645 645 # (state, predicate to detect states, helpful message function)
646 646 ('histedit', fileexistspredicate('histedit-state'), _histeditmsg),
647 647 ('bisect', fileexistspredicate('bisect.state'), _bisectmsg),
648 648 ('graft', fileexistspredicate('graftstate'), _graftmsg),
649 649 ('unshelve', fileexistspredicate('unshelverebasestate'), _unshelvemsg),
650 650 ('rebase', fileexistspredicate('rebasestate'), _rebasemsg),
651 651 # The merge state is part of a list that will be iterated over.
652 652 # They need to be last because some of the other unfinished states may also
653 653 # be in a merge or update state (eg. rebase, histedit, graft, etc).
654 654 # We want those to have priority.
655 655 ('merge', _mergepredicate, _mergemsg),
656 656 )
657 657
658 658 def _getrepostate(repo):
659 659 # experimental config: commands.status.skipstates
660 660 skip = set(repo.ui.configlist('commands', 'status.skipstates'))
661 661 for state, statedetectionpredicate, msgfn in STATES:
662 662 if state in skip:
663 663 continue
664 664 if statedetectionpredicate(repo):
665 665 return (state, statedetectionpredicate, msgfn)
666 666
667 667 def morestatus(repo, fm):
668 668 statetuple = _getrepostate(repo)
669 669 label = 'status.morestatus'
670 670 if statetuple:
671 671 fm.startitem()
672 672 state, statedetectionpredicate, helpfulmsg = statetuple
673 673 statemsg = _('The repository is in an unfinished *%s* state.') % state
674 674 fm.write('statemsg', '%s\n', _commentlines(statemsg), label=label)
675 675 conmsg = _conflictsmsg(repo)
676 676 fm.write('conflictsmsg', '%s\n', conmsg, label=label)
677 677 if helpfulmsg:
678 678 helpmsg = helpfulmsg()
679 679 fm.write('helpmsg', '%s\n', helpmsg, label=label)
680 680
681 681 def findpossible(cmd, table, strict=False):
682 682 """
683 683 Return cmd -> (aliases, command table entry)
684 684 for each matching command.
685 685 Return debug commands (or their aliases) only if no normal command matches.
686 686 """
687 687 choice = {}
688 688 debugchoice = {}
689 689
690 690 if cmd in table:
691 691 # short-circuit exact matches, "log" alias beats "^log|history"
692 692 keys = [cmd]
693 693 else:
694 694 keys = table.keys()
695 695
696 696 allcmds = []
697 697 for e in keys:
698 698 aliases = parsealiases(e)
699 699 allcmds.extend(aliases)
700 700 found = None
701 701 if cmd in aliases:
702 702 found = cmd
703 703 elif not strict:
704 704 for a in aliases:
705 705 if a.startswith(cmd):
706 706 found = a
707 707 break
708 708 if found is not None:
709 709 if aliases[0].startswith("debug") or found.startswith("debug"):
710 710 debugchoice[found] = (aliases, table[e])
711 711 else:
712 712 choice[found] = (aliases, table[e])
713 713
714 714 if not choice and debugchoice:
715 715 choice = debugchoice
716 716
717 717 return choice, allcmds
718 718
719 719 def findcmd(cmd, table, strict=True):
720 720 """Return (aliases, command table entry) for command string."""
721 721 choice, allcmds = findpossible(cmd, table, strict)
722 722
723 723 if cmd in choice:
724 724 return choice[cmd]
725 725
726 726 if len(choice) > 1:
727 727 clist = sorted(choice)
728 728 raise error.AmbiguousCommand(cmd, clist)
729 729
730 730 if choice:
731 731 return list(choice.values())[0]
732 732
733 733 raise error.UnknownCommand(cmd, allcmds)
734 734
735 735 def findrepo(p):
736 736 while not os.path.isdir(os.path.join(p, ".hg")):
737 737 oldp, p = p, os.path.dirname(p)
738 738 if p == oldp:
739 739 return None
740 740
741 741 return p
742 742
743 743 def bailifchanged(repo, merge=True, hint=None):
744 744 """ enforce the precondition that working directory must be clean.
745 745
746 746 'merge' can be set to false if a pending uncommitted merge should be
747 747 ignored (such as when 'update --check' runs).
748 748
749 749 'hint' is the usual hint given to Abort exception.
750 750 """
751 751
752 752 if merge and repo.dirstate.p2() != nullid:
753 753 raise error.Abort(_('outstanding uncommitted merge'), hint=hint)
754 754 modified, added, removed, deleted = repo.status()[:4]
755 755 if modified or added or removed or deleted:
756 756 raise error.Abort(_('uncommitted changes'), hint=hint)
757 757 ctx = repo[None]
758 758 for s in sorted(ctx.substate):
759 759 ctx.sub(s).bailifchanged(hint=hint)
760 760
761 761 def logmessage(ui, opts):
762 762 """ get the log message according to -m and -l option """
763 763 message = opts.get('message')
764 764 logfile = opts.get('logfile')
765 765
766 766 if message and logfile:
767 767 raise error.Abort(_('options --message and --logfile are mutually '
768 768 'exclusive'))
769 769 if not message and logfile:
770 770 try:
771 771 if isstdiofilename(logfile):
772 772 message = ui.fin.read()
773 773 else:
774 774 message = '\n'.join(util.readfile(logfile).splitlines())
775 775 except IOError as inst:
776 776 raise error.Abort(_("can't read commit message '%s': %s") %
777 777 (logfile, inst.strerror))
778 778 return message
779 779
780 780 def mergeeditform(ctxorbool, baseformname):
781 781 """return appropriate editform name (referencing a committemplate)
782 782
783 783 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
784 784 merging is committed.
785 785
786 786 This returns baseformname with '.merge' appended if it is a merge,
787 787 otherwise '.normal' is appended.
788 788 """
789 789 if isinstance(ctxorbool, bool):
790 790 if ctxorbool:
791 791 return baseformname + ".merge"
792 792 elif 1 < len(ctxorbool.parents()):
793 793 return baseformname + ".merge"
794 794
795 795 return baseformname + ".normal"
796 796
797 797 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
798 798 editform='', **opts):
799 799 """get appropriate commit message editor according to '--edit' option
800 800
801 801 'finishdesc' is a function to be called with edited commit message
802 802 (= 'description' of the new changeset) just after editing, but
803 803 before checking empty-ness. It should return actual text to be
804 804 stored into history. This allows to change description before
805 805 storing.
806 806
807 807 'extramsg' is a extra message to be shown in the editor instead of
808 808 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
809 809 is automatically added.
810 810
811 811 'editform' is a dot-separated list of names, to distinguish
812 812 the purpose of commit text editing.
813 813
814 814 'getcommiteditor' returns 'commitforceeditor' regardless of
815 815 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
816 816 they are specific for usage in MQ.
817 817 """
818 818 if edit or finishdesc or extramsg:
819 819 return lambda r, c, s: commitforceeditor(r, c, s,
820 820 finishdesc=finishdesc,
821 821 extramsg=extramsg,
822 822 editform=editform)
823 823 elif editform:
824 824 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
825 825 else:
826 826 return commiteditor
827 827
828 828 def loglimit(opts):
829 829 """get the log limit according to option -l/--limit"""
830 830 limit = opts.get('limit')
831 831 if limit:
832 832 try:
833 833 limit = int(limit)
834 834 except ValueError:
835 835 raise error.Abort(_('limit must be a positive integer'))
836 836 if limit <= 0:
837 837 raise error.Abort(_('limit must be positive'))
838 838 else:
839 839 limit = None
840 840 return limit
841 841
842 842 def makefilename(repo, pat, node, desc=None,
843 843 total=None, seqno=None, revwidth=None, pathname=None):
844 844 node_expander = {
845 845 'H': lambda: hex(node),
846 846 'R': lambda: str(repo.changelog.rev(node)),
847 847 'h': lambda: short(node),
848 848 'm': lambda: re.sub('[^\w]', '_', str(desc))
849 849 }
850 850 expander = {
851 851 '%': lambda: '%',
852 852 'b': lambda: os.path.basename(repo.root),
853 853 }
854 854
855 855 try:
856 856 if node:
857 857 expander.update(node_expander)
858 858 if node:
859 859 expander['r'] = (lambda:
860 860 str(repo.changelog.rev(node)).zfill(revwidth or 0))
861 861 if total is not None:
862 862 expander['N'] = lambda: str(total)
863 863 if seqno is not None:
864 864 expander['n'] = lambda: str(seqno)
865 865 if total is not None and seqno is not None:
866 866 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
867 867 if pathname is not None:
868 868 expander['s'] = lambda: os.path.basename(pathname)
869 869 expander['d'] = lambda: os.path.dirname(pathname) or '.'
870 870 expander['p'] = lambda: pathname
871 871
872 872 newname = []
873 873 patlen = len(pat)
874 874 i = 0
875 875 while i < patlen:
876 876 c = pat[i:i + 1]
877 877 if c == '%':
878 878 i += 1
879 879 c = pat[i:i + 1]
880 880 c = expander[c]()
881 881 newname.append(c)
882 882 i += 1
883 883 return ''.join(newname)
884 884 except KeyError as inst:
885 885 raise error.Abort(_("invalid format spec '%%%s' in output filename") %
886 886 inst.args[0])
887 887
888 888 def isstdiofilename(pat):
889 889 """True if the given pat looks like a filename denoting stdin/stdout"""
890 890 return not pat or pat == '-'
891 891
892 892 class _unclosablefile(object):
893 893 def __init__(self, fp):
894 894 self._fp = fp
895 895
896 896 def close(self):
897 897 pass
898 898
899 899 def __iter__(self):
900 900 return iter(self._fp)
901 901
902 902 def __getattr__(self, attr):
903 903 return getattr(self._fp, attr)
904 904
905 905 def __enter__(self):
906 906 return self
907 907
908 908 def __exit__(self, exc_type, exc_value, exc_tb):
909 909 pass
910 910
911 911 def makefileobj(repo, pat, node=None, desc=None, total=None,
912 912 seqno=None, revwidth=None, mode='wb', modemap=None,
913 913 pathname=None):
914 914
915 915 writable = mode not in ('r', 'rb')
916 916
917 917 if isstdiofilename(pat):
918 918 if writable:
919 919 fp = repo.ui.fout
920 920 else:
921 921 fp = repo.ui.fin
922 922 return _unclosablefile(fp)
923 923 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
924 924 if modemap is not None:
925 925 mode = modemap.get(fn, mode)
926 926 if mode == 'wb':
927 927 modemap[fn] = 'ab'
928 928 return open(fn, mode)
929 929
930 930 def openrevlog(repo, cmd, file_, opts):
931 931 """opens the changelog, manifest, a filelog or a given revlog"""
932 932 cl = opts['changelog']
933 933 mf = opts['manifest']
934 934 dir = opts['dir']
935 935 msg = None
936 936 if cl and mf:
937 937 msg = _('cannot specify --changelog and --manifest at the same time')
938 938 elif cl and dir:
939 939 msg = _('cannot specify --changelog and --dir at the same time')
940 940 elif cl or mf or dir:
941 941 if file_:
942 942 msg = _('cannot specify filename with --changelog or --manifest')
943 943 elif not repo:
944 944 msg = _('cannot specify --changelog or --manifest or --dir '
945 945 'without a repository')
946 946 if msg:
947 947 raise error.Abort(msg)
948 948
949 949 r = None
950 950 if repo:
951 951 if cl:
952 952 r = repo.unfiltered().changelog
953 953 elif dir:
954 954 if 'treemanifest' not in repo.requirements:
955 955 raise error.Abort(_("--dir can only be used on repos with "
956 956 "treemanifest enabled"))
957 957 dirlog = repo.manifestlog._revlog.dirlog(dir)
958 958 if len(dirlog):
959 959 r = dirlog
960 960 elif mf:
961 961 r = repo.manifestlog._revlog
962 962 elif file_:
963 963 filelog = repo.file(file_)
964 964 if len(filelog):
965 965 r = filelog
966 966 if not r:
967 967 if not file_:
968 968 raise error.CommandError(cmd, _('invalid arguments'))
969 969 if not os.path.isfile(file_):
970 970 raise error.Abort(_("revlog '%s' not found") % file_)
971 971 r = revlog.revlog(vfsmod.vfs(pycompat.getcwd(), audit=False),
972 972 file_[:-2] + ".i")
973 973 return r
974 974
975 975 def copy(ui, repo, pats, opts, rename=False):
976 976 # called with the repo lock held
977 977 #
978 978 # hgsep => pathname that uses "/" to separate directories
979 979 # ossep => pathname that uses os.sep to separate directories
980 980 cwd = repo.getcwd()
981 981 targets = {}
982 982 after = opts.get("after")
983 983 dryrun = opts.get("dry_run")
984 984 wctx = repo[None]
985 985
986 986 def walkpat(pat):
987 987 srcs = []
988 988 if after:
989 989 badstates = '?'
990 990 else:
991 991 badstates = '?r'
992 992 m = scmutil.match(wctx, [pat], opts, globbed=True)
993 993 for abs in wctx.walk(m):
994 994 state = repo.dirstate[abs]
995 995 rel = m.rel(abs)
996 996 exact = m.exact(abs)
997 997 if state in badstates:
998 998 if exact and state == '?':
999 999 ui.warn(_('%s: not copying - file is not managed\n') % rel)
1000 1000 if exact and state == 'r':
1001 1001 ui.warn(_('%s: not copying - file has been marked for'
1002 1002 ' remove\n') % rel)
1003 1003 continue
1004 1004 # abs: hgsep
1005 1005 # rel: ossep
1006 1006 srcs.append((abs, rel, exact))
1007 1007 return srcs
1008 1008
1009 1009 # abssrc: hgsep
1010 1010 # relsrc: ossep
1011 1011 # otarget: ossep
1012 1012 def copyfile(abssrc, relsrc, otarget, exact):
1013 1013 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1014 1014 if '/' in abstarget:
1015 1015 # We cannot normalize abstarget itself, this would prevent
1016 1016 # case only renames, like a => A.
1017 1017 abspath, absname = abstarget.rsplit('/', 1)
1018 1018 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
1019 1019 reltarget = repo.pathto(abstarget, cwd)
1020 1020 target = repo.wjoin(abstarget)
1021 1021 src = repo.wjoin(abssrc)
1022 1022 state = repo.dirstate[abstarget]
1023 1023
1024 1024 scmutil.checkportable(ui, abstarget)
1025 1025
1026 1026 # check for collisions
1027 1027 prevsrc = targets.get(abstarget)
1028 1028 if prevsrc is not None:
1029 1029 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
1030 1030 (reltarget, repo.pathto(abssrc, cwd),
1031 1031 repo.pathto(prevsrc, cwd)))
1032 1032 return
1033 1033
1034 1034 # check for overwrites
1035 1035 exists = os.path.lexists(target)
1036 1036 samefile = False
1037 1037 if exists and abssrc != abstarget:
1038 1038 if (repo.dirstate.normalize(abssrc) ==
1039 1039 repo.dirstate.normalize(abstarget)):
1040 1040 if not rename:
1041 1041 ui.warn(_("%s: can't copy - same file\n") % reltarget)
1042 1042 return
1043 1043 exists = False
1044 1044 samefile = True
1045 1045
1046 1046 if not after and exists or after and state in 'mn':
1047 1047 if not opts['force']:
1048 1048 if state in 'mn':
1049 1049 msg = _('%s: not overwriting - file already committed\n')
1050 1050 if after:
1051 1051 flags = '--after --force'
1052 1052 else:
1053 1053 flags = '--force'
1054 1054 if rename:
1055 1055 hint = _('(hg rename %s to replace the file by '
1056 1056 'recording a rename)\n') % flags
1057 1057 else:
1058 1058 hint = _('(hg copy %s to replace the file by '
1059 1059 'recording a copy)\n') % flags
1060 1060 else:
1061 1061 msg = _('%s: not overwriting - file exists\n')
1062 1062 if rename:
1063 1063 hint = _('(hg rename --after to record the rename)\n')
1064 1064 else:
1065 1065 hint = _('(hg copy --after to record the copy)\n')
1066 1066 ui.warn(msg % reltarget)
1067 1067 ui.warn(hint)
1068 1068 return
1069 1069
1070 1070 if after:
1071 1071 if not exists:
1072 1072 if rename:
1073 1073 ui.warn(_('%s: not recording move - %s does not exist\n') %
1074 1074 (relsrc, reltarget))
1075 1075 else:
1076 1076 ui.warn(_('%s: not recording copy - %s does not exist\n') %
1077 1077 (relsrc, reltarget))
1078 1078 return
1079 1079 elif not dryrun:
1080 1080 try:
1081 1081 if exists:
1082 1082 os.unlink(target)
1083 1083 targetdir = os.path.dirname(target) or '.'
1084 1084 if not os.path.isdir(targetdir):
1085 1085 os.makedirs(targetdir)
1086 1086 if samefile:
1087 1087 tmp = target + "~hgrename"
1088 1088 os.rename(src, tmp)
1089 1089 os.rename(tmp, target)
1090 1090 else:
1091 1091 util.copyfile(src, target)
1092 1092 srcexists = True
1093 1093 except IOError as inst:
1094 1094 if inst.errno == errno.ENOENT:
1095 1095 ui.warn(_('%s: deleted in working directory\n') % relsrc)
1096 1096 srcexists = False
1097 1097 else:
1098 1098 ui.warn(_('%s: cannot copy - %s\n') %
1099 1099 (relsrc, inst.strerror))
1100 1100 return True # report a failure
1101 1101
1102 1102 if ui.verbose or not exact:
1103 1103 if rename:
1104 1104 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
1105 1105 else:
1106 1106 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
1107 1107
1108 1108 targets[abstarget] = abssrc
1109 1109
1110 1110 # fix up dirstate
1111 1111 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
1112 1112 dryrun=dryrun, cwd=cwd)
1113 1113 if rename and not dryrun:
1114 1114 if not after and srcexists and not samefile:
1115 1115 repo.wvfs.unlinkpath(abssrc)
1116 1116 wctx.forget([abssrc])
1117 1117
1118 1118 # pat: ossep
1119 1119 # dest ossep
1120 1120 # srcs: list of (hgsep, hgsep, ossep, bool)
1121 1121 # return: function that takes hgsep and returns ossep
1122 1122 def targetpathfn(pat, dest, srcs):
1123 1123 if os.path.isdir(pat):
1124 1124 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1125 1125 abspfx = util.localpath(abspfx)
1126 1126 if destdirexists:
1127 1127 striplen = len(os.path.split(abspfx)[0])
1128 1128 else:
1129 1129 striplen = len(abspfx)
1130 1130 if striplen:
1131 1131 striplen += len(pycompat.ossep)
1132 1132 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1133 1133 elif destdirexists:
1134 1134 res = lambda p: os.path.join(dest,
1135 1135 os.path.basename(util.localpath(p)))
1136 1136 else:
1137 1137 res = lambda p: dest
1138 1138 return res
1139 1139
1140 1140 # pat: ossep
1141 1141 # dest ossep
1142 1142 # srcs: list of (hgsep, hgsep, ossep, bool)
1143 1143 # return: function that takes hgsep and returns ossep
1144 1144 def targetpathafterfn(pat, dest, srcs):
1145 1145 if matchmod.patkind(pat):
1146 1146 # a mercurial pattern
1147 1147 res = lambda p: os.path.join(dest,
1148 1148 os.path.basename(util.localpath(p)))
1149 1149 else:
1150 1150 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1151 1151 if len(abspfx) < len(srcs[0][0]):
1152 1152 # A directory. Either the target path contains the last
1153 1153 # component of the source path or it does not.
1154 1154 def evalpath(striplen):
1155 1155 score = 0
1156 1156 for s in srcs:
1157 1157 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1158 1158 if os.path.lexists(t):
1159 1159 score += 1
1160 1160 return score
1161 1161
1162 1162 abspfx = util.localpath(abspfx)
1163 1163 striplen = len(abspfx)
1164 1164 if striplen:
1165 1165 striplen += len(pycompat.ossep)
1166 1166 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1167 1167 score = evalpath(striplen)
1168 1168 striplen1 = len(os.path.split(abspfx)[0])
1169 1169 if striplen1:
1170 1170 striplen1 += len(pycompat.ossep)
1171 1171 if evalpath(striplen1) > score:
1172 1172 striplen = striplen1
1173 1173 res = lambda p: os.path.join(dest,
1174 1174 util.localpath(p)[striplen:])
1175 1175 else:
1176 1176 # a file
1177 1177 if destdirexists:
1178 1178 res = lambda p: os.path.join(dest,
1179 1179 os.path.basename(util.localpath(p)))
1180 1180 else:
1181 1181 res = lambda p: dest
1182 1182 return res
1183 1183
1184 1184 pats = scmutil.expandpats(pats)
1185 1185 if not pats:
1186 1186 raise error.Abort(_('no source or destination specified'))
1187 1187 if len(pats) == 1:
1188 1188 raise error.Abort(_('no destination specified'))
1189 1189 dest = pats.pop()
1190 1190 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1191 1191 if not destdirexists:
1192 1192 if len(pats) > 1 or matchmod.patkind(pats[0]):
1193 1193 raise error.Abort(_('with multiple sources, destination must be an '
1194 1194 'existing directory'))
1195 1195 if util.endswithsep(dest):
1196 1196 raise error.Abort(_('destination %s is not a directory') % dest)
1197 1197
1198 1198 tfn = targetpathfn
1199 1199 if after:
1200 1200 tfn = targetpathafterfn
1201 1201 copylist = []
1202 1202 for pat in pats:
1203 1203 srcs = walkpat(pat)
1204 1204 if not srcs:
1205 1205 continue
1206 1206 copylist.append((tfn(pat, dest, srcs), srcs))
1207 1207 if not copylist:
1208 1208 raise error.Abort(_('no files to copy'))
1209 1209
1210 1210 errors = 0
1211 1211 for targetpath, srcs in copylist:
1212 1212 for abssrc, relsrc, exact in srcs:
1213 1213 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1214 1214 errors += 1
1215 1215
1216 1216 if errors:
1217 1217 ui.warn(_('(consider using --after)\n'))
1218 1218
1219 1219 return errors != 0
1220 1220
1221 1221 ## facility to let extension process additional data into an import patch
1222 1222 # list of identifier to be executed in order
1223 1223 extrapreimport = [] # run before commit
1224 1224 extrapostimport = [] # run after commit
1225 1225 # mapping from identifier to actual import function
1226 1226 #
1227 1227 # 'preimport' are run before the commit is made and are provided the following
1228 1228 # arguments:
1229 1229 # - repo: the localrepository instance,
1230 1230 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1231 1231 # - extra: the future extra dictionary of the changeset, please mutate it,
1232 1232 # - opts: the import options.
1233 1233 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1234 1234 # mutation of in memory commit and more. Feel free to rework the code to get
1235 1235 # there.
1236 1236 extrapreimportmap = {}
1237 1237 # 'postimport' are run after the commit is made and are provided the following
1238 1238 # argument:
1239 1239 # - ctx: the changectx created by import.
1240 1240 extrapostimportmap = {}
1241 1241
1242 1242 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
1243 1243 """Utility function used by commands.import to import a single patch
1244 1244
1245 1245 This function is explicitly defined here to help the evolve extension to
1246 1246 wrap this part of the import logic.
1247 1247
1248 1248 The API is currently a bit ugly because it a simple code translation from
1249 1249 the import command. Feel free to make it better.
1250 1250
1251 1251 :hunk: a patch (as a binary string)
1252 1252 :parents: nodes that will be parent of the created commit
1253 1253 :opts: the full dict of option passed to the import command
1254 1254 :msgs: list to save commit message to.
1255 1255 (used in case we need to save it when failing)
1256 1256 :updatefunc: a function that update a repo to a given node
1257 1257 updatefunc(<repo>, <node>)
1258 1258 """
1259 1259 # avoid cycle context -> subrepo -> cmdutil
1260 1260 from . import context
1261 1261 extractdata = patch.extract(ui, hunk)
1262 1262 tmpname = extractdata.get('filename')
1263 1263 message = extractdata.get('message')
1264 1264 user = opts.get('user') or extractdata.get('user')
1265 1265 date = opts.get('date') or extractdata.get('date')
1266 1266 branch = extractdata.get('branch')
1267 1267 nodeid = extractdata.get('nodeid')
1268 1268 p1 = extractdata.get('p1')
1269 1269 p2 = extractdata.get('p2')
1270 1270
1271 1271 nocommit = opts.get('no_commit')
1272 1272 importbranch = opts.get('import_branch')
1273 1273 update = not opts.get('bypass')
1274 1274 strip = opts["strip"]
1275 1275 prefix = opts["prefix"]
1276 1276 sim = float(opts.get('similarity') or 0)
1277 1277 if not tmpname:
1278 1278 return (None, None, False)
1279 1279
1280 1280 rejects = False
1281 1281
1282 1282 try:
1283 1283 cmdline_message = logmessage(ui, opts)
1284 1284 if cmdline_message:
1285 1285 # pickup the cmdline msg
1286 1286 message = cmdline_message
1287 1287 elif message:
1288 1288 # pickup the patch msg
1289 1289 message = message.strip()
1290 1290 else:
1291 1291 # launch the editor
1292 1292 message = None
1293 1293 ui.debug('message:\n%s\n' % message)
1294 1294
1295 1295 if len(parents) == 1:
1296 1296 parents.append(repo[nullid])
1297 1297 if opts.get('exact'):
1298 1298 if not nodeid or not p1:
1299 1299 raise error.Abort(_('not a Mercurial patch'))
1300 1300 p1 = repo[p1]
1301 1301 p2 = repo[p2 or nullid]
1302 1302 elif p2:
1303 1303 try:
1304 1304 p1 = repo[p1]
1305 1305 p2 = repo[p2]
1306 1306 # Without any options, consider p2 only if the
1307 1307 # patch is being applied on top of the recorded
1308 1308 # first parent.
1309 1309 if p1 != parents[0]:
1310 1310 p1 = parents[0]
1311 1311 p2 = repo[nullid]
1312 1312 except error.RepoError:
1313 1313 p1, p2 = parents
1314 1314 if p2.node() == nullid:
1315 1315 ui.warn(_("warning: import the patch as a normal revision\n"
1316 1316 "(use --exact to import the patch as a merge)\n"))
1317 1317 else:
1318 1318 p1, p2 = parents
1319 1319
1320 1320 n = None
1321 1321 if update:
1322 1322 if p1 != parents[0]:
1323 1323 updatefunc(repo, p1.node())
1324 1324 if p2 != parents[1]:
1325 1325 repo.setparents(p1.node(), p2.node())
1326 1326
1327 1327 if opts.get('exact') or importbranch:
1328 1328 repo.dirstate.setbranch(branch or 'default')
1329 1329
1330 1330 partial = opts.get('partial', False)
1331 1331 files = set()
1332 1332 try:
1333 1333 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
1334 1334 files=files, eolmode=None, similarity=sim / 100.0)
1335 1335 except patch.PatchError as e:
1336 1336 if not partial:
1337 1337 raise error.Abort(str(e))
1338 1338 if partial:
1339 1339 rejects = True
1340 1340
1341 1341 files = list(files)
1342 1342 if nocommit:
1343 1343 if message:
1344 1344 msgs.append(message)
1345 1345 else:
1346 1346 if opts.get('exact') or p2:
1347 1347 # If you got here, you either use --force and know what
1348 1348 # you are doing or used --exact or a merge patch while
1349 1349 # being updated to its first parent.
1350 1350 m = None
1351 1351 else:
1352 1352 m = scmutil.matchfiles(repo, files or [])
1353 1353 editform = mergeeditform(repo[None], 'import.normal')
1354 1354 if opts.get('exact'):
1355 1355 editor = None
1356 1356 else:
1357 1357 editor = getcommiteditor(editform=editform, **opts)
1358 1358 extra = {}
1359 1359 for idfunc in extrapreimport:
1360 1360 extrapreimportmap[idfunc](repo, extractdata, extra, opts)
1361 1361 overrides = {}
1362 1362 if partial:
1363 1363 overrides[('ui', 'allowemptycommit')] = True
1364 1364 with repo.ui.configoverride(overrides, 'import'):
1365 1365 n = repo.commit(message, user,
1366 1366 date, match=m,
1367 1367 editor=editor, extra=extra)
1368 1368 for idfunc in extrapostimport:
1369 1369 extrapostimportmap[idfunc](repo[n])
1370 1370 else:
1371 1371 if opts.get('exact') or importbranch:
1372 1372 branch = branch or 'default'
1373 1373 else:
1374 1374 branch = p1.branch()
1375 1375 store = patch.filestore()
1376 1376 try:
1377 1377 files = set()
1378 1378 try:
1379 1379 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
1380 1380 files, eolmode=None)
1381 1381 except patch.PatchError as e:
1382 1382 raise error.Abort(str(e))
1383 1383 if opts.get('exact'):
1384 1384 editor = None
1385 1385 else:
1386 1386 editor = getcommiteditor(editform='import.bypass')
1387 1387 memctx = context.memctx(repo, (p1.node(), p2.node()),
1388 1388 message,
1389 1389 files=files,
1390 1390 filectxfn=store,
1391 1391 user=user,
1392 1392 date=date,
1393 1393 branch=branch,
1394 1394 editor=editor)
1395 1395 n = memctx.commit()
1396 1396 finally:
1397 1397 store.close()
1398 1398 if opts.get('exact') and nocommit:
1399 1399 # --exact with --no-commit is still useful in that it does merge
1400 1400 # and branch bits
1401 1401 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1402 1402 elif opts.get('exact') and hex(n) != nodeid:
1403 1403 raise error.Abort(_('patch is damaged or loses information'))
1404 1404 msg = _('applied to working directory')
1405 1405 if n:
1406 1406 # i18n: refers to a short changeset id
1407 1407 msg = _('created %s') % short(n)
1408 1408 return (msg, n, rejects)
1409 1409 finally:
1410 1410 os.unlink(tmpname)
1411 1411
1412 1412 # facility to let extensions include additional data in an exported patch
1413 1413 # list of identifiers to be executed in order
1414 1414 extraexport = []
1415 1415 # mapping from identifier to actual export function
1416 1416 # function as to return a string to be added to the header or None
1417 1417 # it is given two arguments (sequencenumber, changectx)
1418 1418 extraexportmap = {}
1419 1419
1420 1420 def _exportsingle(repo, ctx, match, switch_parent, rev, seqno, write, diffopts):
1421 1421 node = scmutil.binnode(ctx)
1422 1422 parents = [p.node() for p in ctx.parents() if p]
1423 1423 branch = ctx.branch()
1424 1424 if switch_parent:
1425 1425 parents.reverse()
1426 1426
1427 1427 if parents:
1428 1428 prev = parents[0]
1429 1429 else:
1430 1430 prev = nullid
1431 1431
1432 1432 write("# HG changeset patch\n")
1433 1433 write("# User %s\n" % ctx.user())
1434 1434 write("# Date %d %d\n" % ctx.date())
1435 1435 write("# %s\n" % util.datestr(ctx.date()))
1436 1436 if branch and branch != 'default':
1437 1437 write("# Branch %s\n" % branch)
1438 1438 write("# Node ID %s\n" % hex(node))
1439 1439 write("# Parent %s\n" % hex(prev))
1440 1440 if len(parents) > 1:
1441 1441 write("# Parent %s\n" % hex(parents[1]))
1442 1442
1443 1443 for headerid in extraexport:
1444 1444 header = extraexportmap[headerid](seqno, ctx)
1445 1445 if header is not None:
1446 1446 write('# %s\n' % header)
1447 1447 write(ctx.description().rstrip())
1448 1448 write("\n\n")
1449 1449
1450 1450 for chunk, label in patch.diffui(repo, prev, node, match, opts=diffopts):
1451 1451 write(chunk, label=label)
1452 1452
1453 1453 def export(repo, revs, fntemplate='hg-%h.patch', fp=None, switch_parent=False,
1454 1454 opts=None, match=None):
1455 1455 '''export changesets as hg patches
1456 1456
1457 1457 Args:
1458 1458 repo: The repository from which we're exporting revisions.
1459 1459 revs: A list of revisions to export as revision numbers.
1460 1460 fntemplate: An optional string to use for generating patch file names.
1461 1461 fp: An optional file-like object to which patches should be written.
1462 1462 switch_parent: If True, show diffs against second parent when not nullid.
1463 1463 Default is false, which always shows diff against p1.
1464 1464 opts: diff options to use for generating the patch.
1465 1465 match: If specified, only export changes to files matching this matcher.
1466 1466
1467 1467 Returns:
1468 1468 Nothing.
1469 1469
1470 1470 Side Effect:
1471 1471 "HG Changeset Patch" data is emitted to one of the following
1472 1472 destinations:
1473 1473 fp is specified: All revs are written to the specified
1474 1474 file-like object.
1475 1475 fntemplate specified: Each rev is written to a unique file named using
1476 1476 the given template.
1477 1477 Neither fp nor template specified: All revs written to repo.ui.write()
1478 1478 '''
1479 1479
1480 1480 total = len(revs)
1481 1481 revwidth = max(len(str(rev)) for rev in revs)
1482 1482 filemode = {}
1483 1483
1484 1484 write = None
1485 1485 dest = '<unnamed>'
1486 1486 if fp:
1487 1487 dest = getattr(fp, 'name', dest)
1488 1488 def write(s, **kw):
1489 1489 fp.write(s)
1490 1490 elif not fntemplate:
1491 1491 write = repo.ui.write
1492 1492
1493 1493 for seqno, rev in enumerate(revs, 1):
1494 1494 ctx = repo[rev]
1495 1495 fo = None
1496 1496 if not fp and fntemplate:
1497 1497 desc_lines = ctx.description().rstrip().split('\n')
1498 1498 desc = desc_lines[0] #Commit always has a first line.
1499 1499 fo = makefileobj(repo, fntemplate, ctx.node(), desc=desc,
1500 1500 total=total, seqno=seqno, revwidth=revwidth,
1501 1501 mode='wb', modemap=filemode)
1502 1502 dest = fo.name
1503 1503 def write(s, **kw):
1504 1504 fo.write(s)
1505 1505 if not dest.startswith('<'):
1506 1506 repo.ui.note("%s\n" % dest)
1507 1507 _exportsingle(
1508 1508 repo, ctx, match, switch_parent, rev, seqno, write, opts)
1509 1509 if fo is not None:
1510 1510 fo.close()
1511 1511
1512 1512 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
1513 1513 changes=None, stat=False, fp=None, prefix='',
1514 1514 root='', listsubrepos=False):
1515 1515 '''show diff or diffstat.'''
1516 1516 if fp is None:
1517 1517 write = ui.write
1518 1518 else:
1519 1519 def write(s, **kw):
1520 1520 fp.write(s)
1521 1521
1522 1522 if root:
1523 1523 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
1524 1524 else:
1525 1525 relroot = ''
1526 1526 if relroot != '':
1527 1527 # XXX relative roots currently don't work if the root is within a
1528 1528 # subrepo
1529 1529 uirelroot = match.uipath(relroot)
1530 1530 relroot += '/'
1531 1531 for matchroot in match.files():
1532 1532 if not matchroot.startswith(relroot):
1533 1533 ui.warn(_('warning: %s not inside relative root %s\n') % (
1534 1534 match.uipath(matchroot), uirelroot))
1535 1535
1536 1536 if stat:
1537 1537 diffopts = diffopts.copy(context=0)
1538 1538 width = 80
1539 1539 if not ui.plain():
1540 1540 width = ui.termwidth()
1541 1541 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
1542 1542 prefix=prefix, relroot=relroot)
1543 1543 for chunk, label in patch.diffstatui(util.iterlines(chunks),
1544 1544 width=width):
1545 1545 write(chunk, label=label)
1546 1546 else:
1547 1547 for chunk, label in patch.diffui(repo, node1, node2, match,
1548 1548 changes, diffopts, prefix=prefix,
1549 1549 relroot=relroot):
1550 1550 write(chunk, label=label)
1551 1551
1552 1552 if listsubrepos:
1553 1553 ctx1 = repo[node1]
1554 1554 ctx2 = repo[node2]
1555 1555 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
1556 1556 tempnode2 = node2
1557 1557 try:
1558 1558 if node2 is not None:
1559 1559 tempnode2 = ctx2.substate[subpath][1]
1560 1560 except KeyError:
1561 1561 # A subrepo that existed in node1 was deleted between node1 and
1562 1562 # node2 (inclusive). Thus, ctx2's substate won't contain that
1563 1563 # subpath. The best we can do is to ignore it.
1564 1564 tempnode2 = None
1565 1565 submatch = matchmod.subdirmatcher(subpath, match)
1566 1566 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
1567 1567 stat=stat, fp=fp, prefix=prefix)
1568 1568
1569 1569 def _changesetlabels(ctx):
1570 1570 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
1571 1571 if ctx.obsolete():
1572 1572 labels.append('changeset.obsolete')
1573 1573 if ctx.isunstable():
1574 1574 labels.append('changeset.unstable')
1575 1575 for instability in ctx.instabilities():
1576 1576 labels.append('instability.%s' % instability)
1577 1577 return ' '.join(labels)
1578 1578
1579 1579 class changeset_printer(object):
1580 1580 '''show changeset information when templating not requested.'''
1581 1581
1582 1582 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1583 1583 self.ui = ui
1584 1584 self.repo = repo
1585 1585 self.buffered = buffered
1586 1586 self.matchfn = matchfn
1587 1587 self.diffopts = diffopts
1588 1588 self.header = {}
1589 1589 self.hunk = {}
1590 1590 self.lastheader = None
1591 1591 self.footer = None
1592 1592
1593 1593 def flush(self, ctx):
1594 1594 rev = ctx.rev()
1595 1595 if rev in self.header:
1596 1596 h = self.header[rev]
1597 1597 if h != self.lastheader:
1598 1598 self.lastheader = h
1599 1599 self.ui.write(h)
1600 1600 del self.header[rev]
1601 1601 if rev in self.hunk:
1602 1602 self.ui.write(self.hunk[rev])
1603 1603 del self.hunk[rev]
1604 1604 return 1
1605 1605 return 0
1606 1606
1607 1607 def close(self):
1608 1608 if self.footer:
1609 1609 self.ui.write(self.footer)
1610 1610
1611 1611 def show(self, ctx, copies=None, matchfn=None, **props):
1612 1612 props = pycompat.byteskwargs(props)
1613 1613 if self.buffered:
1614 1614 self.ui.pushbuffer(labeled=True)
1615 1615 self._show(ctx, copies, matchfn, props)
1616 1616 self.hunk[ctx.rev()] = self.ui.popbuffer()
1617 1617 else:
1618 1618 self._show(ctx, copies, matchfn, props)
1619 1619
1620 1620 def _show(self, ctx, copies, matchfn, props):
1621 1621 '''show a single changeset or file revision'''
1622 1622 changenode = ctx.node()
1623 1623 rev = ctx.rev()
1624 1624 if self.ui.debugflag:
1625 1625 hexfunc = hex
1626 1626 else:
1627 1627 hexfunc = short
1628 1628 # as of now, wctx.node() and wctx.rev() return None, but we want to
1629 1629 # show the same values as {node} and {rev} templatekw
1630 1630 revnode = (scmutil.intrev(ctx), hexfunc(scmutil.binnode(ctx)))
1631 1631
1632 1632 if self.ui.quiet:
1633 1633 self.ui.write("%d:%s\n" % revnode, label='log.node')
1634 1634 return
1635 1635
1636 1636 date = util.datestr(ctx.date())
1637 1637
1638 1638 # i18n: column positioning for "hg log"
1639 1639 self.ui.write(_("changeset: %d:%s\n") % revnode,
1640 1640 label=_changesetlabels(ctx))
1641 1641
1642 1642 # branches are shown first before any other names due to backwards
1643 1643 # compatibility
1644 1644 branch = ctx.branch()
1645 1645 # don't show the default branch name
1646 1646 if branch != 'default':
1647 1647 # i18n: column positioning for "hg log"
1648 1648 self.ui.write(_("branch: %s\n") % branch,
1649 1649 label='log.branch')
1650 1650
1651 1651 for nsname, ns in self.repo.names.iteritems():
1652 1652 # branches has special logic already handled above, so here we just
1653 1653 # skip it
1654 1654 if nsname == 'branches':
1655 1655 continue
1656 1656 # we will use the templatename as the color name since those two
1657 1657 # should be the same
1658 1658 for name in ns.names(self.repo, changenode):
1659 1659 self.ui.write(ns.logfmt % name,
1660 1660 label='log.%s' % ns.colorname)
1661 1661 if self.ui.debugflag:
1662 1662 # i18n: column positioning for "hg log"
1663 1663 self.ui.write(_("phase: %s\n") % ctx.phasestr(),
1664 1664 label='log.phase')
1665 1665 for pctx in scmutil.meaningfulparents(self.repo, ctx):
1666 1666 label = 'log.parent changeset.%s' % pctx.phasestr()
1667 1667 # i18n: column positioning for "hg log"
1668 1668 self.ui.write(_("parent: %d:%s\n")
1669 1669 % (pctx.rev(), hexfunc(pctx.node())),
1670 1670 label=label)
1671 1671
1672 1672 if self.ui.debugflag and rev is not None:
1673 1673 mnode = ctx.manifestnode()
1674 1674 # i18n: column positioning for "hg log"
1675 1675 self.ui.write(_("manifest: %d:%s\n") %
1676 1676 (self.repo.manifestlog._revlog.rev(mnode),
1677 1677 hex(mnode)),
1678 1678 label='ui.debug log.manifest')
1679 1679 # i18n: column positioning for "hg log"
1680 1680 self.ui.write(_("user: %s\n") % ctx.user(),
1681 1681 label='log.user')
1682 1682 # i18n: column positioning for "hg log"
1683 1683 self.ui.write(_("date: %s\n") % date,
1684 1684 label='log.date')
1685 1685
1686 1686 if ctx.isunstable():
1687 1687 # i18n: column positioning for "hg log"
1688 1688 instabilities = ctx.instabilities()
1689 1689 self.ui.write(_("instability: %s\n") % ', '.join(instabilities),
1690 1690 label='log.instability')
1691 1691
1692 1692 self._exthook(ctx)
1693 1693
1694 1694 if self.ui.debugflag:
1695 1695 files = ctx.p1().status(ctx)[:3]
1696 1696 for key, value in zip([# i18n: column positioning for "hg log"
1697 1697 _("files:"),
1698 1698 # i18n: column positioning for "hg log"
1699 1699 _("files+:"),
1700 1700 # i18n: column positioning for "hg log"
1701 1701 _("files-:")], files):
1702 1702 if value:
1703 1703 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
1704 1704 label='ui.debug log.files')
1705 1705 elif ctx.files() and self.ui.verbose:
1706 1706 # i18n: column positioning for "hg log"
1707 1707 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
1708 1708 label='ui.note log.files')
1709 1709 if copies and self.ui.verbose:
1710 1710 copies = ['%s (%s)' % c for c in copies]
1711 1711 # i18n: column positioning for "hg log"
1712 1712 self.ui.write(_("copies: %s\n") % ' '.join(copies),
1713 1713 label='ui.note log.copies')
1714 1714
1715 1715 extra = ctx.extra()
1716 1716 if extra and self.ui.debugflag:
1717 1717 for key, value in sorted(extra.items()):
1718 1718 # i18n: column positioning for "hg log"
1719 1719 self.ui.write(_("extra: %s=%s\n")
1720 1720 % (key, util.escapestr(value)),
1721 1721 label='ui.debug log.extra')
1722 1722
1723 1723 description = ctx.description().strip()
1724 1724 if description:
1725 1725 if self.ui.verbose:
1726 1726 self.ui.write(_("description:\n"),
1727 1727 label='ui.note log.description')
1728 1728 self.ui.write(description,
1729 1729 label='ui.note log.description')
1730 1730 self.ui.write("\n\n")
1731 1731 else:
1732 1732 # i18n: column positioning for "hg log"
1733 1733 self.ui.write(_("summary: %s\n") %
1734 1734 description.splitlines()[0],
1735 1735 label='log.summary')
1736 1736 self.ui.write("\n")
1737 1737
1738 1738 self.showpatch(ctx, matchfn)
1739 1739
1740 1740 def _exthook(self, ctx):
1741 1741 '''empty method used by extension as a hook point
1742 1742 '''
1743 1743 pass
1744 1744
1745 1745 def showpatch(self, ctx, matchfn):
1746 1746 if not matchfn:
1747 1747 matchfn = self.matchfn
1748 1748 if matchfn:
1749 1749 stat = self.diffopts.get('stat')
1750 1750 diff = self.diffopts.get('patch')
1751 1751 diffopts = patch.diffallopts(self.ui, self.diffopts)
1752 1752 node = ctx.node()
1753 1753 prev = ctx.p1().node()
1754 1754 if stat:
1755 1755 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1756 1756 match=matchfn, stat=True)
1757 1757 if diff:
1758 1758 if stat:
1759 1759 self.ui.write("\n")
1760 1760 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1761 1761 match=matchfn, stat=False)
1762 1762 self.ui.write("\n")
1763 1763
1764 1764 class jsonchangeset(changeset_printer):
1765 1765 '''format changeset information.'''
1766 1766
1767 1767 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1768 1768 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1769 1769 self.cache = {}
1770 1770 self._first = True
1771 1771
1772 1772 def close(self):
1773 1773 if not self._first:
1774 1774 self.ui.write("\n]\n")
1775 1775 else:
1776 1776 self.ui.write("[]\n")
1777 1777
1778 1778 def _show(self, ctx, copies, matchfn, props):
1779 1779 '''show a single changeset or file revision'''
1780 1780 rev = ctx.rev()
1781 1781 if rev is None:
1782 1782 jrev = jnode = 'null'
1783 1783 else:
1784 1784 jrev = '%d' % rev
1785 1785 jnode = '"%s"' % hex(ctx.node())
1786 1786 j = encoding.jsonescape
1787 1787
1788 1788 if self._first:
1789 1789 self.ui.write("[\n {")
1790 1790 self._first = False
1791 1791 else:
1792 1792 self.ui.write(",\n {")
1793 1793
1794 1794 if self.ui.quiet:
1795 1795 self.ui.write(('\n "rev": %s') % jrev)
1796 1796 self.ui.write((',\n "node": %s') % jnode)
1797 1797 self.ui.write('\n }')
1798 1798 return
1799 1799
1800 1800 self.ui.write(('\n "rev": %s') % jrev)
1801 1801 self.ui.write((',\n "node": %s') % jnode)
1802 1802 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
1803 1803 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
1804 1804 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
1805 1805 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
1806 1806 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
1807 1807
1808 1808 self.ui.write((',\n "bookmarks": [%s]') %
1809 1809 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1810 1810 self.ui.write((',\n "tags": [%s]') %
1811 1811 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1812 1812 self.ui.write((',\n "parents": [%s]') %
1813 1813 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1814 1814
1815 1815 if self.ui.debugflag:
1816 1816 if rev is None:
1817 1817 jmanifestnode = 'null'
1818 1818 else:
1819 1819 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
1820 1820 self.ui.write((',\n "manifest": %s') % jmanifestnode)
1821 1821
1822 1822 self.ui.write((',\n "extra": {%s}') %
1823 1823 ", ".join('"%s": "%s"' % (j(k), j(v))
1824 1824 for k, v in ctx.extra().items()))
1825 1825
1826 1826 files = ctx.p1().status(ctx)
1827 1827 self.ui.write((',\n "modified": [%s]') %
1828 1828 ", ".join('"%s"' % j(f) for f in files[0]))
1829 1829 self.ui.write((',\n "added": [%s]') %
1830 1830 ", ".join('"%s"' % j(f) for f in files[1]))
1831 1831 self.ui.write((',\n "removed": [%s]') %
1832 1832 ", ".join('"%s"' % j(f) for f in files[2]))
1833 1833
1834 1834 elif self.ui.verbose:
1835 1835 self.ui.write((',\n "files": [%s]') %
1836 1836 ", ".join('"%s"' % j(f) for f in ctx.files()))
1837 1837
1838 1838 if copies:
1839 1839 self.ui.write((',\n "copies": {%s}') %
1840 1840 ", ".join('"%s": "%s"' % (j(k), j(v))
1841 1841 for k, v in copies))
1842 1842
1843 1843 matchfn = self.matchfn
1844 1844 if matchfn:
1845 1845 stat = self.diffopts.get('stat')
1846 1846 diff = self.diffopts.get('patch')
1847 1847 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1848 1848 node, prev = ctx.node(), ctx.p1().node()
1849 1849 if stat:
1850 1850 self.ui.pushbuffer()
1851 1851 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1852 1852 match=matchfn, stat=True)
1853 1853 self.ui.write((',\n "diffstat": "%s"')
1854 1854 % j(self.ui.popbuffer()))
1855 1855 if diff:
1856 1856 self.ui.pushbuffer()
1857 1857 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1858 1858 match=matchfn, stat=False)
1859 1859 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
1860 1860
1861 1861 self.ui.write("\n }")
1862 1862
1863 1863 class changeset_templater(changeset_printer):
1864 1864 '''format changeset information.'''
1865 1865
1866 1866 # Arguments before "buffered" used to be positional. Consider not
1867 1867 # adding/removing arguments before "buffered" to not break callers.
1868 1868 def __init__(self, ui, repo, tmplspec, matchfn=None, diffopts=None,
1869 1869 buffered=False):
1870 1870 diffopts = diffopts or {}
1871 1871
1872 1872 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1873 1873 self.t = formatter.loadtemplater(ui, tmplspec,
1874 1874 cache=templatekw.defaulttempl)
1875 1875 self._counter = itertools.count()
1876 1876 self.cache = {}
1877 1877
1878 1878 self._tref = tmplspec.ref
1879 1879 self._parts = {'header': '', 'footer': '',
1880 1880 tmplspec.ref: tmplspec.ref,
1881 1881 'docheader': '', 'docfooter': '',
1882 1882 'separator': ''}
1883 1883 if tmplspec.mapfile:
1884 1884 # find correct templates for current mode, for backward
1885 1885 # compatibility with 'log -v/-q/--debug' using a mapfile
1886 1886 tmplmodes = [
1887 1887 (True, ''),
1888 1888 (self.ui.verbose, '_verbose'),
1889 1889 (self.ui.quiet, '_quiet'),
1890 1890 (self.ui.debugflag, '_debug'),
1891 1891 ]
1892 1892 for mode, postfix in tmplmodes:
1893 1893 for t in self._parts:
1894 1894 cur = t + postfix
1895 1895 if mode and cur in self.t:
1896 1896 self._parts[t] = cur
1897 1897 else:
1898 1898 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
1899 1899 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
1900 1900 self._parts.update(m)
1901 1901
1902 1902 if self._parts['docheader']:
1903 1903 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
1904 1904
1905 1905 def close(self):
1906 1906 if self._parts['docfooter']:
1907 1907 if not self.footer:
1908 1908 self.footer = ""
1909 1909 self.footer += templater.stringify(self.t(self._parts['docfooter']))
1910 1910 return super(changeset_templater, self).close()
1911 1911
1912 1912 def _show(self, ctx, copies, matchfn, props):
1913 1913 '''show a single changeset or file revision'''
1914 1914 props = props.copy()
1915 1915 props.update(templatekw.keywords)
1916 1916 props['templ'] = self.t
1917 1917 props['ctx'] = ctx
1918 1918 props['repo'] = self.repo
1919 1919 props['ui'] = self.repo.ui
1920 1920 props['index'] = index = next(self._counter)
1921 1921 props['revcache'] = {'copies': copies}
1922 1922 props['cache'] = self.cache
1923 1923 props = pycompat.strkwargs(props)
1924 1924
1925 1925 # write separator, which wouldn't work well with the header part below
1926 1926 # since there's inherently a conflict between header (across items) and
1927 1927 # separator (per item)
1928 1928 if self._parts['separator'] and index > 0:
1929 1929 self.ui.write(templater.stringify(self.t(self._parts['separator'])))
1930 1930
1931 1931 # write header
1932 1932 if self._parts['header']:
1933 1933 h = templater.stringify(self.t(self._parts['header'], **props))
1934 1934 if self.buffered:
1935 1935 self.header[ctx.rev()] = h
1936 1936 else:
1937 1937 if self.lastheader != h:
1938 1938 self.lastheader = h
1939 1939 self.ui.write(h)
1940 1940
1941 1941 # write changeset metadata, then patch if requested
1942 1942 key = self._parts[self._tref]
1943 1943 self.ui.write(templater.stringify(self.t(key, **props)))
1944 1944 self.showpatch(ctx, matchfn)
1945 1945
1946 1946 if self._parts['footer']:
1947 1947 if not self.footer:
1948 1948 self.footer = templater.stringify(
1949 1949 self.t(self._parts['footer'], **props))
1950 1950
1951 1951 def logtemplatespec(tmpl, mapfile):
1952 1952 if mapfile:
1953 1953 return formatter.templatespec('changeset', tmpl, mapfile)
1954 1954 else:
1955 1955 return formatter.templatespec('', tmpl, None)
1956 1956
1957 1957 def _lookuplogtemplate(ui, tmpl, style):
1958 1958 """Find the template matching the given template spec or style
1959 1959
1960 1960 See formatter.lookuptemplate() for details.
1961 1961 """
1962 1962
1963 1963 # ui settings
1964 1964 if not tmpl and not style: # template are stronger than style
1965 1965 tmpl = ui.config('ui', 'logtemplate')
1966 1966 if tmpl:
1967 1967 return logtemplatespec(templater.unquotestring(tmpl), None)
1968 1968 else:
1969 1969 style = util.expandpath(ui.config('ui', 'style'))
1970 1970
1971 1971 if not tmpl and style:
1972 1972 mapfile = style
1973 1973 if not os.path.split(mapfile)[0]:
1974 1974 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1975 1975 or templater.templatepath(mapfile))
1976 1976 if mapname:
1977 1977 mapfile = mapname
1978 1978 return logtemplatespec(None, mapfile)
1979 1979
1980 1980 if not tmpl:
1981 1981 return logtemplatespec(None, None)
1982 1982
1983 1983 return formatter.lookuptemplate(ui, 'changeset', tmpl)
1984 1984
1985 1985 def makelogtemplater(ui, repo, tmpl, buffered=False):
1986 1986 """Create a changeset_templater from a literal template 'tmpl'"""
1987 1987 spec = logtemplatespec(tmpl, None)
1988 1988 return changeset_templater(ui, repo, spec, buffered=buffered)
1989 1989
1990 1990 def show_changeset(ui, repo, opts, buffered=False):
1991 1991 """show one changeset using template or regular display.
1992 1992
1993 1993 Display format will be the first non-empty hit of:
1994 1994 1. option 'template'
1995 1995 2. option 'style'
1996 1996 3. [ui] setting 'logtemplate'
1997 1997 4. [ui] setting 'style'
1998 1998 If all of these values are either the unset or the empty string,
1999 1999 regular display via changeset_printer() is done.
2000 2000 """
2001 2001 # options
2002 2002 matchfn = None
2003 2003 if opts.get('patch') or opts.get('stat'):
2004 2004 matchfn = scmutil.matchall(repo)
2005 2005
2006 2006 if opts.get('template') == 'json':
2007 2007 return jsonchangeset(ui, repo, matchfn, opts, buffered)
2008 2008
2009 2009 spec = _lookuplogtemplate(ui, opts.get('template'), opts.get('style'))
2010 2010
2011 2011 if not spec.ref and not spec.tmpl and not spec.mapfile:
2012 2012 return changeset_printer(ui, repo, matchfn, opts, buffered)
2013 2013
2014 2014 return changeset_templater(ui, repo, spec, matchfn, opts, buffered)
2015 2015
2016 2016 def showmarker(fm, marker, index=None):
2017 2017 """utility function to display obsolescence marker in a readable way
2018 2018
2019 2019 To be used by debug function."""
2020 2020 if index is not None:
2021 2021 fm.write('index', '%i ', index)
2022 2022 fm.write('prednode', '%s ', hex(marker.prednode()))
2023 2023 succs = marker.succnodes()
2024 2024 fm.condwrite(succs, 'succnodes', '%s ',
2025 2025 fm.formatlist(map(hex, succs), name='node'))
2026 2026 fm.write('flag', '%X ', marker.flags())
2027 2027 parents = marker.parentnodes()
2028 2028 if parents is not None:
2029 2029 fm.write('parentnodes', '{%s} ',
2030 2030 fm.formatlist(map(hex, parents), name='node', sep=', '))
2031 2031 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
2032 2032 meta = marker.metadata().copy()
2033 2033 meta.pop('date', None)
2034 2034 fm.write('metadata', '{%s}', fm.formatdict(meta, fmt='%r: %r', sep=', '))
2035 2035 fm.plain('\n')
2036 2036
2037 2037 def finddate(ui, repo, date):
2038 2038 """Find the tipmost changeset that matches the given date spec"""
2039 2039
2040 2040 df = util.matchdate(date)
2041 2041 m = scmutil.matchall(repo)
2042 2042 results = {}
2043 2043
2044 2044 def prep(ctx, fns):
2045 2045 d = ctx.date()
2046 2046 if df(d[0]):
2047 2047 results[ctx.rev()] = d
2048 2048
2049 2049 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
2050 2050 rev = ctx.rev()
2051 2051 if rev in results:
2052 2052 ui.status(_("found revision %s from %s\n") %
2053 2053 (rev, util.datestr(results[rev])))
2054 2054 return '%d' % rev
2055 2055
2056 2056 raise error.Abort(_("revision matching date not found"))
2057 2057
2058 2058 def increasingwindows(windowsize=8, sizelimit=512):
2059 2059 while True:
2060 2060 yield windowsize
2061 2061 if windowsize < sizelimit:
2062 2062 windowsize *= 2
2063 2063
2064 2064 class FileWalkError(Exception):
2065 2065 pass
2066 2066
2067 2067 def walkfilerevs(repo, match, follow, revs, fncache):
2068 2068 '''Walks the file history for the matched files.
2069 2069
2070 2070 Returns the changeset revs that are involved in the file history.
2071 2071
2072 2072 Throws FileWalkError if the file history can't be walked using
2073 2073 filelogs alone.
2074 2074 '''
2075 2075 wanted = set()
2076 2076 copies = []
2077 2077 minrev, maxrev = min(revs), max(revs)
2078 2078 def filerevgen(filelog, last):
2079 2079 """
2080 2080 Only files, no patterns. Check the history of each file.
2081 2081
2082 2082 Examines filelog entries within minrev, maxrev linkrev range
2083 2083 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
2084 2084 tuples in backwards order
2085 2085 """
2086 2086 cl_count = len(repo)
2087 2087 revs = []
2088 2088 for j in xrange(0, last + 1):
2089 2089 linkrev = filelog.linkrev(j)
2090 2090 if linkrev < minrev:
2091 2091 continue
2092 2092 # only yield rev for which we have the changelog, it can
2093 2093 # happen while doing "hg log" during a pull or commit
2094 2094 if linkrev >= cl_count:
2095 2095 break
2096 2096
2097 2097 parentlinkrevs = []
2098 2098 for p in filelog.parentrevs(j):
2099 2099 if p != nullrev:
2100 2100 parentlinkrevs.append(filelog.linkrev(p))
2101 2101 n = filelog.node(j)
2102 2102 revs.append((linkrev, parentlinkrevs,
2103 2103 follow and filelog.renamed(n)))
2104 2104
2105 2105 return reversed(revs)
2106 2106 def iterfiles():
2107 2107 pctx = repo['.']
2108 2108 for filename in match.files():
2109 2109 if follow:
2110 2110 if filename not in pctx:
2111 2111 raise error.Abort(_('cannot follow file not in parent '
2112 2112 'revision: "%s"') % filename)
2113 2113 yield filename, pctx[filename].filenode()
2114 2114 else:
2115 2115 yield filename, None
2116 2116 for filename_node in copies:
2117 2117 yield filename_node
2118 2118
2119 2119 for file_, node in iterfiles():
2120 2120 filelog = repo.file(file_)
2121 2121 if not len(filelog):
2122 2122 if node is None:
2123 2123 # A zero count may be a directory or deleted file, so
2124 2124 # try to find matching entries on the slow path.
2125 2125 if follow:
2126 2126 raise error.Abort(
2127 2127 _('cannot follow nonexistent file: "%s"') % file_)
2128 2128 raise FileWalkError("Cannot walk via filelog")
2129 2129 else:
2130 2130 continue
2131 2131
2132 2132 if node is None:
2133 2133 last = len(filelog) - 1
2134 2134 else:
2135 2135 last = filelog.rev(node)
2136 2136
2137 2137 # keep track of all ancestors of the file
2138 2138 ancestors = {filelog.linkrev(last)}
2139 2139
2140 2140 # iterate from latest to oldest revision
2141 2141 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
2142 2142 if not follow:
2143 2143 if rev > maxrev:
2144 2144 continue
2145 2145 else:
2146 2146 # Note that last might not be the first interesting
2147 2147 # rev to us:
2148 2148 # if the file has been changed after maxrev, we'll
2149 2149 # have linkrev(last) > maxrev, and we still need
2150 2150 # to explore the file graph
2151 2151 if rev not in ancestors:
2152 2152 continue
2153 2153 # XXX insert 1327 fix here
2154 2154 if flparentlinkrevs:
2155 2155 ancestors.update(flparentlinkrevs)
2156 2156
2157 2157 fncache.setdefault(rev, []).append(file_)
2158 2158 wanted.add(rev)
2159 2159 if copied:
2160 2160 copies.append(copied)
2161 2161
2162 2162 return wanted
2163 2163
2164 2164 class _followfilter(object):
2165 2165 def __init__(self, repo, onlyfirst=False):
2166 2166 self.repo = repo
2167 2167 self.startrev = nullrev
2168 2168 self.roots = set()
2169 2169 self.onlyfirst = onlyfirst
2170 2170
2171 2171 def match(self, rev):
2172 2172 def realparents(rev):
2173 2173 if self.onlyfirst:
2174 2174 return self.repo.changelog.parentrevs(rev)[0:1]
2175 2175 else:
2176 2176 return filter(lambda x: x != nullrev,
2177 2177 self.repo.changelog.parentrevs(rev))
2178 2178
2179 2179 if self.startrev == nullrev:
2180 2180 self.startrev = rev
2181 2181 return True
2182 2182
2183 2183 if rev > self.startrev:
2184 2184 # forward: all descendants
2185 2185 if not self.roots:
2186 2186 self.roots.add(self.startrev)
2187 2187 for parent in realparents(rev):
2188 2188 if parent in self.roots:
2189 2189 self.roots.add(rev)
2190 2190 return True
2191 2191 else:
2192 2192 # backwards: all parents
2193 2193 if not self.roots:
2194 2194 self.roots.update(realparents(self.startrev))
2195 2195 if rev in self.roots:
2196 2196 self.roots.remove(rev)
2197 2197 self.roots.update(realparents(rev))
2198 2198 return True
2199 2199
2200 2200 return False
2201 2201
2202 2202 def walkchangerevs(repo, match, opts, prepare):
2203 2203 '''Iterate over files and the revs in which they changed.
2204 2204
2205 2205 Callers most commonly need to iterate backwards over the history
2206 2206 in which they are interested. Doing so has awful (quadratic-looking)
2207 2207 performance, so we use iterators in a "windowed" way.
2208 2208
2209 2209 We walk a window of revisions in the desired order. Within the
2210 2210 window, we first walk forwards to gather data, then in the desired
2211 2211 order (usually backwards) to display it.
2212 2212
2213 2213 This function returns an iterator yielding contexts. Before
2214 2214 yielding each context, the iterator will first call the prepare
2215 2215 function on each context in the window in forward order.'''
2216 2216
2217 2217 follow = opts.get('follow') or opts.get('follow_first')
2218 2218 revs = _logrevs(repo, opts)
2219 2219 if not revs:
2220 2220 return []
2221 2221 wanted = set()
2222 2222 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
2223 2223 opts.get('removed'))
2224 2224 fncache = {}
2225 2225 change = repo.changectx
2226 2226
2227 2227 # First step is to fill wanted, the set of revisions that we want to yield.
2228 2228 # When it does not induce extra cost, we also fill fncache for revisions in
2229 2229 # wanted: a cache of filenames that were changed (ctx.files()) and that
2230 2230 # match the file filtering conditions.
2231 2231
2232 2232 if match.always():
2233 2233 # No files, no patterns. Display all revs.
2234 2234 wanted = revs
2235 2235 elif not slowpath:
2236 2236 # We only have to read through the filelog to find wanted revisions
2237 2237
2238 2238 try:
2239 2239 wanted = walkfilerevs(repo, match, follow, revs, fncache)
2240 2240 except FileWalkError:
2241 2241 slowpath = True
2242 2242
2243 2243 # We decided to fall back to the slowpath because at least one
2244 2244 # of the paths was not a file. Check to see if at least one of them
2245 2245 # existed in history, otherwise simply return
2246 2246 for path in match.files():
2247 2247 if path == '.' or path in repo.store:
2248 2248 break
2249 2249 else:
2250 2250 return []
2251 2251
2252 2252 if slowpath:
2253 2253 # We have to read the changelog to match filenames against
2254 2254 # changed files
2255 2255
2256 2256 if follow:
2257 2257 raise error.Abort(_('can only follow copies/renames for explicit '
2258 2258 'filenames'))
2259 2259
2260 2260 # The slow path checks files modified in every changeset.
2261 2261 # This is really slow on large repos, so compute the set lazily.
2262 2262 class lazywantedset(object):
2263 2263 def __init__(self):
2264 2264 self.set = set()
2265 2265 self.revs = set(revs)
2266 2266
2267 2267 # No need to worry about locality here because it will be accessed
2268 2268 # in the same order as the increasing window below.
2269 2269 def __contains__(self, value):
2270 2270 if value in self.set:
2271 2271 return True
2272 2272 elif not value in self.revs:
2273 2273 return False
2274 2274 else:
2275 2275 self.revs.discard(value)
2276 2276 ctx = change(value)
2277 2277 matches = filter(match, ctx.files())
2278 2278 if matches:
2279 2279 fncache[value] = matches
2280 2280 self.set.add(value)
2281 2281 return True
2282 2282 return False
2283 2283
2284 2284 def discard(self, value):
2285 2285 self.revs.discard(value)
2286 2286 self.set.discard(value)
2287 2287
2288 2288 wanted = lazywantedset()
2289 2289
2290 2290 # it might be worthwhile to do this in the iterator if the rev range
2291 2291 # is descending and the prune args are all within that range
2292 2292 for rev in opts.get('prune', ()):
2293 2293 rev = repo[rev].rev()
2294 2294 ff = _followfilter(repo)
2295 2295 stop = min(revs[0], revs[-1])
2296 2296 for x in xrange(rev, stop - 1, -1):
2297 2297 if ff.match(x):
2298 2298 wanted = wanted - [x]
2299 2299
2300 2300 # Now that wanted is correctly initialized, we can iterate over the
2301 2301 # revision range, yielding only revisions in wanted.
2302 2302 def iterate():
2303 2303 if follow and match.always():
2304 2304 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
2305 2305 def want(rev):
2306 2306 return ff.match(rev) and rev in wanted
2307 2307 else:
2308 2308 def want(rev):
2309 2309 return rev in wanted
2310 2310
2311 2311 it = iter(revs)
2312 2312 stopiteration = False
2313 2313 for windowsize in increasingwindows():
2314 2314 nrevs = []
2315 2315 for i in xrange(windowsize):
2316 2316 rev = next(it, None)
2317 2317 if rev is None:
2318 2318 stopiteration = True
2319 2319 break
2320 2320 elif want(rev):
2321 2321 nrevs.append(rev)
2322 2322 for rev in sorted(nrevs):
2323 2323 fns = fncache.get(rev)
2324 2324 ctx = change(rev)
2325 2325 if not fns:
2326 2326 def fns_generator():
2327 2327 for f in ctx.files():
2328 2328 if match(f):
2329 2329 yield f
2330 2330 fns = fns_generator()
2331 2331 prepare(ctx, fns)
2332 2332 for rev in nrevs:
2333 2333 yield change(rev)
2334 2334
2335 2335 if stopiteration:
2336 2336 break
2337 2337
2338 2338 return iterate()
2339 2339
2340 2340 def _makefollowlogfilematcher(repo, files, followfirst):
2341 2341 # When displaying a revision with --patch --follow FILE, we have
2342 2342 # to know which file of the revision must be diffed. With
2343 2343 # --follow, we want the names of the ancestors of FILE in the
2344 2344 # revision, stored in "fcache". "fcache" is populated by
2345 2345 # reproducing the graph traversal already done by --follow revset
2346 2346 # and relating revs to file names (which is not "correct" but
2347 2347 # good enough).
2348 2348 fcache = {}
2349 2349 fcacheready = [False]
2350 2350 pctx = repo['.']
2351 2351
2352 2352 def populate():
2353 2353 for fn in files:
2354 2354 fctx = pctx[fn]
2355 2355 fcache.setdefault(fctx.introrev(), set()).add(fctx.path())
2356 2356 for c in fctx.ancestors(followfirst=followfirst):
2357 2357 fcache.setdefault(c.rev(), set()).add(c.path())
2358 2358
2359 2359 def filematcher(rev):
2360 2360 if not fcacheready[0]:
2361 2361 # Lazy initialization
2362 2362 fcacheready[0] = True
2363 2363 populate()
2364 2364 return scmutil.matchfiles(repo, fcache.get(rev, []))
2365 2365
2366 2366 return filematcher
2367 2367
2368 2368 def _makenofollowlogfilematcher(repo, pats, opts):
2369 2369 '''hook for extensions to override the filematcher for non-follow cases'''
2370 2370 return None
2371 2371
2372 2372 def _makelogrevset(repo, pats, opts, revs):
2373 2373 """Return (expr, filematcher) where expr is a revset string built
2374 2374 from log options and file patterns or None. If --stat or --patch
2375 2375 are not passed filematcher is None. Otherwise it is a callable
2376 2376 taking a revision number and returning a match objects filtering
2377 2377 the files to be detailed when displaying the revision.
2378 2378 """
2379 2379 opt2revset = {
2380 2380 'no_merges': ('not merge()', None),
2381 2381 'only_merges': ('merge()', None),
2382 2382 '_ancestors': ('ancestors(%(val)s)', None),
2383 2383 '_fancestors': ('_firstancestors(%(val)s)', None),
2384 2384 '_descendants': ('descendants(%(val)s)', None),
2385 2385 '_fdescendants': ('_firstdescendants(%(val)s)', None),
2386 2386 '_matchfiles': ('_matchfiles(%(val)s)', None),
2387 2387 'date': ('date(%(val)r)', None),
2388 2388 'branch': ('branch(%(val)r)', ' or '),
2389 2389 '_patslog': ('filelog(%(val)r)', ' or '),
2390 2390 '_patsfollow': ('follow(%(val)r)', ' or '),
2391 2391 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
2392 2392 'keyword': ('keyword(%(val)r)', ' or '),
2393 2393 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
2394 2394 'user': ('user(%(val)r)', ' or '),
2395 2395 }
2396 2396
2397 2397 opts = dict(opts)
2398 2398 # follow or not follow?
2399 2399 follow = opts.get('follow') or opts.get('follow_first')
2400 2400 if opts.get('follow_first'):
2401 2401 followfirst = 1
2402 2402 else:
2403 2403 followfirst = 0
2404 2404 # --follow with FILE behavior depends on revs...
2405 2405 it = iter(revs)
2406 2406 startrev = next(it)
2407 2407 followdescendants = startrev < next(it, startrev)
2408 2408
2409 2409 # branch and only_branch are really aliases and must be handled at
2410 2410 # the same time
2411 2411 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
2412 2412 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
2413 2413 # pats/include/exclude are passed to match.match() directly in
2414 2414 # _matchfiles() revset but walkchangerevs() builds its matcher with
2415 2415 # scmutil.match(). The difference is input pats are globbed on
2416 2416 # platforms without shell expansion (windows).
2417 2417 wctx = repo[None]
2418 2418 match, pats = scmutil.matchandpats(wctx, pats, opts)
2419 2419 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
2420 2420 opts.get('removed'))
2421 2421 if not slowpath:
2422 2422 for f in match.files():
2423 2423 if follow and f not in wctx:
2424 2424 # If the file exists, it may be a directory, so let it
2425 2425 # take the slow path.
2426 2426 if os.path.exists(repo.wjoin(f)):
2427 2427 slowpath = True
2428 2428 continue
2429 2429 else:
2430 2430 raise error.Abort(_('cannot follow file not in parent '
2431 2431 'revision: "%s"') % f)
2432 2432 filelog = repo.file(f)
2433 2433 if not filelog:
2434 2434 # A zero count may be a directory or deleted file, so
2435 2435 # try to find matching entries on the slow path.
2436 2436 if follow:
2437 2437 raise error.Abort(
2438 2438 _('cannot follow nonexistent file: "%s"') % f)
2439 2439 slowpath = True
2440 2440
2441 2441 # We decided to fall back to the slowpath because at least one
2442 2442 # of the paths was not a file. Check to see if at least one of them
2443 2443 # existed in history - in that case, we'll continue down the
2444 2444 # slowpath; otherwise, we can turn off the slowpath
2445 2445 if slowpath:
2446 2446 for path in match.files():
2447 2447 if path == '.' or path in repo.store:
2448 2448 break
2449 2449 else:
2450 2450 slowpath = False
2451 2451
2452 2452 fpats = ('_patsfollow', '_patsfollowfirst')
2453 2453 fnopats = (('_ancestors', '_fancestors'),
2454 2454 ('_descendants', '_fdescendants'))
2455 2455 if slowpath:
2456 2456 # See walkchangerevs() slow path.
2457 2457 #
2458 2458 # pats/include/exclude cannot be represented as separate
2459 2459 # revset expressions as their filtering logic applies at file
2460 2460 # level. For instance "-I a -X a" matches a revision touching
2461 2461 # "a" and "b" while "file(a) and not file(b)" does
2462 2462 # not. Besides, filesets are evaluated against the working
2463 2463 # directory.
2464 2464 matchargs = ['r:', 'd:relpath']
2465 2465 for p in pats:
2466 2466 matchargs.append('p:' + p)
2467 2467 for p in opts.get('include', []):
2468 2468 matchargs.append('i:' + p)
2469 2469 for p in opts.get('exclude', []):
2470 2470 matchargs.append('x:' + p)
2471 2471 matchargs = ','.join(('%r' % p) for p in matchargs)
2472 2472 opts['_matchfiles'] = matchargs
2473 2473 if follow:
2474 2474 opts[fnopats[0][followfirst]] = '.'
2475 2475 else:
2476 2476 if follow:
2477 2477 if pats:
2478 2478 # follow() revset interprets its file argument as a
2479 2479 # manifest entry, so use match.files(), not pats.
2480 2480 opts[fpats[followfirst]] = list(match.files())
2481 2481 else:
2482 2482 op = fnopats[followdescendants][followfirst]
2483 2483 opts[op] = 'rev(%d)' % startrev
2484 2484 else:
2485 2485 opts['_patslog'] = list(pats)
2486 2486
2487 2487 filematcher = None
2488 2488 if opts.get('patch') or opts.get('stat'):
2489 2489 # When following files, track renames via a special matcher.
2490 2490 # If we're forced to take the slowpath it means we're following
2491 2491 # at least one pattern/directory, so don't bother with rename tracking.
2492 2492 if follow and not match.always() and not slowpath:
2493 2493 # _makefollowlogfilematcher expects its files argument to be
2494 2494 # relative to the repo root, so use match.files(), not pats.
2495 2495 filematcher = _makefollowlogfilematcher(repo, match.files(),
2496 2496 followfirst)
2497 2497 else:
2498 2498 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
2499 2499 if filematcher is None:
2500 2500 filematcher = lambda rev: match
2501 2501
2502 2502 expr = []
2503 2503 for op, val in sorted(opts.iteritems()):
2504 2504 if not val:
2505 2505 continue
2506 2506 if op not in opt2revset:
2507 2507 continue
2508 2508 revop, andor = opt2revset[op]
2509 2509 if '%(val)' not in revop:
2510 2510 expr.append(revop)
2511 2511 else:
2512 2512 if not isinstance(val, list):
2513 2513 e = revop % {'val': val}
2514 2514 else:
2515 2515 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
2516 2516 expr.append(e)
2517 2517
2518 2518 if expr:
2519 2519 expr = '(' + ' and '.join(expr) + ')'
2520 2520 else:
2521 2521 expr = None
2522 2522 return expr, filematcher
2523 2523
2524 2524 def _logrevs(repo, opts):
2525 2525 # Default --rev value depends on --follow but --follow behavior
2526 2526 # depends on revisions resolved from --rev...
2527 2527 follow = opts.get('follow') or opts.get('follow_first')
2528 2528 if opts.get('rev'):
2529 2529 revs = scmutil.revrange(repo, opts['rev'])
2530 2530 elif follow and repo.dirstate.p1() == nullid:
2531 2531 revs = smartset.baseset()
2532 2532 elif follow:
2533 2533 revs = repo.revs('reverse(:.)')
2534 2534 else:
2535 2535 revs = smartset.spanset(repo)
2536 2536 revs.reverse()
2537 2537 return revs
2538 2538
2539 2539 def getgraphlogrevs(repo, pats, opts):
2540 2540 """Return (revs, expr, filematcher) where revs is an iterable of
2541 2541 revision numbers, expr is a revset string built from log options
2542 2542 and file patterns or None, and used to filter 'revs'. If --stat or
2543 2543 --patch are not passed filematcher is None. Otherwise it is a
2544 2544 callable taking a revision number and returning a match objects
2545 2545 filtering the files to be detailed when displaying the revision.
2546 2546 """
2547 2547 limit = loglimit(opts)
2548 2548 revs = _logrevs(repo, opts)
2549 2549 if not revs:
2550 2550 return smartset.baseset(), None, None
2551 2551 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2552 2552 if opts.get('rev'):
2553 2553 # User-specified revs might be unsorted, but don't sort before
2554 2554 # _makelogrevset because it might depend on the order of revs
2555 2555 if not (revs.isdescending() or revs.istopo()):
2556 2556 revs.sort(reverse=True)
2557 2557 if expr:
2558 2558 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2559 2559 revs = matcher(repo, revs)
2560 2560 if limit is not None:
2561 2561 limitedrevs = []
2562 2562 for idx, rev in enumerate(revs):
2563 2563 if idx >= limit:
2564 2564 break
2565 2565 limitedrevs.append(rev)
2566 2566 revs = smartset.baseset(limitedrevs)
2567 2567
2568 2568 return revs, expr, filematcher
2569 2569
2570 2570 def getlogrevs(repo, pats, opts):
2571 2571 """Return (revs, expr, filematcher) where revs is an iterable of
2572 2572 revision numbers, expr is a revset string built from log options
2573 2573 and file patterns or None, and used to filter 'revs'. If --stat or
2574 2574 --patch are not passed filematcher is None. Otherwise it is a
2575 2575 callable taking a revision number and returning a match objects
2576 2576 filtering the files to be detailed when displaying the revision.
2577 2577 """
2578 2578 limit = loglimit(opts)
2579 2579 revs = _logrevs(repo, opts)
2580 2580 if not revs:
2581 2581 return smartset.baseset([]), None, None
2582 2582 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2583 2583 if expr:
2584 2584 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2585 2585 revs = matcher(repo, revs)
2586 2586 if limit is not None:
2587 2587 limitedrevs = []
2588 2588 for idx, r in enumerate(revs):
2589 2589 if limit <= idx:
2590 2590 break
2591 2591 limitedrevs.append(r)
2592 2592 revs = smartset.baseset(limitedrevs)
2593 2593
2594 2594 return revs, expr, filematcher
2595 2595
2596 2596 def _graphnodeformatter(ui, displayer):
2597 2597 spec = ui.config('ui', 'graphnodetemplate')
2598 2598 if not spec:
2599 2599 return templatekw.showgraphnode # fast path for "{graphnode}"
2600 2600
2601 2601 spec = templater.unquotestring(spec)
2602 2602 templ = formatter.maketemplater(ui, spec)
2603 2603 cache = {}
2604 2604 if isinstance(displayer, changeset_templater):
2605 2605 cache = displayer.cache # reuse cache of slow templates
2606 2606 props = templatekw.keywords.copy()
2607 2607 props['templ'] = templ
2608 2608 props['cache'] = cache
2609 2609 def formatnode(repo, ctx):
2610 2610 props['ctx'] = ctx
2611 2611 props['repo'] = repo
2612 2612 props['ui'] = repo.ui
2613 2613 props['revcache'] = {}
2614 2614 return templ.render(props)
2615 2615 return formatnode
2616 2616
2617 2617 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
2618 2618 filematcher=None):
2619 2619 formatnode = _graphnodeformatter(ui, displayer)
2620 2620 state = graphmod.asciistate()
2621 2621 styles = state['styles']
2622 2622
2623 2623 # only set graph styling if HGPLAIN is not set.
2624 2624 if ui.plain('graph'):
2625 2625 # set all edge styles to |, the default pre-3.8 behaviour
2626 2626 styles.update(dict.fromkeys(styles, '|'))
2627 2627 else:
2628 2628 edgetypes = {
2629 2629 'parent': graphmod.PARENT,
2630 2630 'grandparent': graphmod.GRANDPARENT,
2631 2631 'missing': graphmod.MISSINGPARENT
2632 2632 }
2633 2633 for name, key in edgetypes.items():
2634 2634 # experimental config: experimental.graphstyle.*
2635 2635 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
2636 2636 styles[key])
2637 2637 if not styles[key]:
2638 2638 styles[key] = None
2639 2639
2640 2640 # experimental config: experimental.graphshorten
2641 2641 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
2642 2642
2643 2643 for rev, type, ctx, parents in dag:
2644 2644 char = formatnode(repo, ctx)
2645 2645 copies = None
2646 2646 if getrenamed and ctx.rev():
2647 2647 copies = []
2648 2648 for fn in ctx.files():
2649 2649 rename = getrenamed(fn, ctx.rev())
2650 2650 if rename:
2651 2651 copies.append((fn, rename[0]))
2652 2652 revmatchfn = None
2653 2653 if filematcher is not None:
2654 2654 revmatchfn = filematcher(ctx.rev())
2655 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
2655 edges = edgefn(type, char, state, rev, parents)
2656 firstedge = next(edges)
2657 width = firstedge[2]
2658 displayer.show(ctx, copies=copies, matchfn=revmatchfn,
2659 _graphwidth=width)
2656 2660 lines = displayer.hunk.pop(rev).split('\n')
2657 2661 if not lines[-1]:
2658 2662 del lines[-1]
2659 2663 displayer.flush(ctx)
2660 edges = edgefn(type, char, lines, state, rev, parents)
2661 for type, char, lines, coldata in edges:
2664 for type, char, width, coldata in itertools.chain([firstedge], edges):
2662 2665 graphmod.ascii(ui, state, type, char, lines, coldata)
2666 lines = []
2663 2667 displayer.close()
2664 2668
2665 2669 def graphlog(ui, repo, pats, opts):
2666 2670 # Parameters are identical to log command ones
2667 2671 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
2668 2672 revdag = graphmod.dagwalker(repo, revs)
2669 2673
2670 2674 getrenamed = None
2671 2675 if opts.get('copies'):
2672 2676 endrev = None
2673 2677 if opts.get('rev'):
2674 2678 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
2675 2679 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
2676 2680
2677 2681 ui.pager('log')
2678 2682 displayer = show_changeset(ui, repo, opts, buffered=True)
2679 2683 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
2680 2684 filematcher)
2681 2685
2682 2686 def checkunsupportedgraphflags(pats, opts):
2683 2687 for op in ["newest_first"]:
2684 2688 if op in opts and opts[op]:
2685 2689 raise error.Abort(_("-G/--graph option is incompatible with --%s")
2686 2690 % op.replace("_", "-"))
2687 2691
2688 2692 def graphrevs(repo, nodes, opts):
2689 2693 limit = loglimit(opts)
2690 2694 nodes.reverse()
2691 2695 if limit is not None:
2692 2696 nodes = nodes[:limit]
2693 2697 return graphmod.nodes(repo, nodes)
2694 2698
2695 2699 def add(ui, repo, match, prefix, explicitonly, **opts):
2696 2700 join = lambda f: os.path.join(prefix, f)
2697 2701 bad = []
2698 2702
2699 2703 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2700 2704 names = []
2701 2705 wctx = repo[None]
2702 2706 cca = None
2703 2707 abort, warn = scmutil.checkportabilityalert(ui)
2704 2708 if abort or warn:
2705 2709 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2706 2710
2707 2711 badmatch = matchmod.badmatch(match, badfn)
2708 2712 dirstate = repo.dirstate
2709 2713 # We don't want to just call wctx.walk here, since it would return a lot of
2710 2714 # clean files, which we aren't interested in and takes time.
2711 2715 for f in sorted(dirstate.walk(badmatch, sorted(wctx.substate),
2712 2716 True, False, full=False)):
2713 2717 exact = match.exact(f)
2714 2718 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2715 2719 if cca:
2716 2720 cca(f)
2717 2721 names.append(f)
2718 2722 if ui.verbose or not exact:
2719 2723 ui.status(_('adding %s\n') % match.rel(f))
2720 2724
2721 2725 for subpath in sorted(wctx.substate):
2722 2726 sub = wctx.sub(subpath)
2723 2727 try:
2724 2728 submatch = matchmod.subdirmatcher(subpath, match)
2725 2729 if opts.get(r'subrepos'):
2726 2730 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
2727 2731 else:
2728 2732 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
2729 2733 except error.LookupError:
2730 2734 ui.status(_("skipping missing subrepository: %s\n")
2731 2735 % join(subpath))
2732 2736
2733 2737 if not opts.get(r'dry_run'):
2734 2738 rejected = wctx.add(names, prefix)
2735 2739 bad.extend(f for f in rejected if f in match.files())
2736 2740 return bad
2737 2741
2738 2742 def addwebdirpath(repo, serverpath, webconf):
2739 2743 webconf[serverpath] = repo.root
2740 2744 repo.ui.debug('adding %s = %s\n' % (serverpath, repo.root))
2741 2745
2742 2746 for r in repo.revs('filelog("path:.hgsub")'):
2743 2747 ctx = repo[r]
2744 2748 for subpath in ctx.substate:
2745 2749 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2746 2750
2747 2751 def forget(ui, repo, match, prefix, explicitonly):
2748 2752 join = lambda f: os.path.join(prefix, f)
2749 2753 bad = []
2750 2754 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2751 2755 wctx = repo[None]
2752 2756 forgot = []
2753 2757
2754 2758 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2755 2759 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2756 2760 if explicitonly:
2757 2761 forget = [f for f in forget if match.exact(f)]
2758 2762
2759 2763 for subpath in sorted(wctx.substate):
2760 2764 sub = wctx.sub(subpath)
2761 2765 try:
2762 2766 submatch = matchmod.subdirmatcher(subpath, match)
2763 2767 subbad, subforgot = sub.forget(submatch, prefix)
2764 2768 bad.extend([subpath + '/' + f for f in subbad])
2765 2769 forgot.extend([subpath + '/' + f for f in subforgot])
2766 2770 except error.LookupError:
2767 2771 ui.status(_("skipping missing subrepository: %s\n")
2768 2772 % join(subpath))
2769 2773
2770 2774 if not explicitonly:
2771 2775 for f in match.files():
2772 2776 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2773 2777 if f not in forgot:
2774 2778 if repo.wvfs.exists(f):
2775 2779 # Don't complain if the exact case match wasn't given.
2776 2780 # But don't do this until after checking 'forgot', so
2777 2781 # that subrepo files aren't normalized, and this op is
2778 2782 # purely from data cached by the status walk above.
2779 2783 if repo.dirstate.normalize(f) in repo.dirstate:
2780 2784 continue
2781 2785 ui.warn(_('not removing %s: '
2782 2786 'file is already untracked\n')
2783 2787 % match.rel(f))
2784 2788 bad.append(f)
2785 2789
2786 2790 for f in forget:
2787 2791 if ui.verbose or not match.exact(f):
2788 2792 ui.status(_('removing %s\n') % match.rel(f))
2789 2793
2790 2794 rejected = wctx.forget(forget, prefix)
2791 2795 bad.extend(f for f in rejected if f in match.files())
2792 2796 forgot.extend(f for f in forget if f not in rejected)
2793 2797 return bad, forgot
2794 2798
2795 2799 def files(ui, ctx, m, fm, fmt, subrepos):
2796 2800 rev = ctx.rev()
2797 2801 ret = 1
2798 2802 ds = ctx.repo().dirstate
2799 2803
2800 2804 for f in ctx.matches(m):
2801 2805 if rev is None and ds[f] == 'r':
2802 2806 continue
2803 2807 fm.startitem()
2804 2808 if ui.verbose:
2805 2809 fc = ctx[f]
2806 2810 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2807 2811 fm.data(abspath=f)
2808 2812 fm.write('path', fmt, m.rel(f))
2809 2813 ret = 0
2810 2814
2811 2815 for subpath in sorted(ctx.substate):
2812 2816 submatch = matchmod.subdirmatcher(subpath, m)
2813 2817 if (subrepos or m.exact(subpath) or any(submatch.files())):
2814 2818 sub = ctx.sub(subpath)
2815 2819 try:
2816 2820 recurse = m.exact(subpath) or subrepos
2817 2821 if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
2818 2822 ret = 0
2819 2823 except error.LookupError:
2820 2824 ui.status(_("skipping missing subrepository: %s\n")
2821 2825 % m.abs(subpath))
2822 2826
2823 2827 return ret
2824 2828
2825 2829 def remove(ui, repo, m, prefix, after, force, subrepos, warnings=None):
2826 2830 join = lambda f: os.path.join(prefix, f)
2827 2831 ret = 0
2828 2832 s = repo.status(match=m, clean=True)
2829 2833 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2830 2834
2831 2835 wctx = repo[None]
2832 2836
2833 2837 if warnings is None:
2834 2838 warnings = []
2835 2839 warn = True
2836 2840 else:
2837 2841 warn = False
2838 2842
2839 2843 subs = sorted(wctx.substate)
2840 2844 total = len(subs)
2841 2845 count = 0
2842 2846 for subpath in subs:
2843 2847 count += 1
2844 2848 submatch = matchmod.subdirmatcher(subpath, m)
2845 2849 if subrepos or m.exact(subpath) or any(submatch.files()):
2846 2850 ui.progress(_('searching'), count, total=total, unit=_('subrepos'))
2847 2851 sub = wctx.sub(subpath)
2848 2852 try:
2849 2853 if sub.removefiles(submatch, prefix, after, force, subrepos,
2850 2854 warnings):
2851 2855 ret = 1
2852 2856 except error.LookupError:
2853 2857 warnings.append(_("skipping missing subrepository: %s\n")
2854 2858 % join(subpath))
2855 2859 ui.progress(_('searching'), None)
2856 2860
2857 2861 # warn about failure to delete explicit files/dirs
2858 2862 deleteddirs = util.dirs(deleted)
2859 2863 files = m.files()
2860 2864 total = len(files)
2861 2865 count = 0
2862 2866 for f in files:
2863 2867 def insubrepo():
2864 2868 for subpath in wctx.substate:
2865 2869 if f.startswith(subpath + '/'):
2866 2870 return True
2867 2871 return False
2868 2872
2869 2873 count += 1
2870 2874 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2871 2875 isdir = f in deleteddirs or wctx.hasdir(f)
2872 2876 if (f in repo.dirstate or isdir or f == '.'
2873 2877 or insubrepo() or f in subs):
2874 2878 continue
2875 2879
2876 2880 if repo.wvfs.exists(f):
2877 2881 if repo.wvfs.isdir(f):
2878 2882 warnings.append(_('not removing %s: no tracked files\n')
2879 2883 % m.rel(f))
2880 2884 else:
2881 2885 warnings.append(_('not removing %s: file is untracked\n')
2882 2886 % m.rel(f))
2883 2887 # missing files will generate a warning elsewhere
2884 2888 ret = 1
2885 2889 ui.progress(_('deleting'), None)
2886 2890
2887 2891 if force:
2888 2892 list = modified + deleted + clean + added
2889 2893 elif after:
2890 2894 list = deleted
2891 2895 remaining = modified + added + clean
2892 2896 total = len(remaining)
2893 2897 count = 0
2894 2898 for f in remaining:
2895 2899 count += 1
2896 2900 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2897 2901 warnings.append(_('not removing %s: file still exists\n')
2898 2902 % m.rel(f))
2899 2903 ret = 1
2900 2904 ui.progress(_('skipping'), None)
2901 2905 else:
2902 2906 list = deleted + clean
2903 2907 total = len(modified) + len(added)
2904 2908 count = 0
2905 2909 for f in modified:
2906 2910 count += 1
2907 2911 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2908 2912 warnings.append(_('not removing %s: file is modified (use -f'
2909 2913 ' to force removal)\n') % m.rel(f))
2910 2914 ret = 1
2911 2915 for f in added:
2912 2916 count += 1
2913 2917 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2914 2918 warnings.append(_("not removing %s: file has been marked for add"
2915 2919 " (use 'hg forget' to undo add)\n") % m.rel(f))
2916 2920 ret = 1
2917 2921 ui.progress(_('skipping'), None)
2918 2922
2919 2923 list = sorted(list)
2920 2924 total = len(list)
2921 2925 count = 0
2922 2926 for f in list:
2923 2927 count += 1
2924 2928 if ui.verbose or not m.exact(f):
2925 2929 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2926 2930 ui.status(_('removing %s\n') % m.rel(f))
2927 2931 ui.progress(_('deleting'), None)
2928 2932
2929 2933 with repo.wlock():
2930 2934 if not after:
2931 2935 for f in list:
2932 2936 if f in added:
2933 2937 continue # we never unlink added files on remove
2934 2938 repo.wvfs.unlinkpath(f, ignoremissing=True)
2935 2939 repo[None].forget(list)
2936 2940
2937 2941 if warn:
2938 2942 for warning in warnings:
2939 2943 ui.warn(warning)
2940 2944
2941 2945 return ret
2942 2946
2943 2947 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2944 2948 err = 1
2945 2949
2946 2950 def write(path):
2947 2951 filename = None
2948 2952 if fntemplate:
2949 2953 filename = makefilename(repo, fntemplate, ctx.node(),
2950 2954 pathname=os.path.join(prefix, path))
2951 2955 with formatter.maybereopen(basefm, filename, opts) as fm:
2952 2956 data = ctx[path].data()
2953 2957 if opts.get('decode'):
2954 2958 data = repo.wwritedata(path, data)
2955 2959 fm.startitem()
2956 2960 fm.write('data', '%s', data)
2957 2961 fm.data(abspath=path, path=matcher.rel(path))
2958 2962
2959 2963 # Automation often uses hg cat on single files, so special case it
2960 2964 # for performance to avoid the cost of parsing the manifest.
2961 2965 if len(matcher.files()) == 1 and not matcher.anypats():
2962 2966 file = matcher.files()[0]
2963 2967 mfl = repo.manifestlog
2964 2968 mfnode = ctx.manifestnode()
2965 2969 try:
2966 2970 if mfnode and mfl[mfnode].find(file)[0]:
2967 2971 write(file)
2968 2972 return 0
2969 2973 except KeyError:
2970 2974 pass
2971 2975
2972 2976 for abs in ctx.walk(matcher):
2973 2977 write(abs)
2974 2978 err = 0
2975 2979
2976 2980 for subpath in sorted(ctx.substate):
2977 2981 sub = ctx.sub(subpath)
2978 2982 try:
2979 2983 submatch = matchmod.subdirmatcher(subpath, matcher)
2980 2984
2981 2985 if not sub.cat(submatch, basefm, fntemplate,
2982 2986 os.path.join(prefix, sub._path), **opts):
2983 2987 err = 0
2984 2988 except error.RepoLookupError:
2985 2989 ui.status(_("skipping missing subrepository: %s\n")
2986 2990 % os.path.join(prefix, subpath))
2987 2991
2988 2992 return err
2989 2993
2990 2994 def commit(ui, repo, commitfunc, pats, opts):
2991 2995 '''commit the specified files or all outstanding changes'''
2992 2996 date = opts.get('date')
2993 2997 if date:
2994 2998 opts['date'] = util.parsedate(date)
2995 2999 message = logmessage(ui, opts)
2996 3000 matcher = scmutil.match(repo[None], pats, opts)
2997 3001
2998 3002 dsguard = None
2999 3003 # extract addremove carefully -- this function can be called from a command
3000 3004 # that doesn't support addremove
3001 3005 if opts.get('addremove'):
3002 3006 dsguard = dirstateguard.dirstateguard(repo, 'commit')
3003 3007 with dsguard or util.nullcontextmanager():
3004 3008 if dsguard:
3005 3009 if scmutil.addremove(repo, matcher, "", opts) != 0:
3006 3010 raise error.Abort(
3007 3011 _("failed to mark all new/missing files as added/removed"))
3008 3012
3009 3013 return commitfunc(ui, repo, message, matcher, opts)
3010 3014
3011 3015 def samefile(f, ctx1, ctx2):
3012 3016 if f in ctx1.manifest():
3013 3017 a = ctx1.filectx(f)
3014 3018 if f in ctx2.manifest():
3015 3019 b = ctx2.filectx(f)
3016 3020 return (not a.cmp(b)
3017 3021 and a.flags() == b.flags())
3018 3022 else:
3019 3023 return False
3020 3024 else:
3021 3025 return f not in ctx2.manifest()
3022 3026
3023 3027 def amend(ui, repo, commitfunc, old, extra, pats, opts):
3024 3028 # avoid cycle context -> subrepo -> cmdutil
3025 3029 from . import context
3026 3030
3027 3031 # amend will reuse the existing user if not specified, but the obsolete
3028 3032 # marker creation requires that the current user's name is specified.
3029 3033 if obsolete.isenabled(repo, obsolete.createmarkersopt):
3030 3034 ui.username() # raise exception if username not set
3031 3035
3032 3036 ui.note(_('amending changeset %s\n') % old)
3033 3037 base = old.p1()
3034 3038
3035 3039 newid = None
3036 3040 with repo.wlock(), repo.lock(), repo.transaction('amend'):
3037 3041 # See if we got a message from -m or -l, if not, open the editor
3038 3042 # with the message of the changeset to amend
3039 3043 message = logmessage(ui, opts)
3040 3044 # ensure logfile does not conflict with later enforcement of the
3041 3045 # message. potential logfile content has been processed by
3042 3046 # `logmessage` anyway.
3043 3047 opts.pop('logfile')
3044 3048 # First, do a regular commit to record all changes in the working
3045 3049 # directory (if there are any)
3046 3050 ui.callhooks = False
3047 3051 activebookmark = repo._bookmarks.active
3048 3052 try:
3049 3053 repo._bookmarks.active = None
3050 3054 opts['message'] = 'temporary amend commit for %s' % old
3051 3055 node = commit(ui, repo, commitfunc, pats, opts)
3052 3056 finally:
3053 3057 repo._bookmarks.active = activebookmark
3054 3058 ui.callhooks = True
3055 3059 ctx = repo[node]
3056 3060
3057 3061 # Participating changesets:
3058 3062 #
3059 3063 # node/ctx o - new (intermediate) commit that contains changes
3060 3064 # | from working dir to go into amending commit
3061 3065 # | (or a workingctx if there were no changes)
3062 3066 # |
3063 3067 # old o - changeset to amend
3064 3068 # |
3065 3069 # base o - parent of amending changeset
3066 3070
3067 3071 # Update extra dict from amended commit (e.g. to preserve graft
3068 3072 # source)
3069 3073 extra.update(old.extra())
3070 3074
3071 3075 # Also update it from the intermediate commit or from the wctx
3072 3076 extra.update(ctx.extra())
3073 3077
3074 3078 if len(old.parents()) > 1:
3075 3079 # ctx.files() isn't reliable for merges, so fall back to the
3076 3080 # slower repo.status() method
3077 3081 files = set([fn for st in repo.status(base, old)[:3]
3078 3082 for fn in st])
3079 3083 else:
3080 3084 files = set(old.files())
3081 3085
3082 3086 # Second, we use either the commit we just did, or if there were no
3083 3087 # changes the parent of the working directory as the version of the
3084 3088 # files in the final amend commit
3085 3089 if node:
3086 3090 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
3087 3091
3088 3092 user = ctx.user()
3089 3093 date = ctx.date()
3090 3094 # Recompute copies (avoid recording a -> b -> a)
3091 3095 copied = copies.pathcopies(base, ctx)
3092 3096 if old.p2:
3093 3097 copied.update(copies.pathcopies(old.p2(), ctx))
3094 3098
3095 3099 # Prune files which were reverted by the updates: if old
3096 3100 # introduced file X and our intermediate commit, node,
3097 3101 # renamed that file, then those two files are the same and
3098 3102 # we can discard X from our list of files. Likewise if X
3099 3103 # was deleted, it's no longer relevant
3100 3104 files.update(ctx.files())
3101 3105 files = [f for f in files if not samefile(f, ctx, base)]
3102 3106
3103 3107 def filectxfn(repo, ctx_, path):
3104 3108 try:
3105 3109 fctx = ctx[path]
3106 3110 flags = fctx.flags()
3107 3111 mctx = context.memfilectx(repo,
3108 3112 fctx.path(), fctx.data(),
3109 3113 islink='l' in flags,
3110 3114 isexec='x' in flags,
3111 3115 copied=copied.get(path))
3112 3116 return mctx
3113 3117 except KeyError:
3114 3118 return None
3115 3119 else:
3116 3120 ui.note(_('copying changeset %s to %s\n') % (old, base))
3117 3121
3118 3122 # Use version of files as in the old cset
3119 3123 def filectxfn(repo, ctx_, path):
3120 3124 try:
3121 3125 return old.filectx(path)
3122 3126 except KeyError:
3123 3127 return None
3124 3128
3125 3129 user = opts.get('user') or old.user()
3126 3130 date = opts.get('date') or old.date()
3127 3131 editform = mergeeditform(old, 'commit.amend')
3128 3132 editor = getcommiteditor(editform=editform,
3129 3133 **pycompat.strkwargs(opts))
3130 3134 if not message:
3131 3135 editor = getcommiteditor(edit=True, editform=editform)
3132 3136 message = old.description()
3133 3137
3134 3138 pureextra = extra.copy()
3135 3139 extra['amend_source'] = old.hex()
3136 3140
3137 3141 new = context.memctx(repo,
3138 3142 parents=[base.node(), old.p2().node()],
3139 3143 text=message,
3140 3144 files=files,
3141 3145 filectxfn=filectxfn,
3142 3146 user=user,
3143 3147 date=date,
3144 3148 extra=extra,
3145 3149 editor=editor)
3146 3150
3147 3151 newdesc = changelog.stripdesc(new.description())
3148 3152 if ((not node)
3149 3153 and newdesc == old.description()
3150 3154 and user == old.user()
3151 3155 and date == old.date()
3152 3156 and pureextra == old.extra()):
3153 3157 # nothing changed. continuing here would create a new node
3154 3158 # anyway because of the amend_source noise.
3155 3159 #
3156 3160 # This not what we expect from amend.
3157 3161 return old.node()
3158 3162
3159 3163 ph = repo.ui.config('phases', 'new-commit', phases.draft)
3160 3164 try:
3161 3165 if opts.get('secret'):
3162 3166 commitphase = 'secret'
3163 3167 else:
3164 3168 commitphase = old.phase()
3165 3169 repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
3166 3170 newid = repo.commitctx(new)
3167 3171 finally:
3168 3172 repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
3169 3173 if newid != old.node():
3170 3174 # Reroute the working copy parent to the new changeset
3171 3175 repo.setparents(newid, nullid)
3172 3176 mapping = {old.node(): (newid,)}
3173 3177 if node:
3174 3178 mapping[node] = ()
3175 3179 scmutil.cleanupnodes(repo, mapping, 'amend')
3176 3180 return newid
3177 3181
3178 3182 def commiteditor(repo, ctx, subs, editform=''):
3179 3183 if ctx.description():
3180 3184 return ctx.description()
3181 3185 return commitforceeditor(repo, ctx, subs, editform=editform,
3182 3186 unchangedmessagedetection=True)
3183 3187
3184 3188 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
3185 3189 editform='', unchangedmessagedetection=False):
3186 3190 if not extramsg:
3187 3191 extramsg = _("Leave message empty to abort commit.")
3188 3192
3189 3193 forms = [e for e in editform.split('.') if e]
3190 3194 forms.insert(0, 'changeset')
3191 3195 templatetext = None
3192 3196 while forms:
3193 3197 ref = '.'.join(forms)
3194 3198 if repo.ui.config('committemplate', ref):
3195 3199 templatetext = committext = buildcommittemplate(
3196 3200 repo, ctx, subs, extramsg, ref)
3197 3201 break
3198 3202 forms.pop()
3199 3203 else:
3200 3204 committext = buildcommittext(repo, ctx, subs, extramsg)
3201 3205
3202 3206 # run editor in the repository root
3203 3207 olddir = pycompat.getcwd()
3204 3208 os.chdir(repo.root)
3205 3209
3206 3210 # make in-memory changes visible to external process
3207 3211 tr = repo.currenttransaction()
3208 3212 repo.dirstate.write(tr)
3209 3213 pending = tr and tr.writepending() and repo.root
3210 3214
3211 3215 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
3212 3216 editform=editform, pending=pending,
3213 3217 repopath=repo.path)
3214 3218 text = editortext
3215 3219
3216 3220 # strip away anything below this special string (used for editors that want
3217 3221 # to display the diff)
3218 3222 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
3219 3223 if stripbelow:
3220 3224 text = text[:stripbelow.start()]
3221 3225
3222 3226 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
3223 3227 os.chdir(olddir)
3224 3228
3225 3229 if finishdesc:
3226 3230 text = finishdesc(text)
3227 3231 if not text.strip():
3228 3232 raise error.Abort(_("empty commit message"))
3229 3233 if unchangedmessagedetection and editortext == templatetext:
3230 3234 raise error.Abort(_("commit message unchanged"))
3231 3235
3232 3236 return text
3233 3237
3234 3238 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
3235 3239 ui = repo.ui
3236 3240 spec = formatter.templatespec(ref, None, None)
3237 3241 t = changeset_templater(ui, repo, spec, None, {}, False)
3238 3242 t.t.cache.update((k, templater.unquotestring(v))
3239 3243 for k, v in repo.ui.configitems('committemplate'))
3240 3244
3241 3245 if not extramsg:
3242 3246 extramsg = '' # ensure that extramsg is string
3243 3247
3244 3248 ui.pushbuffer()
3245 3249 t.show(ctx, extramsg=extramsg)
3246 3250 return ui.popbuffer()
3247 3251
3248 3252 def hgprefix(msg):
3249 3253 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
3250 3254
3251 3255 def buildcommittext(repo, ctx, subs, extramsg):
3252 3256 edittext = []
3253 3257 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
3254 3258 if ctx.description():
3255 3259 edittext.append(ctx.description())
3256 3260 edittext.append("")
3257 3261 edittext.append("") # Empty line between message and comments.
3258 3262 edittext.append(hgprefix(_("Enter commit message."
3259 3263 " Lines beginning with 'HG:' are removed.")))
3260 3264 edittext.append(hgprefix(extramsg))
3261 3265 edittext.append("HG: --")
3262 3266 edittext.append(hgprefix(_("user: %s") % ctx.user()))
3263 3267 if ctx.p2():
3264 3268 edittext.append(hgprefix(_("branch merge")))
3265 3269 if ctx.branch():
3266 3270 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
3267 3271 if bookmarks.isactivewdirparent(repo):
3268 3272 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
3269 3273 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
3270 3274 edittext.extend([hgprefix(_("added %s") % f) for f in added])
3271 3275 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
3272 3276 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
3273 3277 if not added and not modified and not removed:
3274 3278 edittext.append(hgprefix(_("no files changed")))
3275 3279 edittext.append("")
3276 3280
3277 3281 return "\n".join(edittext)
3278 3282
3279 3283 def commitstatus(repo, node, branch, bheads=None, opts=None):
3280 3284 if opts is None:
3281 3285 opts = {}
3282 3286 ctx = repo[node]
3283 3287 parents = ctx.parents()
3284 3288
3285 3289 if (not opts.get('amend') and bheads and node not in bheads and not
3286 3290 [x for x in parents if x.node() in bheads and x.branch() == branch]):
3287 3291 repo.ui.status(_('created new head\n'))
3288 3292 # The message is not printed for initial roots. For the other
3289 3293 # changesets, it is printed in the following situations:
3290 3294 #
3291 3295 # Par column: for the 2 parents with ...
3292 3296 # N: null or no parent
3293 3297 # B: parent is on another named branch
3294 3298 # C: parent is a regular non head changeset
3295 3299 # H: parent was a branch head of the current branch
3296 3300 # Msg column: whether we print "created new head" message
3297 3301 # In the following, it is assumed that there already exists some
3298 3302 # initial branch heads of the current branch, otherwise nothing is
3299 3303 # printed anyway.
3300 3304 #
3301 3305 # Par Msg Comment
3302 3306 # N N y additional topo root
3303 3307 #
3304 3308 # B N y additional branch root
3305 3309 # C N y additional topo head
3306 3310 # H N n usual case
3307 3311 #
3308 3312 # B B y weird additional branch root
3309 3313 # C B y branch merge
3310 3314 # H B n merge with named branch
3311 3315 #
3312 3316 # C C y additional head from merge
3313 3317 # C H n merge with a head
3314 3318 #
3315 3319 # H H n head merge: head count decreases
3316 3320
3317 3321 if not opts.get('close_branch'):
3318 3322 for r in parents:
3319 3323 if r.closesbranch() and r.branch() == branch:
3320 3324 repo.ui.status(_('reopening closed branch head %d\n') % r)
3321 3325
3322 3326 if repo.ui.debugflag:
3323 3327 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
3324 3328 elif repo.ui.verbose:
3325 3329 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
3326 3330
3327 3331 def postcommitstatus(repo, pats, opts):
3328 3332 return repo.status(match=scmutil.match(repo[None], pats, opts))
3329 3333
3330 3334 def revert(ui, repo, ctx, parents, *pats, **opts):
3331 3335 parent, p2 = parents
3332 3336 node = ctx.node()
3333 3337
3334 3338 mf = ctx.manifest()
3335 3339 if node == p2:
3336 3340 parent = p2
3337 3341
3338 3342 # need all matching names in dirstate and manifest of target rev,
3339 3343 # so have to walk both. do not print errors if files exist in one
3340 3344 # but not other. in both cases, filesets should be evaluated against
3341 3345 # workingctx to get consistent result (issue4497). this means 'set:**'
3342 3346 # cannot be used to select missing files from target rev.
3343 3347
3344 3348 # `names` is a mapping for all elements in working copy and target revision
3345 3349 # The mapping is in the form:
3346 3350 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
3347 3351 names = {}
3348 3352
3349 3353 with repo.wlock():
3350 3354 ## filling of the `names` mapping
3351 3355 # walk dirstate to fill `names`
3352 3356
3353 3357 interactive = opts.get('interactive', False)
3354 3358 wctx = repo[None]
3355 3359 m = scmutil.match(wctx, pats, opts)
3356 3360
3357 3361 # we'll need this later
3358 3362 targetsubs = sorted(s for s in wctx.substate if m(s))
3359 3363
3360 3364 if not m.always():
3361 3365 matcher = matchmod.badmatch(m, lambda x, y: False)
3362 3366 for abs in wctx.walk(matcher):
3363 3367 names[abs] = m.rel(abs), m.exact(abs)
3364 3368
3365 3369 # walk target manifest to fill `names`
3366 3370
3367 3371 def badfn(path, msg):
3368 3372 if path in names:
3369 3373 return
3370 3374 if path in ctx.substate:
3371 3375 return
3372 3376 path_ = path + '/'
3373 3377 for f in names:
3374 3378 if f.startswith(path_):
3375 3379 return
3376 3380 ui.warn("%s: %s\n" % (m.rel(path), msg))
3377 3381
3378 3382 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
3379 3383 if abs not in names:
3380 3384 names[abs] = m.rel(abs), m.exact(abs)
3381 3385
3382 3386 # Find status of all file in `names`.
3383 3387 m = scmutil.matchfiles(repo, names)
3384 3388
3385 3389 changes = repo.status(node1=node, match=m,
3386 3390 unknown=True, ignored=True, clean=True)
3387 3391 else:
3388 3392 changes = repo.status(node1=node, match=m)
3389 3393 for kind in changes:
3390 3394 for abs in kind:
3391 3395 names[abs] = m.rel(abs), m.exact(abs)
3392 3396
3393 3397 m = scmutil.matchfiles(repo, names)
3394 3398
3395 3399 modified = set(changes.modified)
3396 3400 added = set(changes.added)
3397 3401 removed = set(changes.removed)
3398 3402 _deleted = set(changes.deleted)
3399 3403 unknown = set(changes.unknown)
3400 3404 unknown.update(changes.ignored)
3401 3405 clean = set(changes.clean)
3402 3406 modadded = set()
3403 3407
3404 3408 # We need to account for the state of the file in the dirstate,
3405 3409 # even when we revert against something else than parent. This will
3406 3410 # slightly alter the behavior of revert (doing back up or not, delete
3407 3411 # or just forget etc).
3408 3412 if parent == node:
3409 3413 dsmodified = modified
3410 3414 dsadded = added
3411 3415 dsremoved = removed
3412 3416 # store all local modifications, useful later for rename detection
3413 3417 localchanges = dsmodified | dsadded
3414 3418 modified, added, removed = set(), set(), set()
3415 3419 else:
3416 3420 changes = repo.status(node1=parent, match=m)
3417 3421 dsmodified = set(changes.modified)
3418 3422 dsadded = set(changes.added)
3419 3423 dsremoved = set(changes.removed)
3420 3424 # store all local modifications, useful later for rename detection
3421 3425 localchanges = dsmodified | dsadded
3422 3426
3423 3427 # only take into account for removes between wc and target
3424 3428 clean |= dsremoved - removed
3425 3429 dsremoved &= removed
3426 3430 # distinct between dirstate remove and other
3427 3431 removed -= dsremoved
3428 3432
3429 3433 modadded = added & dsmodified
3430 3434 added -= modadded
3431 3435
3432 3436 # tell newly modified apart.
3433 3437 dsmodified &= modified
3434 3438 dsmodified |= modified & dsadded # dirstate added may need backup
3435 3439 modified -= dsmodified
3436 3440
3437 3441 # We need to wait for some post-processing to update this set
3438 3442 # before making the distinction. The dirstate will be used for
3439 3443 # that purpose.
3440 3444 dsadded = added
3441 3445
3442 3446 # in case of merge, files that are actually added can be reported as
3443 3447 # modified, we need to post process the result
3444 3448 if p2 != nullid:
3445 3449 mergeadd = set(dsmodified)
3446 3450 for path in dsmodified:
3447 3451 if path in mf:
3448 3452 mergeadd.remove(path)
3449 3453 dsadded |= mergeadd
3450 3454 dsmodified -= mergeadd
3451 3455
3452 3456 # if f is a rename, update `names` to also revert the source
3453 3457 cwd = repo.getcwd()
3454 3458 for f in localchanges:
3455 3459 src = repo.dirstate.copied(f)
3456 3460 # XXX should we check for rename down to target node?
3457 3461 if src and src not in names and repo.dirstate[src] == 'r':
3458 3462 dsremoved.add(src)
3459 3463 names[src] = (repo.pathto(src, cwd), True)
3460 3464
3461 3465 # determine the exact nature of the deleted changesets
3462 3466 deladded = set(_deleted)
3463 3467 for path in _deleted:
3464 3468 if path in mf:
3465 3469 deladded.remove(path)
3466 3470 deleted = _deleted - deladded
3467 3471
3468 3472 # distinguish between file to forget and the other
3469 3473 added = set()
3470 3474 for abs in dsadded:
3471 3475 if repo.dirstate[abs] != 'a':
3472 3476 added.add(abs)
3473 3477 dsadded -= added
3474 3478
3475 3479 for abs in deladded:
3476 3480 if repo.dirstate[abs] == 'a':
3477 3481 dsadded.add(abs)
3478 3482 deladded -= dsadded
3479 3483
3480 3484 # For files marked as removed, we check if an unknown file is present at
3481 3485 # the same path. If a such file exists it may need to be backed up.
3482 3486 # Making the distinction at this stage helps have simpler backup
3483 3487 # logic.
3484 3488 removunk = set()
3485 3489 for abs in removed:
3486 3490 target = repo.wjoin(abs)
3487 3491 if os.path.lexists(target):
3488 3492 removunk.add(abs)
3489 3493 removed -= removunk
3490 3494
3491 3495 dsremovunk = set()
3492 3496 for abs in dsremoved:
3493 3497 target = repo.wjoin(abs)
3494 3498 if os.path.lexists(target):
3495 3499 dsremovunk.add(abs)
3496 3500 dsremoved -= dsremovunk
3497 3501
3498 3502 # action to be actually performed by revert
3499 3503 # (<list of file>, message>) tuple
3500 3504 actions = {'revert': ([], _('reverting %s\n')),
3501 3505 'add': ([], _('adding %s\n')),
3502 3506 'remove': ([], _('removing %s\n')),
3503 3507 'drop': ([], _('removing %s\n')),
3504 3508 'forget': ([], _('forgetting %s\n')),
3505 3509 'undelete': ([], _('undeleting %s\n')),
3506 3510 'noop': (None, _('no changes needed to %s\n')),
3507 3511 'unknown': (None, _('file not managed: %s\n')),
3508 3512 }
3509 3513
3510 3514 # "constant" that convey the backup strategy.
3511 3515 # All set to `discard` if `no-backup` is set do avoid checking
3512 3516 # no_backup lower in the code.
3513 3517 # These values are ordered for comparison purposes
3514 3518 backupinteractive = 3 # do backup if interactively modified
3515 3519 backup = 2 # unconditionally do backup
3516 3520 check = 1 # check if the existing file differs from target
3517 3521 discard = 0 # never do backup
3518 3522 if opts.get('no_backup'):
3519 3523 backupinteractive = backup = check = discard
3520 3524 if interactive:
3521 3525 dsmodifiedbackup = backupinteractive
3522 3526 else:
3523 3527 dsmodifiedbackup = backup
3524 3528 tobackup = set()
3525 3529
3526 3530 backupanddel = actions['remove']
3527 3531 if not opts.get('no_backup'):
3528 3532 backupanddel = actions['drop']
3529 3533
3530 3534 disptable = (
3531 3535 # dispatch table:
3532 3536 # file state
3533 3537 # action
3534 3538 # make backup
3535 3539
3536 3540 ## Sets that results that will change file on disk
3537 3541 # Modified compared to target, no local change
3538 3542 (modified, actions['revert'], discard),
3539 3543 # Modified compared to target, but local file is deleted
3540 3544 (deleted, actions['revert'], discard),
3541 3545 # Modified compared to target, local change
3542 3546 (dsmodified, actions['revert'], dsmodifiedbackup),
3543 3547 # Added since target
3544 3548 (added, actions['remove'], discard),
3545 3549 # Added in working directory
3546 3550 (dsadded, actions['forget'], discard),
3547 3551 # Added since target, have local modification
3548 3552 (modadded, backupanddel, backup),
3549 3553 # Added since target but file is missing in working directory
3550 3554 (deladded, actions['drop'], discard),
3551 3555 # Removed since target, before working copy parent
3552 3556 (removed, actions['add'], discard),
3553 3557 # Same as `removed` but an unknown file exists at the same path
3554 3558 (removunk, actions['add'], check),
3555 3559 # Removed since targe, marked as such in working copy parent
3556 3560 (dsremoved, actions['undelete'], discard),
3557 3561 # Same as `dsremoved` but an unknown file exists at the same path
3558 3562 (dsremovunk, actions['undelete'], check),
3559 3563 ## the following sets does not result in any file changes
3560 3564 # File with no modification
3561 3565 (clean, actions['noop'], discard),
3562 3566 # Existing file, not tracked anywhere
3563 3567 (unknown, actions['unknown'], discard),
3564 3568 )
3565 3569
3566 3570 for abs, (rel, exact) in sorted(names.items()):
3567 3571 # target file to be touch on disk (relative to cwd)
3568 3572 target = repo.wjoin(abs)
3569 3573 # search the entry in the dispatch table.
3570 3574 # if the file is in any of these sets, it was touched in the working
3571 3575 # directory parent and we are sure it needs to be reverted.
3572 3576 for table, (xlist, msg), dobackup in disptable:
3573 3577 if abs not in table:
3574 3578 continue
3575 3579 if xlist is not None:
3576 3580 xlist.append(abs)
3577 3581 if dobackup:
3578 3582 # If in interactive mode, don't automatically create
3579 3583 # .orig files (issue4793)
3580 3584 if dobackup == backupinteractive:
3581 3585 tobackup.add(abs)
3582 3586 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
3583 3587 bakname = scmutil.origpath(ui, repo, rel)
3584 3588 ui.note(_('saving current version of %s as %s\n') %
3585 3589 (rel, bakname))
3586 3590 if not opts.get('dry_run'):
3587 3591 if interactive:
3588 3592 util.copyfile(target, bakname)
3589 3593 else:
3590 3594 util.rename(target, bakname)
3591 3595 if ui.verbose or not exact:
3592 3596 if not isinstance(msg, basestring):
3593 3597 msg = msg(abs)
3594 3598 ui.status(msg % rel)
3595 3599 elif exact:
3596 3600 ui.warn(msg % rel)
3597 3601 break
3598 3602
3599 3603 if not opts.get('dry_run'):
3600 3604 needdata = ('revert', 'add', 'undelete')
3601 3605 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
3602 3606 _performrevert(repo, parents, ctx, actions, interactive, tobackup)
3603 3607
3604 3608 if targetsubs:
3605 3609 # Revert the subrepos on the revert list
3606 3610 for sub in targetsubs:
3607 3611 try:
3608 3612 wctx.sub(sub).revert(ctx.substate[sub], *pats, **opts)
3609 3613 except KeyError:
3610 3614 raise error.Abort("subrepository '%s' does not exist in %s!"
3611 3615 % (sub, short(ctx.node())))
3612 3616
3613 3617 def _revertprefetch(repo, ctx, *files):
3614 3618 """Let extension changing the storage layer prefetch content"""
3615 3619 pass
3616 3620
3617 3621 def _performrevert(repo, parents, ctx, actions, interactive=False,
3618 3622 tobackup=None):
3619 3623 """function that actually perform all the actions computed for revert
3620 3624
3621 3625 This is an independent function to let extension to plug in and react to
3622 3626 the imminent revert.
3623 3627
3624 3628 Make sure you have the working directory locked when calling this function.
3625 3629 """
3626 3630 parent, p2 = parents
3627 3631 node = ctx.node()
3628 3632 excluded_files = []
3629 3633 matcher_opts = {"exclude": excluded_files}
3630 3634
3631 3635 def checkout(f):
3632 3636 fc = ctx[f]
3633 3637 repo.wwrite(f, fc.data(), fc.flags())
3634 3638
3635 3639 def doremove(f):
3636 3640 try:
3637 3641 repo.wvfs.unlinkpath(f)
3638 3642 except OSError:
3639 3643 pass
3640 3644 repo.dirstate.remove(f)
3641 3645
3642 3646 audit_path = pathutil.pathauditor(repo.root, cached=True)
3643 3647 for f in actions['forget'][0]:
3644 3648 if interactive:
3645 3649 choice = repo.ui.promptchoice(
3646 3650 _("forget added file %s (Yn)?$$ &Yes $$ &No") % f)
3647 3651 if choice == 0:
3648 3652 repo.dirstate.drop(f)
3649 3653 else:
3650 3654 excluded_files.append(repo.wjoin(f))
3651 3655 else:
3652 3656 repo.dirstate.drop(f)
3653 3657 for f in actions['remove'][0]:
3654 3658 audit_path(f)
3655 3659 if interactive:
3656 3660 choice = repo.ui.promptchoice(
3657 3661 _("remove added file %s (Yn)?$$ &Yes $$ &No") % f)
3658 3662 if choice == 0:
3659 3663 doremove(f)
3660 3664 else:
3661 3665 excluded_files.append(repo.wjoin(f))
3662 3666 else:
3663 3667 doremove(f)
3664 3668 for f in actions['drop'][0]:
3665 3669 audit_path(f)
3666 3670 repo.dirstate.remove(f)
3667 3671
3668 3672 normal = None
3669 3673 if node == parent:
3670 3674 # We're reverting to our parent. If possible, we'd like status
3671 3675 # to report the file as clean. We have to use normallookup for
3672 3676 # merges to avoid losing information about merged/dirty files.
3673 3677 if p2 != nullid:
3674 3678 normal = repo.dirstate.normallookup
3675 3679 else:
3676 3680 normal = repo.dirstate.normal
3677 3681
3678 3682 newlyaddedandmodifiedfiles = set()
3679 3683 if interactive:
3680 3684 # Prompt the user for changes to revert
3681 3685 torevert = [repo.wjoin(f) for f in actions['revert'][0]]
3682 3686 m = scmutil.match(ctx, torevert, matcher_opts)
3683 3687 diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
3684 3688 diffopts.nodates = True
3685 3689 diffopts.git = True
3686 3690 operation = 'discard'
3687 3691 reversehunks = True
3688 3692 if node != parent:
3689 3693 operation = 'revert'
3690 3694 reversehunks = repo.ui.configbool('experimental',
3691 3695 'revertalternateinteractivemode')
3692 3696 if reversehunks:
3693 3697 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3694 3698 else:
3695 3699 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3696 3700 originalchunks = patch.parsepatch(diff)
3697 3701
3698 3702 try:
3699 3703
3700 3704 chunks, opts = recordfilter(repo.ui, originalchunks,
3701 3705 operation=operation)
3702 3706 if reversehunks:
3703 3707 chunks = patch.reversehunks(chunks)
3704 3708
3705 3709 except patch.PatchError as err:
3706 3710 raise error.Abort(_('error parsing patch: %s') % err)
3707 3711
3708 3712 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3709 3713 if tobackup is None:
3710 3714 tobackup = set()
3711 3715 # Apply changes
3712 3716 fp = stringio()
3713 3717 for c in chunks:
3714 3718 # Create a backup file only if this hunk should be backed up
3715 3719 if ishunk(c) and c.header.filename() in tobackup:
3716 3720 abs = c.header.filename()
3717 3721 target = repo.wjoin(abs)
3718 3722 bakname = scmutil.origpath(repo.ui, repo, m.rel(abs))
3719 3723 util.copyfile(target, bakname)
3720 3724 tobackup.remove(abs)
3721 3725 c.write(fp)
3722 3726 dopatch = fp.tell()
3723 3727 fp.seek(0)
3724 3728 if dopatch:
3725 3729 try:
3726 3730 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3727 3731 except patch.PatchError as err:
3728 3732 raise error.Abort(str(err))
3729 3733 del fp
3730 3734 else:
3731 3735 for f in actions['revert'][0]:
3732 3736 checkout(f)
3733 3737 if normal:
3734 3738 normal(f)
3735 3739
3736 3740 for f in actions['add'][0]:
3737 3741 # Don't checkout modified files, they are already created by the diff
3738 3742 if f not in newlyaddedandmodifiedfiles:
3739 3743 checkout(f)
3740 3744 repo.dirstate.add(f)
3741 3745
3742 3746 normal = repo.dirstate.normallookup
3743 3747 if node == parent and p2 == nullid:
3744 3748 normal = repo.dirstate.normal
3745 3749 for f in actions['undelete'][0]:
3746 3750 checkout(f)
3747 3751 normal(f)
3748 3752
3749 3753 copied = copies.pathcopies(repo[parent], ctx)
3750 3754
3751 3755 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3752 3756 if f in copied:
3753 3757 repo.dirstate.copy(copied[f], f)
3754 3758
3755 3759 class command(registrar.command):
3756 3760 def _doregister(self, func, name, *args, **kwargs):
3757 3761 func._deprecatedregistrar = True # flag for deprecwarn in extensions.py
3758 3762 return super(command, self)._doregister(func, name, *args, **kwargs)
3759 3763
3760 3764 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3761 3765 # commands.outgoing. "missing" is "missing" of the result of
3762 3766 # "findcommonoutgoing()"
3763 3767 outgoinghooks = util.hooks()
3764 3768
3765 3769 # a list of (ui, repo) functions called by commands.summary
3766 3770 summaryhooks = util.hooks()
3767 3771
3768 3772 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3769 3773 #
3770 3774 # functions should return tuple of booleans below, if 'changes' is None:
3771 3775 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3772 3776 #
3773 3777 # otherwise, 'changes' is a tuple of tuples below:
3774 3778 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3775 3779 # - (desturl, destbranch, destpeer, outgoing)
3776 3780 summaryremotehooks = util.hooks()
3777 3781
3778 3782 # A list of state files kept by multistep operations like graft.
3779 3783 # Since graft cannot be aborted, it is considered 'clearable' by update.
3780 3784 # note: bisect is intentionally excluded
3781 3785 # (state file, clearable, allowcommit, error, hint)
3782 3786 unfinishedstates = [
3783 3787 ('graftstate', True, False, _('graft in progress'),
3784 3788 _("use 'hg graft --continue' or 'hg update' to abort")),
3785 3789 ('updatestate', True, False, _('last update was interrupted'),
3786 3790 _("use 'hg update' to get a consistent checkout"))
3787 3791 ]
3788 3792
3789 3793 def checkunfinished(repo, commit=False):
3790 3794 '''Look for an unfinished multistep operation, like graft, and abort
3791 3795 if found. It's probably good to check this right before
3792 3796 bailifchanged().
3793 3797 '''
3794 3798 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3795 3799 if commit and allowcommit:
3796 3800 continue
3797 3801 if repo.vfs.exists(f):
3798 3802 raise error.Abort(msg, hint=hint)
3799 3803
3800 3804 def clearunfinished(repo):
3801 3805 '''Check for unfinished operations (as above), and clear the ones
3802 3806 that are clearable.
3803 3807 '''
3804 3808 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3805 3809 if not clearable and repo.vfs.exists(f):
3806 3810 raise error.Abort(msg, hint=hint)
3807 3811 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3808 3812 if clearable and repo.vfs.exists(f):
3809 3813 util.unlink(repo.vfs.join(f))
3810 3814
3811 3815 afterresolvedstates = [
3812 3816 ('graftstate',
3813 3817 _('hg graft --continue')),
3814 3818 ]
3815 3819
3816 3820 def howtocontinue(repo):
3817 3821 '''Check for an unfinished operation and return the command to finish
3818 3822 it.
3819 3823
3820 3824 afterresolvedstates tuples define a .hg/{file} and the corresponding
3821 3825 command needed to finish it.
3822 3826
3823 3827 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3824 3828 a boolean.
3825 3829 '''
3826 3830 contmsg = _("continue: %s")
3827 3831 for f, msg in afterresolvedstates:
3828 3832 if repo.vfs.exists(f):
3829 3833 return contmsg % msg, True
3830 3834 if repo[None].dirty(missing=True, merge=False, branch=False):
3831 3835 return contmsg % _("hg commit"), False
3832 3836 return None, None
3833 3837
3834 3838 def checkafterresolved(repo):
3835 3839 '''Inform the user about the next action after completing hg resolve
3836 3840
3837 3841 If there's a matching afterresolvedstates, howtocontinue will yield
3838 3842 repo.ui.warn as the reporter.
3839 3843
3840 3844 Otherwise, it will yield repo.ui.note.
3841 3845 '''
3842 3846 msg, warning = howtocontinue(repo)
3843 3847 if msg is not None:
3844 3848 if warning:
3845 3849 repo.ui.warn("%s\n" % msg)
3846 3850 else:
3847 3851 repo.ui.note("%s\n" % msg)
3848 3852
3849 3853 def wrongtooltocontinue(repo, task):
3850 3854 '''Raise an abort suggesting how to properly continue if there is an
3851 3855 active task.
3852 3856
3853 3857 Uses howtocontinue() to find the active task.
3854 3858
3855 3859 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3856 3860 a hint.
3857 3861 '''
3858 3862 after = howtocontinue(repo)
3859 3863 hint = None
3860 3864 if after[1]:
3861 3865 hint = after[0]
3862 3866 raise error.Abort(_('no %s in progress') % task, hint=hint)
@@ -1,478 +1,481 b''
1 1 # Revision graph generator for Mercurial
2 2 #
3 3 # Copyright 2008 Dirkjan Ochtman <dirkjan@ochtman.nl>
4 4 # Copyright 2007 Joel Rosdahl <joel@rosdahl.net>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 """supports walking the history as DAGs suitable for graphical output
10 10
11 11 The most basic format we use is that of::
12 12
13 13 (id, type, data, [parentids])
14 14
15 15 The node and parent ids are arbitrary integers which identify a node in the
16 16 context of the graph returned. Type is a constant specifying the node type.
17 17 Data depends on type.
18 18 """
19 19
20 20 from __future__ import absolute_import
21 21
22 22 from .node import nullrev
23 23 from . import (
24 24 dagop,
25 25 smartset,
26 26 util,
27 27 )
28 28
29 29 CHANGESET = 'C'
30 30 PARENT = 'P'
31 31 GRANDPARENT = 'G'
32 32 MISSINGPARENT = 'M'
33 33 # Style of line to draw. None signals a line that ends and is removed at this
34 34 # point. A number prefix means only the last N characters of the current block
35 35 # will use that style, the rest will use the PARENT style. Add a - sign
36 36 # (so making N negative) and all but the first N characters use that style.
37 37 EDGES = {PARENT: '|', GRANDPARENT: ':', MISSINGPARENT: None}
38 38
39 39 def dagwalker(repo, revs):
40 40 """cset DAG generator yielding (id, CHANGESET, ctx, [parentinfo]) tuples
41 41
42 42 This generator function walks through revisions (which should be ordered
43 43 from bigger to lower). It returns a tuple for each node.
44 44
45 45 Each parentinfo entry is a tuple with (edgetype, parentid), where edgetype
46 46 is one of PARENT, GRANDPARENT or MISSINGPARENT. The node and parent ids
47 47 are arbitrary integers which identify a node in the context of the graph
48 48 returned.
49 49
50 50 """
51 51 if not revs:
52 52 return
53 53
54 54 gpcache = {}
55 55
56 56 for rev in revs:
57 57 ctx = repo[rev]
58 58 # partition into parents in the rev set and missing parents, then
59 59 # augment the lists with markers, to inform graph drawing code about
60 60 # what kind of edge to draw between nodes.
61 61 pset = set(p.rev() for p in ctx.parents() if p.rev() in revs)
62 62 mpars = [p.rev() for p in ctx.parents()
63 63 if p.rev() != nullrev and p.rev() not in pset]
64 64 parents = [(PARENT, p) for p in sorted(pset)]
65 65
66 66 for mpar in mpars:
67 67 gp = gpcache.get(mpar)
68 68 if gp is None:
69 69 # precompute slow query as we know reachableroots() goes
70 70 # through all revs (issue4782)
71 71 if not isinstance(revs, smartset.baseset):
72 72 revs = smartset.baseset(revs)
73 73 gp = gpcache[mpar] = sorted(set(dagop.reachableroots(
74 74 repo, revs, [mpar])))
75 75 if not gp:
76 76 parents.append((MISSINGPARENT, mpar))
77 77 pset.add(mpar)
78 78 else:
79 79 parents.extend((GRANDPARENT, g) for g in gp if g not in pset)
80 80 pset.update(gp)
81 81
82 82 yield (ctx.rev(), CHANGESET, ctx, parents)
83 83
84 84 def nodes(repo, nodes):
85 85 """cset DAG generator yielding (id, CHANGESET, ctx, [parentids]) tuples
86 86
87 87 This generator function walks the given nodes. It only returns parents
88 88 that are in nodes, too.
89 89 """
90 90 include = set(nodes)
91 91 for node in nodes:
92 92 ctx = repo[node]
93 93 parents = set((PARENT, p.rev()) for p in ctx.parents()
94 94 if p.node() in include)
95 95 yield (ctx.rev(), CHANGESET, ctx, sorted(parents))
96 96
97 97 def colored(dag, repo):
98 98 """annotates a DAG with colored edge information
99 99
100 100 For each DAG node this function emits tuples::
101 101
102 102 (id, type, data, (col, color), [(col, nextcol, color)])
103 103
104 104 with the following new elements:
105 105
106 106 - Tuple (col, color) with column and color index for the current node
107 107 - A list of tuples indicating the edges between the current node and its
108 108 parents.
109 109 """
110 110 seen = []
111 111 colors = {}
112 112 newcolor = 1
113 113 config = {}
114 114
115 115 for key, val in repo.ui.configitems('graph'):
116 116 if '.' in key:
117 117 branch, setting = key.rsplit('.', 1)
118 118 # Validation
119 119 if setting == "width" and val.isdigit():
120 120 config.setdefault(branch, {})[setting] = int(val)
121 121 elif setting == "color" and val.isalnum():
122 122 config.setdefault(branch, {})[setting] = val
123 123
124 124 if config:
125 125 getconf = util.lrucachefunc(
126 126 lambda rev: config.get(repo[rev].branch(), {}))
127 127 else:
128 128 getconf = lambda rev: {}
129 129
130 130 for (cur, type, data, parents) in dag:
131 131
132 132 # Compute seen and next
133 133 if cur not in seen:
134 134 seen.append(cur) # new head
135 135 colors[cur] = newcolor
136 136 newcolor += 1
137 137
138 138 col = seen.index(cur)
139 139 color = colors.pop(cur)
140 140 next = seen[:]
141 141
142 142 # Add parents to next
143 143 addparents = [p for pt, p in parents if p not in next]
144 144 next[col:col + 1] = addparents
145 145
146 146 # Set colors for the parents
147 147 for i, p in enumerate(addparents):
148 148 if not i:
149 149 colors[p] = color
150 150 else:
151 151 colors[p] = newcolor
152 152 newcolor += 1
153 153
154 154 # Add edges to the graph
155 155 edges = []
156 156 for ecol, eid in enumerate(seen):
157 157 if eid in next:
158 158 bconf = getconf(eid)
159 159 edges.append((
160 160 ecol, next.index(eid), colors[eid],
161 161 bconf.get('width', -1),
162 162 bconf.get('color', '')))
163 163 elif eid == cur:
164 164 for ptype, p in parents:
165 165 bconf = getconf(p)
166 166 edges.append((
167 167 ecol, next.index(p), color,
168 168 bconf.get('width', -1),
169 169 bconf.get('color', '')))
170 170
171 171 # Yield and move on
172 172 yield (cur, type, data, (col, color), edges)
173 173 seen = next
174 174
175 def asciiedges(type, char, lines, state, rev, parents):
175 def asciiedges(type, char, state, rev, parents):
176 176 """adds edge info to changelog DAG walk suitable for ascii()"""
177 177 seen = state['seen']
178 178 if rev not in seen:
179 179 seen.append(rev)
180 180 nodeidx = seen.index(rev)
181 181
182 182 knownparents = []
183 183 newparents = []
184 184 for ptype, parent in parents:
185 185 if parent == rev:
186 186 # self reference (should only be seen in null rev)
187 187 continue
188 188 if parent in seen:
189 189 knownparents.append(parent)
190 190 else:
191 191 newparents.append(parent)
192 192 state['edges'][parent] = state['styles'].get(ptype, '|')
193 193
194 194 ncols = len(seen)
195 width = 1 + ncols * 2
195 196 nextseen = seen[:]
196 197 nextseen[nodeidx:nodeidx + 1] = newparents
197 198 edges = [(nodeidx, nextseen.index(p)) for p in knownparents]
198 199
199 200 seen[:] = nextseen
200 201 while len(newparents) > 2:
201 202 # ascii() only knows how to add or remove a single column between two
202 203 # calls. Nodes with more than two parents break this constraint so we
203 204 # introduce intermediate expansion lines to grow the active node list
204 205 # slowly.
205 206 edges.append((nodeidx, nodeidx))
206 207 edges.append((nodeidx, nodeidx + 1))
207 208 nmorecols = 1
208 yield (type, char, lines, (nodeidx, edges, ncols, nmorecols))
209 width += 2
210 yield (type, char, width, (nodeidx, edges, ncols, nmorecols))
209 211 char = '\\'
210 lines = []
211 212 nodeidx += 1
212 213 ncols += 1
213 214 edges = []
214 215 del newparents[0]
215 216
216 217 if len(newparents) > 0:
217 218 edges.append((nodeidx, nodeidx))
218 219 if len(newparents) > 1:
219 220 edges.append((nodeidx, nodeidx + 1))
220 221 nmorecols = len(nextseen) - ncols
222 if nmorecols > 0:
223 width += 2
221 224 # remove current node from edge characters, no longer needed
222 225 state['edges'].pop(rev, None)
223 yield (type, char, lines, (nodeidx, edges, ncols, nmorecols))
226 yield (type, char, width, (nodeidx, edges, ncols, nmorecols))
224 227
225 228 def _fixlongrightedges(edges):
226 229 for (i, (start, end)) in enumerate(edges):
227 230 if end > start:
228 231 edges[i] = (start, end + 1)
229 232
230 233 def _getnodelineedgestail(
231 234 echars, idx, pidx, ncols, coldiff, pdiff, fix_tail):
232 235 if fix_tail and coldiff == pdiff and coldiff != 0:
233 236 # Still going in the same non-vertical direction.
234 237 if coldiff == -1:
235 238 start = max(idx + 1, pidx)
236 239 tail = echars[idx * 2:(start - 1) * 2]
237 240 tail.extend(["/", " "] * (ncols - start))
238 241 return tail
239 242 else:
240 243 return ["\\", " "] * (ncols - idx - 1)
241 244 else:
242 245 remainder = (ncols - idx - 1)
243 246 return echars[-(remainder * 2):] if remainder > 0 else []
244 247
245 248 def _drawedges(echars, edges, nodeline, interline):
246 249 for (start, end) in edges:
247 250 if start == end + 1:
248 251 interline[2 * end + 1] = "/"
249 252 elif start == end - 1:
250 253 interline[2 * start + 1] = "\\"
251 254 elif start == end:
252 255 interline[2 * start] = echars[2 * start]
253 256 else:
254 257 if 2 * end >= len(nodeline):
255 258 continue
256 259 nodeline[2 * end] = "+"
257 260 if start > end:
258 261 (start, end) = (end, start)
259 262 for i in range(2 * start + 1, 2 * end):
260 263 if nodeline[i] != "+":
261 264 nodeline[i] = "-"
262 265
263 266 def _getpaddingline(echars, idx, ncols, edges):
264 267 # all edges up to the current node
265 268 line = echars[:idx * 2]
266 269 # an edge for the current node, if there is one
267 270 if (idx, idx - 1) in edges or (idx, idx) in edges:
268 271 # (idx, idx - 1) (idx, idx)
269 272 # | | | | | | | |
270 273 # +---o | | o---+
271 274 # | | X | | X | |
272 275 # | |/ / | |/ /
273 276 # | | | | | |
274 277 line.extend(echars[idx * 2:(idx + 1) * 2])
275 278 else:
276 279 line.extend([' ', ' '])
277 280 # all edges to the right of the current node
278 281 remainder = ncols - idx - 1
279 282 if remainder > 0:
280 283 line.extend(echars[-(remainder * 2):])
281 284 return line
282 285
283 286 def _drawendinglines(lines, extra, edgemap, seen):
284 287 """Draw ending lines for missing parent edges
285 288
286 289 None indicates an edge that ends at between this node and the next
287 290 Replace with a short line ending in ~ and add / lines to any edges to
288 291 the right.
289 292
290 293 """
291 294 if None not in edgemap.values():
292 295 return
293 296
294 297 # Check for more edges to the right of our ending edges.
295 298 # We need enough space to draw adjustment lines for these.
296 299 edgechars = extra[::2]
297 300 while edgechars and edgechars[-1] is None:
298 301 edgechars.pop()
299 302 shift_size = max((edgechars.count(None) * 2) - 1, 0)
300 303 while len(lines) < 3 + shift_size:
301 304 lines.append(extra[:])
302 305
303 306 if shift_size:
304 307 empties = []
305 308 toshift = []
306 309 first_empty = extra.index(None)
307 310 for i, c in enumerate(extra[first_empty::2], first_empty // 2):
308 311 if c is None:
309 312 empties.append(i * 2)
310 313 else:
311 314 toshift.append(i * 2)
312 315 targets = list(range(first_empty, first_empty + len(toshift) * 2, 2))
313 316 positions = toshift[:]
314 317 for line in lines[-shift_size:]:
315 318 line[first_empty:] = [' '] * (len(line) - first_empty)
316 319 for i in range(len(positions)):
317 320 pos = positions[i] - 1
318 321 positions[i] = max(pos, targets[i])
319 322 line[pos] = '/' if pos > targets[i] else extra[toshift[i]]
320 323
321 324 map = {1: '|', 2: '~'}
322 325 for i, line in enumerate(lines):
323 326 if None not in line:
324 327 continue
325 328 line[:] = [c or map.get(i, ' ') for c in line]
326 329
327 330 # remove edges that ended
328 331 remove = [p for p, c in edgemap.items() if c is None]
329 332 for parent in remove:
330 333 del edgemap[parent]
331 334 seen.remove(parent)
332 335
333 336 def asciistate():
334 337 """returns the initial value for the "state" argument to ascii()"""
335 338 return {
336 339 'seen': [],
337 340 'edges': {},
338 341 'lastcoldiff': 0,
339 342 'lastindex': 0,
340 343 'styles': EDGES.copy(),
341 344 'graphshorten': False,
342 345 }
343 346
344 347 def ascii(ui, state, type, char, text, coldata):
345 348 """prints an ASCII graph of the DAG
346 349
347 350 takes the following arguments (one call per node in the graph):
348 351
349 352 - ui to write to
350 353 - Somewhere to keep the needed state in (init to asciistate())
351 354 - Column of the current node in the set of ongoing edges.
352 355 - Type indicator of node data, usually 'C' for changesets.
353 356 - Payload: (char, lines):
354 357 - Character to use as node's symbol.
355 358 - List of lines to display as the node's text.
356 359 - Edges; a list of (col, next_col) indicating the edges between
357 360 the current node and its parents.
358 361 - Number of columns (ongoing edges) in the current revision.
359 362 - The difference between the number of columns (ongoing edges)
360 363 in the next revision and the number of columns (ongoing edges)
361 364 in the current revision. That is: -1 means one column removed;
362 365 0 means no columns added or removed; 1 means one column added.
363 366 """
364 367 idx, edges, ncols, coldiff = coldata
365 368 assert -2 < coldiff < 2
366 369
367 370 edgemap, seen = state['edges'], state['seen']
368 371 # Be tolerant of history issues; make sure we have at least ncols + coldiff
369 372 # elements to work with. See test-glog.t for broken history test cases.
370 373 echars = [c for p in seen for c in (edgemap.get(p, '|'), ' ')]
371 374 echars.extend(('|', ' ') * max(ncols + coldiff - len(seen), 0))
372 375
373 376 if coldiff == -1:
374 377 # Transform
375 378 #
376 379 # | | | | | |
377 380 # o | | into o---+
378 381 # |X / |/ /
379 382 # | | | |
380 383 _fixlongrightedges(edges)
381 384
382 385 # add_padding_line says whether to rewrite
383 386 #
384 387 # | | | | | | | |
385 388 # | o---+ into | o---+
386 389 # | / / | | | # <--- padding line
387 390 # o | | | / /
388 391 # o | |
389 392 add_padding_line = (len(text) > 2 and coldiff == -1 and
390 393 [x for (x, y) in edges if x + 1 < y])
391 394
392 395 # fix_nodeline_tail says whether to rewrite
393 396 #
394 397 # | | o | | | | o | |
395 398 # | | |/ / | | |/ /
396 399 # | o | | into | o / / # <--- fixed nodeline tail
397 400 # | |/ / | |/ /
398 401 # o | | o | |
399 402 fix_nodeline_tail = len(text) <= 2 and not add_padding_line
400 403
401 404 # nodeline is the line containing the node character (typically o)
402 405 nodeline = echars[:idx * 2]
403 406 nodeline.extend([char, " "])
404 407
405 408 nodeline.extend(
406 409 _getnodelineedgestail(
407 410 echars, idx, state['lastindex'], ncols, coldiff,
408 411 state['lastcoldiff'], fix_nodeline_tail))
409 412
410 413 # shift_interline is the line containing the non-vertical
411 414 # edges between this entry and the next
412 415 shift_interline = echars[:idx * 2]
413 416 for i in xrange(2 + coldiff):
414 417 shift_interline.append(' ')
415 418 count = ncols - idx - 1
416 419 if coldiff == -1:
417 420 for i in xrange(count):
418 421 shift_interline.extend(['/', ' '])
419 422 elif coldiff == 0:
420 423 shift_interline.extend(echars[(idx + 1) * 2:ncols * 2])
421 424 else:
422 425 for i in xrange(count):
423 426 shift_interline.extend(['\\', ' '])
424 427
425 428 # draw edges from the current node to its parents
426 429 _drawedges(echars, edges, nodeline, shift_interline)
427 430
428 431 # lines is the list of all graph lines to print
429 432 lines = [nodeline]
430 433 if add_padding_line:
431 434 lines.append(_getpaddingline(echars, idx, ncols, edges))
432 435
433 436 # If 'graphshorten' config, only draw shift_interline
434 437 # when there is any non vertical flow in graph.
435 438 if state['graphshorten']:
436 439 if any(c in '\/' for c in shift_interline if c):
437 440 lines.append(shift_interline)
438 441 # Else, no 'graphshorten' config so draw shift_interline.
439 442 else:
440 443 lines.append(shift_interline)
441 444
442 445 # make sure that there are as many graph lines as there are
443 446 # log strings
444 447 extra_interline = echars[:(ncols + coldiff) * 2]
445 448 if len(lines) < len(text):
446 449 while len(lines) < len(text):
447 450 lines.append(extra_interline[:])
448 451
449 452 _drawendinglines(lines, extra_interline, edgemap, seen)
450 453
451 454 while len(text) < len(lines):
452 455 text.append("")
453 456
454 457 if any(len(char) > 1 for char in edgemap.values()):
455 458 # limit drawing an edge to the first or last N lines of the current
456 459 # section the rest of the edge is drawn like a parent line.
457 460 parent = state['styles'][PARENT][-1]
458 461 def _drawgp(char, i):
459 462 # should a grandparent character be drawn for this line?
460 463 if len(char) < 2:
461 464 return True
462 465 num = int(char[:-1])
463 466 # either skip first num lines or take last num lines, based on sign
464 467 return -num <= i if num < 0 else (len(lines) - i) <= num
465 468 for i, line in enumerate(lines):
466 469 line[:] = [c[-1] if _drawgp(c, i) else parent for c in line]
467 470 edgemap.update(
468 471 (e, (c if len(c) < 2 else parent)) for e, c in edgemap.items())
469 472
470 473 # print lines
471 474 indentation_level = max(ncols, ncols + coldiff)
472 475 for (line, logstr) in zip(lines, text):
473 476 ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
474 477 ui.write(ln.rstrip() + '\n')
475 478
476 479 # ... and start over
477 480 state['lastcoldiff'] = coldiff
478 481 state['lastindex'] = idx
@@ -1,790 +1,797 b''
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 10 from .i18n import _
11 11 from .node import (
12 12 hex,
13 13 nullid,
14 14 short,
15 15 )
16 16
17 17 from . import (
18 18 encoding,
19 19 error,
20 20 hbisect,
21 21 obsutil,
22 22 patch,
23 23 pycompat,
24 24 registrar,
25 25 scmutil,
26 26 util,
27 27 )
28 28
29 29 class _hybrid(object):
30 30 """Wrapper for list or dict to support legacy template
31 31
32 32 This class allows us to handle both:
33 33 - "{files}" (legacy command-line-specific list hack) and
34 34 - "{files % '{file}\n'}" (hgweb-style with inlining and function support)
35 35 and to access raw values:
36 36 - "{ifcontains(file, files, ...)}", "{ifcontains(key, extras, ...)}"
37 37 - "{get(extras, key)}"
38 38 - "{files|json}"
39 39 """
40 40
41 41 def __init__(self, gen, values, makemap, joinfmt):
42 42 if gen is not None:
43 43 self.gen = gen
44 44 self._values = values
45 45 self._makemap = makemap
46 46 self.joinfmt = joinfmt
47 47 @util.propertycache
48 48 def gen(self):
49 49 return self._defaultgen()
50 50 def _defaultgen(self):
51 51 """Generator to stringify this as {join(self, ' ')}"""
52 52 for i, d in enumerate(self.itermaps()):
53 53 if i > 0:
54 54 yield ' '
55 55 yield self.joinfmt(d)
56 56 def itermaps(self):
57 57 makemap = self._makemap
58 58 for x in self._values:
59 59 yield makemap(x)
60 60 def __contains__(self, x):
61 61 return x in self._values
62 62 def __getitem__(self, key):
63 63 return self._values[key]
64 64 def __len__(self):
65 65 return len(self._values)
66 66 def __iter__(self):
67 67 return iter(self._values)
68 68 def __getattr__(self, name):
69 69 if name not in ('get', 'items', 'iteritems', 'iterkeys', 'itervalues',
70 70 'keys', 'values'):
71 71 raise AttributeError(name)
72 72 return getattr(self._values, name)
73 73
74 74 def hybriddict(data, key='key', value='value', fmt='%s=%s', gen=None):
75 75 """Wrap data to support both dict-like and string-like operations"""
76 76 return _hybrid(gen, data, lambda k: {key: k, value: data[k]},
77 77 lambda d: fmt % (d[key], d[value]))
78 78
79 79 def hybridlist(data, name, fmt='%s', gen=None):
80 80 """Wrap data to support both list-like and string-like operations"""
81 81 return _hybrid(gen, data, lambda x: {name: x}, lambda d: fmt % d[name])
82 82
83 83 def unwraphybrid(thing):
84 84 """Return an object which can be stringified possibly by using a legacy
85 85 template"""
86 86 if not util.safehasattr(thing, 'gen'):
87 87 return thing
88 88 return thing.gen
89 89
90 90 def showdict(name, data, mapping, plural=None, key='key', value='value',
91 91 fmt='%s=%s', separator=' '):
92 92 c = [{key: k, value: v} for k, v in data.iteritems()]
93 93 f = _showlist(name, c, mapping, plural, separator)
94 94 return hybriddict(data, key=key, value=value, fmt=fmt, gen=f)
95 95
96 96 def showlist(name, values, mapping, plural=None, element=None, separator=' '):
97 97 if not element:
98 98 element = name
99 99 f = _showlist(name, values, mapping, plural, separator)
100 100 return hybridlist(values, name=element, gen=f)
101 101
102 102 def _showlist(name, values, mapping, plural=None, separator=' '):
103 103 '''expand set of values.
104 104 name is name of key in template map.
105 105 values is list of strings or dicts.
106 106 plural is plural of name, if not simply name + 's'.
107 107 separator is used to join values as a string
108 108
109 109 expansion works like this, given name 'foo'.
110 110
111 111 if values is empty, expand 'no_foos'.
112 112
113 113 if 'foo' not in template map, return values as a string,
114 114 joined by 'separator'.
115 115
116 116 expand 'start_foos'.
117 117
118 118 for each value, expand 'foo'. if 'last_foo' in template
119 119 map, expand it instead of 'foo' for last key.
120 120
121 121 expand 'end_foos'.
122 122 '''
123 123 templ = mapping['templ']
124 124 strmapping = pycompat.strkwargs(mapping)
125 125 if not plural:
126 126 plural = name + 's'
127 127 if not values:
128 128 noname = 'no_' + plural
129 129 if noname in templ:
130 130 yield templ(noname, **strmapping)
131 131 return
132 132 if name not in templ:
133 133 if isinstance(values[0], bytes):
134 134 yield separator.join(values)
135 135 else:
136 136 for v in values:
137 137 yield dict(v, **strmapping)
138 138 return
139 139 startname = 'start_' + plural
140 140 if startname in templ:
141 141 yield templ(startname, **strmapping)
142 142 vmapping = mapping.copy()
143 143 def one(v, tag=name):
144 144 try:
145 145 vmapping.update(v)
146 146 except (AttributeError, ValueError):
147 147 try:
148 148 for a, b in v:
149 149 vmapping[a] = b
150 150 except ValueError:
151 151 vmapping[name] = v
152 152 return templ(tag, **pycompat.strkwargs(vmapping))
153 153 lastname = 'last_' + name
154 154 if lastname in templ:
155 155 last = values.pop()
156 156 else:
157 157 last = None
158 158 for v in values:
159 159 yield one(v)
160 160 if last is not None:
161 161 yield one(last, tag=lastname)
162 162 endname = 'end_' + plural
163 163 if endname in templ:
164 164 yield templ(endname, **strmapping)
165 165
166 166 def _formatrevnode(ctx):
167 167 """Format changeset as '{rev}:{node|formatnode}', which is the default
168 168 template provided by cmdutil.changeset_templater"""
169 169 repo = ctx.repo()
170 170 if repo.ui.debugflag:
171 171 hexfunc = hex
172 172 else:
173 173 hexfunc = short
174 174 return '%d:%s' % (scmutil.intrev(ctx), hexfunc(scmutil.binnode(ctx)))
175 175
176 176 def getfiles(repo, ctx, revcache):
177 177 if 'files' not in revcache:
178 178 revcache['files'] = repo.status(ctx.p1(), ctx)[:3]
179 179 return revcache['files']
180 180
181 181 def getlatesttags(repo, ctx, cache, pattern=None):
182 182 '''return date, distance and name for the latest tag of rev'''
183 183
184 184 cachename = 'latesttags'
185 185 if pattern is not None:
186 186 cachename += '-' + pattern
187 187 match = util.stringmatcher(pattern)[2]
188 188 else:
189 189 match = util.always
190 190
191 191 if cachename not in cache:
192 192 # Cache mapping from rev to a tuple with tag date, tag
193 193 # distance and tag name
194 194 cache[cachename] = {-1: (0, 0, ['null'])}
195 195 latesttags = cache[cachename]
196 196
197 197 rev = ctx.rev()
198 198 todo = [rev]
199 199 while todo:
200 200 rev = todo.pop()
201 201 if rev in latesttags:
202 202 continue
203 203 ctx = repo[rev]
204 204 tags = [t for t in ctx.tags()
205 205 if (repo.tagtype(t) and repo.tagtype(t) != 'local'
206 206 and match(t))]
207 207 if tags:
208 208 latesttags[rev] = ctx.date()[0], 0, [t for t in sorted(tags)]
209 209 continue
210 210 try:
211 211 # The tuples are laid out so the right one can be found by
212 212 # comparison.
213 213 pdate, pdist, ptag = max(
214 214 latesttags[p.rev()] for p in ctx.parents())
215 215 except KeyError:
216 216 # Cache miss - recurse
217 217 todo.append(rev)
218 218 todo.extend(p.rev() for p in ctx.parents())
219 219 continue
220 220 latesttags[rev] = pdate, pdist + 1, ptag
221 221 return latesttags[rev]
222 222
223 223 def getrenamedfn(repo, endrev=None):
224 224 rcache = {}
225 225 if endrev is None:
226 226 endrev = len(repo)
227 227
228 228 def getrenamed(fn, rev):
229 229 '''looks up all renames for a file (up to endrev) the first
230 230 time the file is given. It indexes on the changerev and only
231 231 parses the manifest if linkrev != changerev.
232 232 Returns rename info for fn at changerev rev.'''
233 233 if fn not in rcache:
234 234 rcache[fn] = {}
235 235 fl = repo.file(fn)
236 236 for i in fl:
237 237 lr = fl.linkrev(i)
238 238 renamed = fl.renamed(fl.node(i))
239 239 rcache[fn][lr] = renamed
240 240 if lr >= endrev:
241 241 break
242 242 if rev in rcache[fn]:
243 243 return rcache[fn][rev]
244 244
245 245 # If linkrev != rev (i.e. rev not found in rcache) fallback to
246 246 # filectx logic.
247 247 try:
248 248 return repo[rev][fn].renamed()
249 249 except error.LookupError:
250 250 return None
251 251
252 252 return getrenamed
253 253
254 254 # default templates internally used for rendering of lists
255 255 defaulttempl = {
256 256 'parent': '{rev}:{node|formatnode} ',
257 257 'manifest': '{rev}:{node|formatnode}',
258 258 'file_copy': '{name} ({source})',
259 259 'envvar': '{key}={value}',
260 260 'extra': '{key}={value|stringescape}'
261 261 }
262 262 # filecopy is preserved for compatibility reasons
263 263 defaulttempl['filecopy'] = defaulttempl['file_copy']
264 264
265 265 # keywords are callables like:
266 266 # fn(repo, ctx, templ, cache, revcache, **args)
267 267 # with:
268 268 # repo - current repository instance
269 269 # ctx - the changectx being displayed
270 270 # templ - the templater instance
271 271 # cache - a cache dictionary for the whole templater run
272 272 # revcache - a cache dictionary for the current revision
273 273 keywords = {}
274 274
275 275 templatekeyword = registrar.templatekeyword(keywords)
276 276
277 277 @templatekeyword('author')
278 278 def showauthor(repo, ctx, templ, **args):
279 279 """String. The unmodified author of the changeset."""
280 280 return ctx.user()
281 281
282 282 @templatekeyword('bisect')
283 283 def showbisect(repo, ctx, templ, **args):
284 284 """String. The changeset bisection status."""
285 285 return hbisect.label(repo, ctx.node())
286 286
287 287 @templatekeyword('branch')
288 288 def showbranch(**args):
289 289 """String. The name of the branch on which the changeset was
290 290 committed.
291 291 """
292 292 return args[r'ctx'].branch()
293 293
294 294 @templatekeyword('branches')
295 295 def showbranches(**args):
296 296 """List of strings. The name of the branch on which the
297 297 changeset was committed. Will be empty if the branch name was
298 298 default. (DEPRECATED)
299 299 """
300 300 args = pycompat.byteskwargs(args)
301 301 branch = args['ctx'].branch()
302 302 if branch != 'default':
303 303 return showlist('branch', [branch], args, plural='branches')
304 304 return showlist('branch', [], args, plural='branches')
305 305
306 306 @templatekeyword('bookmarks')
307 307 def showbookmarks(**args):
308 308 """List of strings. Any bookmarks associated with the
309 309 changeset. Also sets 'active', the name of the active bookmark.
310 310 """
311 311 args = pycompat.byteskwargs(args)
312 312 repo = args['ctx']._repo
313 313 bookmarks = args['ctx'].bookmarks()
314 314 active = repo._activebookmark
315 315 makemap = lambda v: {'bookmark': v, 'active': active, 'current': active}
316 316 f = _showlist('bookmark', bookmarks, args)
317 317 return _hybrid(f, bookmarks, makemap, lambda x: x['bookmark'])
318 318
319 319 @templatekeyword('children')
320 320 def showchildren(**args):
321 321 """List of strings. The children of the changeset."""
322 322 args = pycompat.byteskwargs(args)
323 323 ctx = args['ctx']
324 324 childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
325 325 return showlist('children', childrevs, args, element='child')
326 326
327 327 # Deprecated, but kept alive for help generation a purpose.
328 328 @templatekeyword('currentbookmark')
329 329 def showcurrentbookmark(**args):
330 330 """String. The active bookmark, if it is
331 331 associated with the changeset (DEPRECATED)"""
332 332 return showactivebookmark(**args)
333 333
334 334 @templatekeyword('activebookmark')
335 335 def showactivebookmark(**args):
336 336 """String. The active bookmark, if it is
337 337 associated with the changeset"""
338 338 active = args[r'repo']._activebookmark
339 339 if active and active in args[r'ctx'].bookmarks():
340 340 return active
341 341 return ''
342 342
343 343 @templatekeyword('date')
344 344 def showdate(repo, ctx, templ, **args):
345 345 """Date information. The date when the changeset was committed."""
346 346 return ctx.date()
347 347
348 348 @templatekeyword('desc')
349 349 def showdescription(repo, ctx, templ, **args):
350 350 """String. The text of the changeset description."""
351 351 s = ctx.description()
352 352 if isinstance(s, encoding.localstr):
353 353 # try hard to preserve utf-8 bytes
354 354 return encoding.tolocal(encoding.fromlocal(s).strip())
355 355 else:
356 356 return s.strip()
357 357
358 358 @templatekeyword('diffstat')
359 359 def showdiffstat(repo, ctx, templ, **args):
360 360 """String. Statistics of changes with the following format:
361 361 "modified files: +added/-removed lines"
362 362 """
363 363 stats = patch.diffstatdata(util.iterlines(ctx.diff(noprefix=False)))
364 364 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
365 365 return '%s: +%s/-%s' % (len(stats), adds, removes)
366 366
367 367 @templatekeyword('envvars')
368 368 def showenvvars(repo, **args):
369 369 """A dictionary of environment variables. (EXPERIMENTAL)"""
370 370 args = pycompat.byteskwargs(args)
371 371 env = repo.ui.exportableenviron()
372 372 env = util.sortdict((k, env[k]) for k in sorted(env))
373 373 return showdict('envvar', env, args, plural='envvars')
374 374
375 375 @templatekeyword('extras')
376 376 def showextras(**args):
377 377 """List of dicts with key, value entries of the 'extras'
378 378 field of this changeset."""
379 379 args = pycompat.byteskwargs(args)
380 380 extras = args['ctx'].extra()
381 381 extras = util.sortdict((k, extras[k]) for k in sorted(extras))
382 382 makemap = lambda k: {'key': k, 'value': extras[k]}
383 383 c = [makemap(k) for k in extras]
384 384 f = _showlist('extra', c, args, plural='extras')
385 385 return _hybrid(f, extras, makemap,
386 386 lambda x: '%s=%s' % (x['key'], util.escapestr(x['value'])))
387 387
388 388 @templatekeyword('file_adds')
389 389 def showfileadds(**args):
390 390 """List of strings. Files added by this changeset."""
391 391 args = pycompat.byteskwargs(args)
392 392 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
393 393 return showlist('file_add', getfiles(repo, ctx, revcache)[1], args,
394 394 element='file')
395 395
396 396 @templatekeyword('file_copies')
397 397 def showfilecopies(**args):
398 398 """List of strings. Files copied in this changeset with
399 399 their sources.
400 400 """
401 401 args = pycompat.byteskwargs(args)
402 402 cache, ctx = args['cache'], args['ctx']
403 403 copies = args['revcache'].get('copies')
404 404 if copies is None:
405 405 if 'getrenamed' not in cache:
406 406 cache['getrenamed'] = getrenamedfn(args['repo'])
407 407 copies = []
408 408 getrenamed = cache['getrenamed']
409 409 for fn in ctx.files():
410 410 rename = getrenamed(fn, ctx.rev())
411 411 if rename:
412 412 copies.append((fn, rename[0]))
413 413
414 414 copies = util.sortdict(copies)
415 415 return showdict('file_copy', copies, args, plural='file_copies',
416 416 key='name', value='source', fmt='%s (%s)')
417 417
418 418 # showfilecopiesswitch() displays file copies only if copy records are
419 419 # provided before calling the templater, usually with a --copies
420 420 # command line switch.
421 421 @templatekeyword('file_copies_switch')
422 422 def showfilecopiesswitch(**args):
423 423 """List of strings. Like "file_copies" but displayed
424 424 only if the --copied switch is set.
425 425 """
426 426 args = pycompat.byteskwargs(args)
427 427 copies = args['revcache'].get('copies') or []
428 428 copies = util.sortdict(copies)
429 429 return showdict('file_copy', copies, args, plural='file_copies',
430 430 key='name', value='source', fmt='%s (%s)')
431 431
432 432 @templatekeyword('file_dels')
433 433 def showfiledels(**args):
434 434 """List of strings. Files removed by this changeset."""
435 435 args = pycompat.byteskwargs(args)
436 436 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
437 437 return showlist('file_del', getfiles(repo, ctx, revcache)[2], args,
438 438 element='file')
439 439
440 440 @templatekeyword('file_mods')
441 441 def showfilemods(**args):
442 442 """List of strings. Files modified by this changeset."""
443 443 args = pycompat.byteskwargs(args)
444 444 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
445 445 return showlist('file_mod', getfiles(repo, ctx, revcache)[0], args,
446 446 element='file')
447 447
448 448 @templatekeyword('files')
449 449 def showfiles(**args):
450 450 """List of strings. All files modified, added, or removed by this
451 451 changeset.
452 452 """
453 453 args = pycompat.byteskwargs(args)
454 454 return showlist('file', args['ctx'].files(), args)
455 455
456 456 @templatekeyword('graphnode')
457 457 def showgraphnode(repo, ctx, **args):
458 458 """String. The character representing the changeset node in
459 459 an ASCII revision graph"""
460 460 wpnodes = repo.dirstate.parents()
461 461 if wpnodes[1] == nullid:
462 462 wpnodes = wpnodes[:1]
463 463 if ctx.node() in wpnodes:
464 464 return '@'
465 465 elif ctx.obsolete():
466 466 return 'x'
467 467 elif ctx.closesbranch():
468 468 return '_'
469 469 else:
470 470 return 'o'
471 471
472 @templatekeyword('graphwidth')
473 def showgraphwidth(repo, ctx, templ, **args):
474 """Integer. The width of the graph drawn by 'log --graph' or zero."""
475 # The value args['graphwidth'] will be this function, so we use an internal
476 # name to pass the value through props into this function.
477 return args.get('_graphwidth', 0)
478
472 479 @templatekeyword('index')
473 480 def showindex(**args):
474 481 """Integer. The current iteration of the loop. (0 indexed)"""
475 482 # just hosts documentation; should be overridden by template mapping
476 483 raise error.Abort(_("can't use index in this context"))
477 484
478 485 @templatekeyword('latesttag')
479 486 def showlatesttag(**args):
480 487 """List of strings. The global tags on the most recent globally
481 488 tagged ancestor of this changeset. If no such tags exist, the list
482 489 consists of the single string "null".
483 490 """
484 491 return showlatesttags(None, **args)
485 492
486 493 def showlatesttags(pattern, **args):
487 494 """helper method for the latesttag keyword and function"""
488 495 args = pycompat.byteskwargs(args)
489 496 repo, ctx = args['repo'], args['ctx']
490 497 cache = args['cache']
491 498 latesttags = getlatesttags(repo, ctx, cache, pattern)
492 499
493 500 # latesttag[0] is an implementation detail for sorting csets on different
494 501 # branches in a stable manner- it is the date the tagged cset was created,
495 502 # not the date the tag was created. Therefore it isn't made visible here.
496 503 makemap = lambda v: {
497 504 'changes': _showchangessincetag,
498 505 'distance': latesttags[1],
499 506 'latesttag': v, # BC with {latesttag % '{latesttag}'}
500 507 'tag': v
501 508 }
502 509
503 510 tags = latesttags[2]
504 511 f = _showlist('latesttag', tags, args, separator=':')
505 512 return _hybrid(f, tags, makemap, lambda x: x['latesttag'])
506 513
507 514 @templatekeyword('latesttagdistance')
508 515 def showlatesttagdistance(repo, ctx, templ, cache, **args):
509 516 """Integer. Longest path to the latest tag."""
510 517 return getlatesttags(repo, ctx, cache)[1]
511 518
512 519 @templatekeyword('changessincelatesttag')
513 520 def showchangessincelatesttag(repo, ctx, templ, cache, **args):
514 521 """Integer. All ancestors not in the latest tag."""
515 522 latesttag = getlatesttags(repo, ctx, cache)[2][0]
516 523
517 524 return _showchangessincetag(repo, ctx, tag=latesttag, **args)
518 525
519 526 def _showchangessincetag(repo, ctx, **args):
520 527 offset = 0
521 528 revs = [ctx.rev()]
522 529 tag = args[r'tag']
523 530
524 531 # The only() revset doesn't currently support wdir()
525 532 if ctx.rev() is None:
526 533 offset = 1
527 534 revs = [p.rev() for p in ctx.parents()]
528 535
529 536 return len(repo.revs('only(%ld, %s)', revs, tag)) + offset
530 537
531 538 @templatekeyword('manifest')
532 539 def showmanifest(**args):
533 540 repo, ctx, templ = args[r'repo'], args[r'ctx'], args[r'templ']
534 541 mnode = ctx.manifestnode()
535 542 if mnode is None:
536 543 # just avoid crash, we might want to use the 'ff...' hash in future
537 544 return
538 545 args = args.copy()
539 546 args.update({r'rev': repo.manifestlog._revlog.rev(mnode),
540 547 r'node': hex(mnode)})
541 548 return templ('manifest', **args)
542 549
543 550 def shownames(namespace, **args):
544 551 """helper method to generate a template keyword for a namespace"""
545 552 args = pycompat.byteskwargs(args)
546 553 ctx = args['ctx']
547 554 repo = ctx.repo()
548 555 ns = repo.names[namespace]
549 556 names = ns.names(repo, ctx.node())
550 557 return showlist(ns.templatename, names, args, plural=namespace)
551 558
552 559 @templatekeyword('namespaces')
553 560 def shownamespaces(**args):
554 561 """Dict of lists. Names attached to this changeset per
555 562 namespace."""
556 563 args = pycompat.byteskwargs(args)
557 564 ctx = args['ctx']
558 565 repo = ctx.repo()
559 566
560 567 namespaces = util.sortdict()
561 568 colornames = {}
562 569 builtins = {}
563 570
564 571 for k, ns in repo.names.iteritems():
565 572 namespaces[k] = showlist('name', ns.names(repo, ctx.node()), args)
566 573 colornames[k] = ns.colorname
567 574 builtins[k] = ns.builtin
568 575
569 576 f = _showlist('namespace', list(namespaces), args)
570 577
571 578 def makemap(ns):
572 579 return {
573 580 'namespace': ns,
574 581 'names': namespaces[ns],
575 582 'builtin': builtins[ns],
576 583 'colorname': colornames[ns],
577 584 }
578 585
579 586 return _hybrid(f, namespaces, makemap, lambda x: x['namespace'])
580 587
581 588 @templatekeyword('node')
582 589 def shownode(repo, ctx, templ, **args):
583 590 """String. The changeset identification hash, as a 40 hexadecimal
584 591 digit string.
585 592 """
586 593 return ctx.hex()
587 594
588 595 @templatekeyword('obsolete')
589 596 def showobsolete(repo, ctx, templ, **args):
590 597 """String. Whether the changeset is obsolete.
591 598 """
592 599 if ctx.obsolete():
593 600 return 'obsolete'
594 601 return ''
595 602
596 603 @templatekeyword('peerpaths')
597 604 def showpeerpaths(repo, **args):
598 605 """A dictionary of repository locations defined in the [paths] section
599 606 of your configuration file. (EXPERIMENTAL)"""
600 607 # see commands.paths() for naming of dictionary keys
601 608 paths = util.sortdict()
602 609 for k, p in sorted(repo.ui.paths.iteritems()):
603 610 d = util.sortdict()
604 611 d['url'] = p.rawloc
605 612 d.update((o, v) for o, v in sorted(p.suboptions.iteritems()))
606 613 def f():
607 614 yield d['url']
608 615 paths[k] = hybriddict(d, gen=f())
609 616
610 617 # no hybriddict() since d['path'] can't be formatted as a string. perhaps
611 618 # hybriddict() should call templatefilters.stringify(d[value]).
612 619 return _hybrid(None, paths, lambda k: {'name': k, 'path': paths[k]},
613 620 lambda d: '%s=%s' % (d['name'], d['path']['url']))
614 621
615 622 @templatekeyword("predecessors")
616 623 def showpredecessors(repo, ctx, **args):
617 624 """Returns the list if the closest visible successors
618 625 """
619 626 predecessors = sorted(obsutil.closestpredecessors(repo, ctx.node()))
620 627 predecessors = map(hex, predecessors)
621 628
622 629 return _hybrid(None, predecessors,
623 630 lambda x: {'ctx': repo[x], 'revcache': {}},
624 631 lambda d: _formatrevnode(d['ctx']))
625 632
626 633 @templatekeyword("successorssets")
627 634 def showsuccessorssets(repo, ctx, **args):
628 635 """Returns a string of sets of successors for a changectx
629 636
630 637 Format used is: [ctx1, ctx2], [ctx3] if ctx has been splitted into ctx1 and
631 638 ctx2 while also diverged into ctx3"""
632 639 if not ctx.obsolete():
633 640 return ''
634 641 args = pycompat.byteskwargs(args)
635 642
636 643 ssets = obsutil.successorssets(repo, ctx.node(), closest=True)
637 644 ssets = [[hex(n) for n in ss] for ss in ssets]
638 645
639 646 data = []
640 647 for ss in ssets:
641 648 h = _hybrid(None, ss, lambda x: {'ctx': repo[x], 'revcache': {}},
642 649 lambda d: _formatrevnode(d['ctx']))
643 650 data.append(h)
644 651
645 652 # Format the successorssets
646 653 def render(d):
647 654 t = []
648 655 for i in d.gen:
649 656 t.append(i)
650 657 return "".join(t)
651 658
652 659 def gen(data):
653 660 yield "; ".join(render(d) for d in data)
654 661
655 662 return _hybrid(gen(data), data, lambda x: {'successorset': x},
656 663 lambda d: d["successorset"])
657 664
658 665 @templatekeyword('p1rev')
659 666 def showp1rev(repo, ctx, templ, **args):
660 667 """Integer. The repository-local revision number of the changeset's
661 668 first parent, or -1 if the changeset has no parents."""
662 669 return ctx.p1().rev()
663 670
664 671 @templatekeyword('p2rev')
665 672 def showp2rev(repo, ctx, templ, **args):
666 673 """Integer. The repository-local revision number of the changeset's
667 674 second parent, or -1 if the changeset has no second parent."""
668 675 return ctx.p2().rev()
669 676
670 677 @templatekeyword('p1node')
671 678 def showp1node(repo, ctx, templ, **args):
672 679 """String. The identification hash of the changeset's first parent,
673 680 as a 40 digit hexadecimal string. If the changeset has no parents, all
674 681 digits are 0."""
675 682 return ctx.p1().hex()
676 683
677 684 @templatekeyword('p2node')
678 685 def showp2node(repo, ctx, templ, **args):
679 686 """String. The identification hash of the changeset's second
680 687 parent, as a 40 digit hexadecimal string. If the changeset has no second
681 688 parent, all digits are 0."""
682 689 return ctx.p2().hex()
683 690
684 691 @templatekeyword('parents')
685 692 def showparents(**args):
686 693 """List of strings. The parents of the changeset in "rev:node"
687 694 format. If the changeset has only one "natural" parent (the predecessor
688 695 revision) nothing is shown."""
689 696 args = pycompat.byteskwargs(args)
690 697 repo = args['repo']
691 698 ctx = args['ctx']
692 699 pctxs = scmutil.meaningfulparents(repo, ctx)
693 700 # ifcontains() needs a list of str
694 701 prevs = ["%d" % p.rev() for p in pctxs]
695 702 parents = [[('rev', p.rev()),
696 703 ('node', p.hex()),
697 704 ('phase', p.phasestr())]
698 705 for p in pctxs]
699 706 f = _showlist('parent', parents, args)
700 707 return _hybrid(f, prevs, lambda x: {'ctx': repo[int(x)], 'revcache': {}},
701 708 lambda d: _formatrevnode(d['ctx']))
702 709
703 710 @templatekeyword('phase')
704 711 def showphase(repo, ctx, templ, **args):
705 712 """String. The changeset phase name."""
706 713 return ctx.phasestr()
707 714
708 715 @templatekeyword('phaseidx')
709 716 def showphaseidx(repo, ctx, templ, **args):
710 717 """Integer. The changeset phase index."""
711 718 return ctx.phase()
712 719
713 720 @templatekeyword('rev')
714 721 def showrev(repo, ctx, templ, **args):
715 722 """Integer. The repository-local changeset revision number."""
716 723 return scmutil.intrev(ctx)
717 724
718 725 def showrevslist(name, revs, **args):
719 726 """helper to generate a list of revisions in which a mapped template will
720 727 be evaluated"""
721 728 args = pycompat.byteskwargs(args)
722 729 repo = args['ctx'].repo()
723 730 # ifcontains() needs a list of str
724 731 revs = ["%d" % r for r in revs]
725 732 f = _showlist(name, revs, args)
726 733 return _hybrid(f, revs,
727 734 lambda x: {name: x, 'ctx': repo[int(x)], 'revcache': {}},
728 735 lambda d: d[name])
729 736
730 737 @templatekeyword('subrepos')
731 738 def showsubrepos(**args):
732 739 """List of strings. Updated subrepositories in the changeset."""
733 740 args = pycompat.byteskwargs(args)
734 741 ctx = args['ctx']
735 742 substate = ctx.substate
736 743 if not substate:
737 744 return showlist('subrepo', [], args)
738 745 psubstate = ctx.parents()[0].substate or {}
739 746 subrepos = []
740 747 for sub in substate:
741 748 if sub not in psubstate or substate[sub] != psubstate[sub]:
742 749 subrepos.append(sub) # modified or newly added in ctx
743 750 for sub in psubstate:
744 751 if sub not in substate:
745 752 subrepos.append(sub) # removed in ctx
746 753 return showlist('subrepo', sorted(subrepos), args)
747 754
748 755 # don't remove "showtags" definition, even though namespaces will put
749 756 # a helper function for "tags" keyword into "keywords" map automatically,
750 757 # because online help text is built without namespaces initialization
751 758 @templatekeyword('tags')
752 759 def showtags(**args):
753 760 """List of strings. Any tags associated with the changeset."""
754 761 return shownames('tags', **args)
755 762
756 763 def loadkeyword(ui, extname, registrarobj):
757 764 """Load template keyword from specified registrarobj
758 765 """
759 766 for name, func in registrarobj._table.iteritems():
760 767 keywords[name] = func
761 768
762 769 @templatekeyword('termwidth')
763 770 def showtermwidth(repo, ctx, templ, **args):
764 771 """Integer. The width of the current terminal."""
765 772 return repo.ui.termwidth()
766 773
767 774 @templatekeyword('troubles')
768 775 def showtroubles(repo, **args):
769 776 """List of strings. Evolution troubles affecting the changeset.
770 777
771 778 (DEPRECATED)
772 779 """
773 780 msg = ("'troubles' is deprecated, "
774 781 "use 'instabilities'")
775 782 repo.ui.deprecwarn(msg, '4.4')
776 783
777 784 return showinstabilities(repo=repo, **args)
778 785
779 786 @templatekeyword('instabilities')
780 787 def showinstabilities(**args):
781 788 """List of strings. Evolution instabilities affecting the changeset.
782 789
783 790 (EXPERIMENTAL)
784 791 """
785 792 args = pycompat.byteskwargs(args)
786 793 return showlist('instability', args['ctx'].instabilities(), args,
787 794 plural='instabilities')
788 795
789 796 # tell hggettext to extract docstrings from these functions:
790 797 i18nfunctions = keywords.values()
@@ -1,4321 +1,4473 b''
1 1 $ hg init a
2 2 $ cd a
3 3 $ echo a > a
4 4 $ hg add a
5 5 $ echo line 1 > b
6 6 $ echo line 2 >> b
7 7 $ hg commit -l b -d '1000000 0' -u 'User Name <user@hostname>'
8 8
9 9 $ hg add b
10 10 $ echo other 1 > c
11 11 $ echo other 2 >> c
12 12 $ echo >> c
13 13 $ echo other 3 >> c
14 14 $ hg commit -l c -d '1100000 0' -u 'A. N. Other <other@place>'
15 15
16 16 $ hg add c
17 17 $ hg commit -m 'no person' -d '1200000 0' -u 'other@place'
18 18 $ echo c >> c
19 19 $ hg commit -m 'no user, no domain' -d '1300000 0' -u 'person'
20 20
21 21 $ echo foo > .hg/branch
22 22 $ hg commit -m 'new branch' -d '1400000 0' -u 'person'
23 23
24 24 $ hg co -q 3
25 25 $ echo other 4 >> d
26 26 $ hg add d
27 27 $ hg commit -m 'new head' -d '1500000 0' -u 'person'
28 28
29 29 $ hg merge -q foo
30 30 $ hg commit -m 'merge' -d '1500001 0' -u 'person'
31 31
32 32 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 Keyword arguments:
138 138
139 139 $ hg debugtemplate -r0 -v '{foo=bar|baz}'
140 140 (template
141 141 (keyvalue
142 142 ('symbol', 'foo')
143 143 (|
144 144 ('symbol', 'bar')
145 145 ('symbol', 'baz'))))
146 146 hg: parse error: can't use a key-value pair in this context
147 147 [255]
148 148
149 149 $ hg debugtemplate '{pad("foo", width=10, left=true)}\n'
150 150 foo
151 151
152 152 Call function which takes named arguments by filter syntax:
153 153
154 154 $ hg debugtemplate '{" "|separate}'
155 155 $ hg debugtemplate '{("not", "an", "argument", "list")|separate}'
156 156 hg: parse error: unknown method 'list'
157 157 [255]
158 158
159 159 Second branch starting at nullrev:
160 160
161 161 $ hg update null
162 162 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
163 163 $ echo second > second
164 164 $ hg add second
165 165 $ hg commit -m second -d '1000000 0' -u 'User Name <user@hostname>'
166 166 created new head
167 167
168 168 $ echo third > third
169 169 $ hg add third
170 170 $ hg mv second fourth
171 171 $ hg commit -m third -d "2020-01-01 10:01"
172 172
173 173 $ hg log --template '{join(file_copies, ",\n")}\n' -r .
174 174 fourth (second)
175 175 $ hg log -T '{file_copies % "{source} -> {name}\n"}' -r .
176 176 second -> fourth
177 177 $ hg log -T '{rev} {ifcontains("fourth", file_copies, "t", "f")}\n' -r .:7
178 178 8 t
179 179 7 f
180 180
181 181 Working-directory revision has special identifiers, though they are still
182 182 experimental:
183 183
184 184 $ hg log -r 'wdir()' -T '{rev}:{node}\n'
185 185 2147483647:ffffffffffffffffffffffffffffffffffffffff
186 186
187 187 Some keywords are invalid for working-directory revision, but they should
188 188 never cause crash:
189 189
190 190 $ hg log -r 'wdir()' -T '{manifest}\n'
191 191
192 192
193 193 Quoting for ui.logtemplate
194 194
195 195 $ hg tip --config "ui.logtemplate={rev}\n"
196 196 8
197 197 $ hg tip --config "ui.logtemplate='{rev}\n'"
198 198 8
199 199 $ hg tip --config 'ui.logtemplate="{rev}\n"'
200 200 8
201 201 $ hg tip --config 'ui.logtemplate=n{rev}\n'
202 202 n8
203 203
204 204 Make sure user/global hgrc does not affect tests
205 205
206 206 $ echo '[ui]' > .hg/hgrc
207 207 $ echo 'logtemplate =' >> .hg/hgrc
208 208 $ echo 'style =' >> .hg/hgrc
209 209
210 210 Add some simple styles to settings
211 211
212 212 $ cat <<'EOF' >> .hg/hgrc
213 213 > [templates]
214 214 > simple = "{rev}\n"
215 215 > simple2 = {rev}\n
216 216 > rev = "should not precede {rev} keyword\n"
217 217 > EOF
218 218
219 219 $ hg log -l1 -Tsimple
220 220 8
221 221 $ hg log -l1 -Tsimple2
222 222 8
223 223 $ hg log -l1 -Trev
224 224 should not precede 8 keyword
225 225 $ hg log -l1 -T '{simple}'
226 226 8
227 227
228 228 Map file shouldn't see user templates:
229 229
230 230 $ cat <<EOF > tmpl
231 231 > changeset = 'nothing expanded:{simple}\n'
232 232 > EOF
233 233 $ hg log -l1 --style ./tmpl
234 234 nothing expanded:
235 235
236 236 Test templates and style maps in files:
237 237
238 238 $ echo "{rev}" > tmpl
239 239 $ hg log -l1 -T./tmpl
240 240 8
241 241 $ hg log -l1 -Tblah/blah
242 242 blah/blah (no-eol)
243 243
244 244 $ printf 'changeset = "{rev}\\n"\n' > map-simple
245 245 $ hg log -l1 -T./map-simple
246 246 8
247 247
248 248 Test template map inheritance
249 249
250 250 $ echo "__base__ = map-cmdline.default" > map-simple
251 251 $ printf 'cset = "changeset: ***{rev}***\\n"\n' >> map-simple
252 252 $ hg log -l1 -T./map-simple
253 253 changeset: ***8***
254 254 tag: tip
255 255 user: test
256 256 date: Wed Jan 01 10:01:00 2020 +0000
257 257 summary: third
258 258
259 259
260 260 Test docheader, docfooter and separator in template map
261 261
262 262 $ cat <<'EOF' > map-myjson
263 263 > docheader = '\{\n'
264 264 > docfooter = '\n}\n'
265 265 > separator = ',\n'
266 266 > changeset = ' {dict(rev, node|short)|json}'
267 267 > EOF
268 268 $ hg log -l2 -T./map-myjson
269 269 {
270 270 {"node": "95c24699272e", "rev": 8},
271 271 {"node": "29114dbae42b", "rev": 7}
272 272 }
273 273
274 274 Test docheader, docfooter and separator in [templates] section
275 275
276 276 $ cat <<'EOF' >> .hg/hgrc
277 277 > [templates]
278 278 > myjson = ' {dict(rev, node|short)|json}'
279 279 > myjson:docheader = '\{\n'
280 280 > myjson:docfooter = '\n}\n'
281 281 > myjson:separator = ',\n'
282 282 > :docheader = 'should not be selected as a docheader for literal templates\n'
283 283 > EOF
284 284 $ hg log -l2 -Tmyjson
285 285 {
286 286 {"node": "95c24699272e", "rev": 8},
287 287 {"node": "29114dbae42b", "rev": 7}
288 288 }
289 289 $ hg log -l1 -T'{rev}\n'
290 290 8
291 291
292 292 Template should precede style option
293 293
294 294 $ hg log -l1 --style default -T '{rev}\n'
295 295 8
296 296
297 297 Add a commit with empty description, to ensure that the templates
298 298 below will omit the description line.
299 299
300 300 $ echo c >> c
301 301 $ hg add c
302 302 $ hg commit -qm ' '
303 303
304 304 Default style is like normal output. Phases style should be the same
305 305 as default style, except for extra phase lines.
306 306
307 307 $ hg log > log.out
308 308 $ hg log --style default > style.out
309 309 $ cmp log.out style.out || diff -u log.out style.out
310 310 $ hg log -T phases > phases.out
311 311 $ diff -U 0 log.out phases.out | egrep -v '^---|^\+\+\+|^@@'
312 312 +phase: draft
313 313 +phase: draft
314 314 +phase: draft
315 315 +phase: draft
316 316 +phase: draft
317 317 +phase: draft
318 318 +phase: draft
319 319 +phase: draft
320 320 +phase: draft
321 321 +phase: draft
322 322
323 323 $ hg log -v > log.out
324 324 $ hg log -v --style default > style.out
325 325 $ cmp log.out style.out || diff -u log.out style.out
326 326 $ hg log -v -T phases > phases.out
327 327 $ diff -U 0 log.out phases.out | egrep -v '^---|^\+\+\+|^@@'
328 328 +phase: draft
329 329 +phase: draft
330 330 +phase: draft
331 331 +phase: draft
332 332 +phase: draft
333 333 +phase: draft
334 334 +phase: draft
335 335 +phase: draft
336 336 +phase: draft
337 337 +phase: draft
338 338
339 339 $ hg log -q > log.out
340 340 $ hg log -q --style default > style.out
341 341 $ cmp log.out style.out || diff -u log.out style.out
342 342 $ hg log -q -T phases > phases.out
343 343 $ cmp log.out phases.out || diff -u log.out phases.out
344 344
345 345 $ hg log --debug > log.out
346 346 $ hg log --debug --style default > style.out
347 347 $ cmp log.out style.out || diff -u log.out style.out
348 348 $ hg log --debug -T phases > phases.out
349 349 $ cmp log.out phases.out || diff -u log.out phases.out
350 350
351 351 Default style of working-directory revision should also be the same (but
352 352 date may change while running tests):
353 353
354 354 $ hg log -r 'wdir()' | sed 's|^date:.*|date:|' > log.out
355 355 $ hg log -r 'wdir()' --style default | sed 's|^date:.*|date:|' > style.out
356 356 $ cmp log.out style.out || diff -u log.out style.out
357 357
358 358 $ hg log -r 'wdir()' -v | sed 's|^date:.*|date:|' > log.out
359 359 $ hg log -r 'wdir()' -v --style default | sed 's|^date:.*|date:|' > style.out
360 360 $ cmp log.out style.out || diff -u log.out style.out
361 361
362 362 $ hg log -r 'wdir()' -q > log.out
363 363 $ hg log -r 'wdir()' -q --style default > style.out
364 364 $ cmp log.out style.out || diff -u log.out style.out
365 365
366 366 $ hg log -r 'wdir()' --debug | sed 's|^date:.*|date:|' > log.out
367 367 $ hg log -r 'wdir()' --debug --style default \
368 368 > | sed 's|^date:.*|date:|' > style.out
369 369 $ cmp log.out style.out || diff -u log.out style.out
370 370
371 371 Default style should also preserve color information (issue2866):
372 372
373 373 $ cp $HGRCPATH $HGRCPATH-bak
374 374 $ cat <<EOF >> $HGRCPATH
375 375 > [extensions]
376 376 > color=
377 377 > EOF
378 378
379 379 $ hg --color=debug log > log.out
380 380 $ hg --color=debug log --style default > style.out
381 381 $ cmp log.out style.out || diff -u log.out style.out
382 382 $ hg --color=debug log -T phases > phases.out
383 383 $ diff -U 0 log.out phases.out | egrep -v '^---|^\+\+\+|^@@'
384 384 +[log.phase|phase: draft]
385 385 +[log.phase|phase: draft]
386 386 +[log.phase|phase: draft]
387 387 +[log.phase|phase: draft]
388 388 +[log.phase|phase: draft]
389 389 +[log.phase|phase: draft]
390 390 +[log.phase|phase: draft]
391 391 +[log.phase|phase: draft]
392 392 +[log.phase|phase: draft]
393 393 +[log.phase|phase: draft]
394 394
395 395 $ hg --color=debug -v log > log.out
396 396 $ hg --color=debug -v log --style default > style.out
397 397 $ cmp log.out style.out || diff -u log.out style.out
398 398 $ hg --color=debug -v log -T phases > phases.out
399 399 $ diff -U 0 log.out phases.out | egrep -v '^---|^\+\+\+|^@@'
400 400 +[log.phase|phase: draft]
401 401 +[log.phase|phase: draft]
402 402 +[log.phase|phase: draft]
403 403 +[log.phase|phase: draft]
404 404 +[log.phase|phase: draft]
405 405 +[log.phase|phase: draft]
406 406 +[log.phase|phase: draft]
407 407 +[log.phase|phase: draft]
408 408 +[log.phase|phase: draft]
409 409 +[log.phase|phase: draft]
410 410
411 411 $ hg --color=debug -q log > log.out
412 412 $ hg --color=debug -q log --style default > style.out
413 413 $ cmp log.out style.out || diff -u log.out style.out
414 414 $ hg --color=debug -q log -T phases > phases.out
415 415 $ cmp log.out phases.out || diff -u log.out phases.out
416 416
417 417 $ hg --color=debug --debug log > log.out
418 418 $ hg --color=debug --debug log --style default > style.out
419 419 $ cmp log.out style.out || diff -u log.out style.out
420 420 $ hg --color=debug --debug log -T phases > phases.out
421 421 $ cmp log.out phases.out || diff -u log.out phases.out
422 422
423 423 $ mv $HGRCPATH-bak $HGRCPATH
424 424
425 425 Remove commit with empty commit message, so as to not pollute further
426 426 tests.
427 427
428 428 $ hg --config extensions.strip= strip -q .
429 429
430 430 Revision with no copies (used to print a traceback):
431 431
432 432 $ hg tip -v --template '\n'
433 433
434 434
435 435 Compact style works:
436 436
437 437 $ hg log -Tcompact
438 438 8[tip] 95c24699272e 2020-01-01 10:01 +0000 test
439 439 third
440 440
441 441 7:-1 29114dbae42b 1970-01-12 13:46 +0000 user
442 442 second
443 443
444 444 6:5,4 d41e714fe50d 1970-01-18 08:40 +0000 person
445 445 merge
446 446
447 447 5:3 13207e5a10d9 1970-01-18 08:40 +0000 person
448 448 new head
449 449
450 450 4 bbe44766e73d 1970-01-17 04:53 +0000 person
451 451 new branch
452 452
453 453 3 10e46f2dcbf4 1970-01-16 01:06 +0000 person
454 454 no user, no domain
455 455
456 456 2 97054abb4ab8 1970-01-14 21:20 +0000 other
457 457 no person
458 458
459 459 1 b608e9d1a3f0 1970-01-13 17:33 +0000 other
460 460 other 1
461 461
462 462 0 1e4e1b8f71e0 1970-01-12 13:46 +0000 user
463 463 line 1
464 464
465 465
466 466 $ hg log -v --style compact
467 467 8[tip] 95c24699272e 2020-01-01 10:01 +0000 test
468 468 third
469 469
470 470 7:-1 29114dbae42b 1970-01-12 13:46 +0000 User Name <user@hostname>
471 471 second
472 472
473 473 6:5,4 d41e714fe50d 1970-01-18 08:40 +0000 person
474 474 merge
475 475
476 476 5:3 13207e5a10d9 1970-01-18 08:40 +0000 person
477 477 new head
478 478
479 479 4 bbe44766e73d 1970-01-17 04:53 +0000 person
480 480 new branch
481 481
482 482 3 10e46f2dcbf4 1970-01-16 01:06 +0000 person
483 483 no user, no domain
484 484
485 485 2 97054abb4ab8 1970-01-14 21:20 +0000 other@place
486 486 no person
487 487
488 488 1 b608e9d1a3f0 1970-01-13 17:33 +0000 A. N. Other <other@place>
489 489 other 1
490 490 other 2
491 491
492 492 other 3
493 493
494 494 0 1e4e1b8f71e0 1970-01-12 13:46 +0000 User Name <user@hostname>
495 495 line 1
496 496 line 2
497 497
498 498
499 499 $ hg log --debug --style compact
500 500 8[tip]:7,-1 95c24699272e 2020-01-01 10:01 +0000 test
501 501 third
502 502
503 503 7:-1,-1 29114dbae42b 1970-01-12 13:46 +0000 User Name <user@hostname>
504 504 second
505 505
506 506 6:5,4 d41e714fe50d 1970-01-18 08:40 +0000 person
507 507 merge
508 508
509 509 5:3,-1 13207e5a10d9 1970-01-18 08:40 +0000 person
510 510 new head
511 511
512 512 4:3,-1 bbe44766e73d 1970-01-17 04:53 +0000 person
513 513 new branch
514 514
515 515 3:2,-1 10e46f2dcbf4 1970-01-16 01:06 +0000 person
516 516 no user, no domain
517 517
518 518 2:1,-1 97054abb4ab8 1970-01-14 21:20 +0000 other@place
519 519 no person
520 520
521 521 1:0,-1 b608e9d1a3f0 1970-01-13 17:33 +0000 A. N. Other <other@place>
522 522 other 1
523 523 other 2
524 524
525 525 other 3
526 526
527 527 0:-1,-1 1e4e1b8f71e0 1970-01-12 13:46 +0000 User Name <user@hostname>
528 528 line 1
529 529 line 2
530 530
531 531
532 532 Test xml styles:
533 533
534 534 $ hg log --style xml -r 'not all()'
535 535 <?xml version="1.0"?>
536 536 <log>
537 537 </log>
538 538
539 539 $ hg log --style xml
540 540 <?xml version="1.0"?>
541 541 <log>
542 542 <logentry revision="8" node="95c24699272ef57d062b8bccc32c878bf841784a">
543 543 <tag>tip</tag>
544 544 <author email="test">test</author>
545 545 <date>2020-01-01T10:01:00+00:00</date>
546 546 <msg xml:space="preserve">third</msg>
547 547 </logentry>
548 548 <logentry revision="7" node="29114dbae42b9f078cf2714dbe3a86bba8ec7453">
549 549 <parent revision="-1" node="0000000000000000000000000000000000000000" />
550 550 <author email="user@hostname">User Name</author>
551 551 <date>1970-01-12T13:46:40+00:00</date>
552 552 <msg xml:space="preserve">second</msg>
553 553 </logentry>
554 554 <logentry revision="6" node="d41e714fe50d9e4a5f11b4d595d543481b5f980b">
555 555 <parent revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f" />
556 556 <parent revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74" />
557 557 <author email="person">person</author>
558 558 <date>1970-01-18T08:40:01+00:00</date>
559 559 <msg xml:space="preserve">merge</msg>
560 560 </logentry>
561 561 <logentry revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f">
562 562 <parent revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47" />
563 563 <author email="person">person</author>
564 564 <date>1970-01-18T08:40:00+00:00</date>
565 565 <msg xml:space="preserve">new head</msg>
566 566 </logentry>
567 567 <logentry revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74">
568 568 <branch>foo</branch>
569 569 <author email="person">person</author>
570 570 <date>1970-01-17T04:53:20+00:00</date>
571 571 <msg xml:space="preserve">new branch</msg>
572 572 </logentry>
573 573 <logentry revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47">
574 574 <author email="person">person</author>
575 575 <date>1970-01-16T01:06:40+00:00</date>
576 576 <msg xml:space="preserve">no user, no domain</msg>
577 577 </logentry>
578 578 <logentry revision="2" node="97054abb4ab824450e9164180baf491ae0078465">
579 579 <author email="other@place">other</author>
580 580 <date>1970-01-14T21:20:00+00:00</date>
581 581 <msg xml:space="preserve">no person</msg>
582 582 </logentry>
583 583 <logentry revision="1" node="b608e9d1a3f0273ccf70fb85fd6866b3482bf965">
584 584 <author email="other@place">A. N. Other</author>
585 585 <date>1970-01-13T17:33:20+00:00</date>
586 586 <msg xml:space="preserve">other 1
587 587 other 2
588 588
589 589 other 3</msg>
590 590 </logentry>
591 591 <logentry revision="0" node="1e4e1b8f71e05681d422154f5421e385fec3454f">
592 592 <author email="user@hostname">User Name</author>
593 593 <date>1970-01-12T13:46:40+00:00</date>
594 594 <msg xml:space="preserve">line 1
595 595 line 2</msg>
596 596 </logentry>
597 597 </log>
598 598
599 599 $ hg log -v --style xml
600 600 <?xml version="1.0"?>
601 601 <log>
602 602 <logentry revision="8" node="95c24699272ef57d062b8bccc32c878bf841784a">
603 603 <tag>tip</tag>
604 604 <author email="test">test</author>
605 605 <date>2020-01-01T10:01:00+00:00</date>
606 606 <msg xml:space="preserve">third</msg>
607 607 <paths>
608 608 <path action="A">fourth</path>
609 609 <path action="A">third</path>
610 610 <path action="R">second</path>
611 611 </paths>
612 612 <copies>
613 613 <copy source="second">fourth</copy>
614 614 </copies>
615 615 </logentry>
616 616 <logentry revision="7" node="29114dbae42b9f078cf2714dbe3a86bba8ec7453">
617 617 <parent revision="-1" node="0000000000000000000000000000000000000000" />
618 618 <author email="user@hostname">User Name</author>
619 619 <date>1970-01-12T13:46:40+00:00</date>
620 620 <msg xml:space="preserve">second</msg>
621 621 <paths>
622 622 <path action="A">second</path>
623 623 </paths>
624 624 </logentry>
625 625 <logentry revision="6" node="d41e714fe50d9e4a5f11b4d595d543481b5f980b">
626 626 <parent revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f" />
627 627 <parent revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74" />
628 628 <author email="person">person</author>
629 629 <date>1970-01-18T08:40:01+00:00</date>
630 630 <msg xml:space="preserve">merge</msg>
631 631 <paths>
632 632 </paths>
633 633 </logentry>
634 634 <logentry revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f">
635 635 <parent revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47" />
636 636 <author email="person">person</author>
637 637 <date>1970-01-18T08:40:00+00:00</date>
638 638 <msg xml:space="preserve">new head</msg>
639 639 <paths>
640 640 <path action="A">d</path>
641 641 </paths>
642 642 </logentry>
643 643 <logentry revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74">
644 644 <branch>foo</branch>
645 645 <author email="person">person</author>
646 646 <date>1970-01-17T04:53:20+00:00</date>
647 647 <msg xml:space="preserve">new branch</msg>
648 648 <paths>
649 649 </paths>
650 650 </logentry>
651 651 <logentry revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47">
652 652 <author email="person">person</author>
653 653 <date>1970-01-16T01:06:40+00:00</date>
654 654 <msg xml:space="preserve">no user, no domain</msg>
655 655 <paths>
656 656 <path action="M">c</path>
657 657 </paths>
658 658 </logentry>
659 659 <logentry revision="2" node="97054abb4ab824450e9164180baf491ae0078465">
660 660 <author email="other@place">other</author>
661 661 <date>1970-01-14T21:20:00+00:00</date>
662 662 <msg xml:space="preserve">no person</msg>
663 663 <paths>
664 664 <path action="A">c</path>
665 665 </paths>
666 666 </logentry>
667 667 <logentry revision="1" node="b608e9d1a3f0273ccf70fb85fd6866b3482bf965">
668 668 <author email="other@place">A. N. Other</author>
669 669 <date>1970-01-13T17:33:20+00:00</date>
670 670 <msg xml:space="preserve">other 1
671 671 other 2
672 672
673 673 other 3</msg>
674 674 <paths>
675 675 <path action="A">b</path>
676 676 </paths>
677 677 </logentry>
678 678 <logentry revision="0" node="1e4e1b8f71e05681d422154f5421e385fec3454f">
679 679 <author email="user@hostname">User Name</author>
680 680 <date>1970-01-12T13:46:40+00:00</date>
681 681 <msg xml:space="preserve">line 1
682 682 line 2</msg>
683 683 <paths>
684 684 <path action="A">a</path>
685 685 </paths>
686 686 </logentry>
687 687 </log>
688 688
689 689 $ hg log --debug --style xml
690 690 <?xml version="1.0"?>
691 691 <log>
692 692 <logentry revision="8" node="95c24699272ef57d062b8bccc32c878bf841784a">
693 693 <tag>tip</tag>
694 694 <parent revision="7" node="29114dbae42b9f078cf2714dbe3a86bba8ec7453" />
695 695 <parent revision="-1" node="0000000000000000000000000000000000000000" />
696 696 <author email="test">test</author>
697 697 <date>2020-01-01T10:01:00+00:00</date>
698 698 <msg xml:space="preserve">third</msg>
699 699 <paths>
700 700 <path action="A">fourth</path>
701 701 <path action="A">third</path>
702 702 <path action="R">second</path>
703 703 </paths>
704 704 <copies>
705 705 <copy source="second">fourth</copy>
706 706 </copies>
707 707 <extra key="branch">default</extra>
708 708 </logentry>
709 709 <logentry revision="7" node="29114dbae42b9f078cf2714dbe3a86bba8ec7453">
710 710 <parent revision="-1" node="0000000000000000000000000000000000000000" />
711 711 <parent revision="-1" node="0000000000000000000000000000000000000000" />
712 712 <author email="user@hostname">User Name</author>
713 713 <date>1970-01-12T13:46:40+00:00</date>
714 714 <msg xml:space="preserve">second</msg>
715 715 <paths>
716 716 <path action="A">second</path>
717 717 </paths>
718 718 <extra key="branch">default</extra>
719 719 </logentry>
720 720 <logentry revision="6" node="d41e714fe50d9e4a5f11b4d595d543481b5f980b">
721 721 <parent revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f" />
722 722 <parent revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74" />
723 723 <author email="person">person</author>
724 724 <date>1970-01-18T08:40:01+00:00</date>
725 725 <msg xml:space="preserve">merge</msg>
726 726 <paths>
727 727 </paths>
728 728 <extra key="branch">default</extra>
729 729 </logentry>
730 730 <logentry revision="5" node="13207e5a10d9fd28ec424934298e176197f2c67f">
731 731 <parent revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47" />
732 732 <parent revision="-1" node="0000000000000000000000000000000000000000" />
733 733 <author email="person">person</author>
734 734 <date>1970-01-18T08:40:00+00:00</date>
735 735 <msg xml:space="preserve">new head</msg>
736 736 <paths>
737 737 <path action="A">d</path>
738 738 </paths>
739 739 <extra key="branch">default</extra>
740 740 </logentry>
741 741 <logentry revision="4" node="bbe44766e73d5f11ed2177f1838de10c53ef3e74">
742 742 <branch>foo</branch>
743 743 <parent revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47" />
744 744 <parent revision="-1" node="0000000000000000000000000000000000000000" />
745 745 <author email="person">person</author>
746 746 <date>1970-01-17T04:53:20+00:00</date>
747 747 <msg xml:space="preserve">new branch</msg>
748 748 <paths>
749 749 </paths>
750 750 <extra key="branch">foo</extra>
751 751 </logentry>
752 752 <logentry revision="3" node="10e46f2dcbf4823578cf180f33ecf0b957964c47">
753 753 <parent revision="2" node="97054abb4ab824450e9164180baf491ae0078465" />
754 754 <parent revision="-1" node="0000000000000000000000000000000000000000" />
755 755 <author email="person">person</author>
756 756 <date>1970-01-16T01:06:40+00:00</date>
757 757 <msg xml:space="preserve">no user, no domain</msg>
758 758 <paths>
759 759 <path action="M">c</path>
760 760 </paths>
761 761 <extra key="branch">default</extra>
762 762 </logentry>
763 763 <logentry revision="2" node="97054abb4ab824450e9164180baf491ae0078465">
764 764 <parent revision="1" node="b608e9d1a3f0273ccf70fb85fd6866b3482bf965" />
765 765 <parent revision="-1" node="0000000000000000000000000000000000000000" />
766 766 <author email="other@place">other</author>
767 767 <date>1970-01-14T21:20:00+00:00</date>
768 768 <msg xml:space="preserve">no person</msg>
769 769 <paths>
770 770 <path action="A">c</path>
771 771 </paths>
772 772 <extra key="branch">default</extra>
773 773 </logentry>
774 774 <logentry revision="1" node="b608e9d1a3f0273ccf70fb85fd6866b3482bf965">
775 775 <parent revision="0" node="1e4e1b8f71e05681d422154f5421e385fec3454f" />
776 776 <parent revision="-1" node="0000000000000000000000000000000000000000" />
777 777 <author email="other@place">A. N. Other</author>
778 778 <date>1970-01-13T17:33:20+00:00</date>
779 779 <msg xml:space="preserve">other 1
780 780 other 2
781 781
782 782 other 3</msg>
783 783 <paths>
784 784 <path action="A">b</path>
785 785 </paths>
786 786 <extra key="branch">default</extra>
787 787 </logentry>
788 788 <logentry revision="0" node="1e4e1b8f71e05681d422154f5421e385fec3454f">
789 789 <parent revision="-1" node="0000000000000000000000000000000000000000" />
790 790 <parent revision="-1" node="0000000000000000000000000000000000000000" />
791 791 <author email="user@hostname">User Name</author>
792 792 <date>1970-01-12T13:46:40+00:00</date>
793 793 <msg xml:space="preserve">line 1
794 794 line 2</msg>
795 795 <paths>
796 796 <path action="A">a</path>
797 797 </paths>
798 798 <extra key="branch">default</extra>
799 799 </logentry>
800 800 </log>
801 801
802 802
803 803 Test JSON style:
804 804
805 805 $ hg log -k nosuch -Tjson
806 806 []
807 807
808 808 $ hg log -qr . -Tjson
809 809 [
810 810 {
811 811 "rev": 8,
812 812 "node": "95c24699272ef57d062b8bccc32c878bf841784a"
813 813 }
814 814 ]
815 815
816 816 $ hg log -vpr . -Tjson --stat
817 817 [
818 818 {
819 819 "rev": 8,
820 820 "node": "95c24699272ef57d062b8bccc32c878bf841784a",
821 821 "branch": "default",
822 822 "phase": "draft",
823 823 "user": "test",
824 824 "date": [1577872860, 0],
825 825 "desc": "third",
826 826 "bookmarks": [],
827 827 "tags": ["tip"],
828 828 "parents": ["29114dbae42b9f078cf2714dbe3a86bba8ec7453"],
829 829 "files": ["fourth", "second", "third"],
830 830 "diffstat": " fourth | 1 +\n second | 1 -\n third | 1 +\n 3 files changed, 2 insertions(+), 1 deletions(-)\n",
831 831 "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"
832 832 }
833 833 ]
834 834
835 835 honor --git but not format-breaking diffopts
836 836 $ hg --config diff.noprefix=True log --git -vpr . -Tjson
837 837 [
838 838 {
839 839 "rev": 8,
840 840 "node": "95c24699272ef57d062b8bccc32c878bf841784a",
841 841 "branch": "default",
842 842 "phase": "draft",
843 843 "user": "test",
844 844 "date": [1577872860, 0],
845 845 "desc": "third",
846 846 "bookmarks": [],
847 847 "tags": ["tip"],
848 848 "parents": ["29114dbae42b9f078cf2714dbe3a86bba8ec7453"],
849 849 "files": ["fourth", "second", "third"],
850 850 "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"
851 851 }
852 852 ]
853 853
854 854 $ hg log -T json
855 855 [
856 856 {
857 857 "rev": 8,
858 858 "node": "95c24699272ef57d062b8bccc32c878bf841784a",
859 859 "branch": "default",
860 860 "phase": "draft",
861 861 "user": "test",
862 862 "date": [1577872860, 0],
863 863 "desc": "third",
864 864 "bookmarks": [],
865 865 "tags": ["tip"],
866 866 "parents": ["29114dbae42b9f078cf2714dbe3a86bba8ec7453"]
867 867 },
868 868 {
869 869 "rev": 7,
870 870 "node": "29114dbae42b9f078cf2714dbe3a86bba8ec7453",
871 871 "branch": "default",
872 872 "phase": "draft",
873 873 "user": "User Name <user@hostname>",
874 874 "date": [1000000, 0],
875 875 "desc": "second",
876 876 "bookmarks": [],
877 877 "tags": [],
878 878 "parents": ["0000000000000000000000000000000000000000"]
879 879 },
880 880 {
881 881 "rev": 6,
882 882 "node": "d41e714fe50d9e4a5f11b4d595d543481b5f980b",
883 883 "branch": "default",
884 884 "phase": "draft",
885 885 "user": "person",
886 886 "date": [1500001, 0],
887 887 "desc": "merge",
888 888 "bookmarks": [],
889 889 "tags": [],
890 890 "parents": ["13207e5a10d9fd28ec424934298e176197f2c67f", "bbe44766e73d5f11ed2177f1838de10c53ef3e74"]
891 891 },
892 892 {
893 893 "rev": 5,
894 894 "node": "13207e5a10d9fd28ec424934298e176197f2c67f",
895 895 "branch": "default",
896 896 "phase": "draft",
897 897 "user": "person",
898 898 "date": [1500000, 0],
899 899 "desc": "new head",
900 900 "bookmarks": [],
901 901 "tags": [],
902 902 "parents": ["10e46f2dcbf4823578cf180f33ecf0b957964c47"]
903 903 },
904 904 {
905 905 "rev": 4,
906 906 "node": "bbe44766e73d5f11ed2177f1838de10c53ef3e74",
907 907 "branch": "foo",
908 908 "phase": "draft",
909 909 "user": "person",
910 910 "date": [1400000, 0],
911 911 "desc": "new branch",
912 912 "bookmarks": [],
913 913 "tags": [],
914 914 "parents": ["10e46f2dcbf4823578cf180f33ecf0b957964c47"]
915 915 },
916 916 {
917 917 "rev": 3,
918 918 "node": "10e46f2dcbf4823578cf180f33ecf0b957964c47",
919 919 "branch": "default",
920 920 "phase": "draft",
921 921 "user": "person",
922 922 "date": [1300000, 0],
923 923 "desc": "no user, no domain",
924 924 "bookmarks": [],
925 925 "tags": [],
926 926 "parents": ["97054abb4ab824450e9164180baf491ae0078465"]
927 927 },
928 928 {
929 929 "rev": 2,
930 930 "node": "97054abb4ab824450e9164180baf491ae0078465",
931 931 "branch": "default",
932 932 "phase": "draft",
933 933 "user": "other@place",
934 934 "date": [1200000, 0],
935 935 "desc": "no person",
936 936 "bookmarks": [],
937 937 "tags": [],
938 938 "parents": ["b608e9d1a3f0273ccf70fb85fd6866b3482bf965"]
939 939 },
940 940 {
941 941 "rev": 1,
942 942 "node": "b608e9d1a3f0273ccf70fb85fd6866b3482bf965",
943 943 "branch": "default",
944 944 "phase": "draft",
945 945 "user": "A. N. Other <other@place>",
946 946 "date": [1100000, 0],
947 947 "desc": "other 1\nother 2\n\nother 3",
948 948 "bookmarks": [],
949 949 "tags": [],
950 950 "parents": ["1e4e1b8f71e05681d422154f5421e385fec3454f"]
951 951 },
952 952 {
953 953 "rev": 0,
954 954 "node": "1e4e1b8f71e05681d422154f5421e385fec3454f",
955 955 "branch": "default",
956 956 "phase": "draft",
957 957 "user": "User Name <user@hostname>",
958 958 "date": [1000000, 0],
959 959 "desc": "line 1\nline 2",
960 960 "bookmarks": [],
961 961 "tags": [],
962 962 "parents": ["0000000000000000000000000000000000000000"]
963 963 }
964 964 ]
965 965
966 966 $ hg heads -v -Tjson
967 967 [
968 968 {
969 969 "rev": 8,
970 970 "node": "95c24699272ef57d062b8bccc32c878bf841784a",
971 971 "branch": "default",
972 972 "phase": "draft",
973 973 "user": "test",
974 974 "date": [1577872860, 0],
975 975 "desc": "third",
976 976 "bookmarks": [],
977 977 "tags": ["tip"],
978 978 "parents": ["29114dbae42b9f078cf2714dbe3a86bba8ec7453"],
979 979 "files": ["fourth", "second", "third"]
980 980 },
981 981 {
982 982 "rev": 6,
983 983 "node": "d41e714fe50d9e4a5f11b4d595d543481b5f980b",
984 984 "branch": "default",
985 985 "phase": "draft",
986 986 "user": "person",
987 987 "date": [1500001, 0],
988 988 "desc": "merge",
989 989 "bookmarks": [],
990 990 "tags": [],
991 991 "parents": ["13207e5a10d9fd28ec424934298e176197f2c67f", "bbe44766e73d5f11ed2177f1838de10c53ef3e74"],
992 992 "files": []
993 993 },
994 994 {
995 995 "rev": 4,
996 996 "node": "bbe44766e73d5f11ed2177f1838de10c53ef3e74",
997 997 "branch": "foo",
998 998 "phase": "draft",
999 999 "user": "person",
1000 1000 "date": [1400000, 0],
1001 1001 "desc": "new branch",
1002 1002 "bookmarks": [],
1003 1003 "tags": [],
1004 1004 "parents": ["10e46f2dcbf4823578cf180f33ecf0b957964c47"],
1005 1005 "files": []
1006 1006 }
1007 1007 ]
1008 1008
1009 1009 $ hg log --debug -Tjson
1010 1010 [
1011 1011 {
1012 1012 "rev": 8,
1013 1013 "node": "95c24699272ef57d062b8bccc32c878bf841784a",
1014 1014 "branch": "default",
1015 1015 "phase": "draft",
1016 1016 "user": "test",
1017 1017 "date": [1577872860, 0],
1018 1018 "desc": "third",
1019 1019 "bookmarks": [],
1020 1020 "tags": ["tip"],
1021 1021 "parents": ["29114dbae42b9f078cf2714dbe3a86bba8ec7453"],
1022 1022 "manifest": "94961b75a2da554b4df6fb599e5bfc7d48de0c64",
1023 1023 "extra": {"branch": "default"},
1024 1024 "modified": [],
1025 1025 "added": ["fourth", "third"],
1026 1026 "removed": ["second"]
1027 1027 },
1028 1028 {
1029 1029 "rev": 7,
1030 1030 "node": "29114dbae42b9f078cf2714dbe3a86bba8ec7453",
1031 1031 "branch": "default",
1032 1032 "phase": "draft",
1033 1033 "user": "User Name <user@hostname>",
1034 1034 "date": [1000000, 0],
1035 1035 "desc": "second",
1036 1036 "bookmarks": [],
1037 1037 "tags": [],
1038 1038 "parents": ["0000000000000000000000000000000000000000"],
1039 1039 "manifest": "f2dbc354b94e5ec0b4f10680ee0cee816101d0bf",
1040 1040 "extra": {"branch": "default"},
1041 1041 "modified": [],
1042 1042 "added": ["second"],
1043 1043 "removed": []
1044 1044 },
1045 1045 {
1046 1046 "rev": 6,
1047 1047 "node": "d41e714fe50d9e4a5f11b4d595d543481b5f980b",
1048 1048 "branch": "default",
1049 1049 "phase": "draft",
1050 1050 "user": "person",
1051 1051 "date": [1500001, 0],
1052 1052 "desc": "merge",
1053 1053 "bookmarks": [],
1054 1054 "tags": [],
1055 1055 "parents": ["13207e5a10d9fd28ec424934298e176197f2c67f", "bbe44766e73d5f11ed2177f1838de10c53ef3e74"],
1056 1056 "manifest": "4dc3def4f9b4c6e8de820f6ee74737f91e96a216",
1057 1057 "extra": {"branch": "default"},
1058 1058 "modified": [],
1059 1059 "added": [],
1060 1060 "removed": []
1061 1061 },
1062 1062 {
1063 1063 "rev": 5,
1064 1064 "node": "13207e5a10d9fd28ec424934298e176197f2c67f",
1065 1065 "branch": "default",
1066 1066 "phase": "draft",
1067 1067 "user": "person",
1068 1068 "date": [1500000, 0],
1069 1069 "desc": "new head",
1070 1070 "bookmarks": [],
1071 1071 "tags": [],
1072 1072 "parents": ["10e46f2dcbf4823578cf180f33ecf0b957964c47"],
1073 1073 "manifest": "4dc3def4f9b4c6e8de820f6ee74737f91e96a216",
1074 1074 "extra": {"branch": "default"},
1075 1075 "modified": [],
1076 1076 "added": ["d"],
1077 1077 "removed": []
1078 1078 },
1079 1079 {
1080 1080 "rev": 4,
1081 1081 "node": "bbe44766e73d5f11ed2177f1838de10c53ef3e74",
1082 1082 "branch": "foo",
1083 1083 "phase": "draft",
1084 1084 "user": "person",
1085 1085 "date": [1400000, 0],
1086 1086 "desc": "new branch",
1087 1087 "bookmarks": [],
1088 1088 "tags": [],
1089 1089 "parents": ["10e46f2dcbf4823578cf180f33ecf0b957964c47"],
1090 1090 "manifest": "cb5a1327723bada42f117e4c55a303246eaf9ccc",
1091 1091 "extra": {"branch": "foo"},
1092 1092 "modified": [],
1093 1093 "added": [],
1094 1094 "removed": []
1095 1095 },
1096 1096 {
1097 1097 "rev": 3,
1098 1098 "node": "10e46f2dcbf4823578cf180f33ecf0b957964c47",
1099 1099 "branch": "default",
1100 1100 "phase": "draft",
1101 1101 "user": "person",
1102 1102 "date": [1300000, 0],
1103 1103 "desc": "no user, no domain",
1104 1104 "bookmarks": [],
1105 1105 "tags": [],
1106 1106 "parents": ["97054abb4ab824450e9164180baf491ae0078465"],
1107 1107 "manifest": "cb5a1327723bada42f117e4c55a303246eaf9ccc",
1108 1108 "extra": {"branch": "default"},
1109 1109 "modified": ["c"],
1110 1110 "added": [],
1111 1111 "removed": []
1112 1112 },
1113 1113 {
1114 1114 "rev": 2,
1115 1115 "node": "97054abb4ab824450e9164180baf491ae0078465",
1116 1116 "branch": "default",
1117 1117 "phase": "draft",
1118 1118 "user": "other@place",
1119 1119 "date": [1200000, 0],
1120 1120 "desc": "no person",
1121 1121 "bookmarks": [],
1122 1122 "tags": [],
1123 1123 "parents": ["b608e9d1a3f0273ccf70fb85fd6866b3482bf965"],
1124 1124 "manifest": "6e0e82995c35d0d57a52aca8da4e56139e06b4b1",
1125 1125 "extra": {"branch": "default"},
1126 1126 "modified": [],
1127 1127 "added": ["c"],
1128 1128 "removed": []
1129 1129 },
1130 1130 {
1131 1131 "rev": 1,
1132 1132 "node": "b608e9d1a3f0273ccf70fb85fd6866b3482bf965",
1133 1133 "branch": "default",
1134 1134 "phase": "draft",
1135 1135 "user": "A. N. Other <other@place>",
1136 1136 "date": [1100000, 0],
1137 1137 "desc": "other 1\nother 2\n\nother 3",
1138 1138 "bookmarks": [],
1139 1139 "tags": [],
1140 1140 "parents": ["1e4e1b8f71e05681d422154f5421e385fec3454f"],
1141 1141 "manifest": "4e8d705b1e53e3f9375e0e60dc7b525d8211fe55",
1142 1142 "extra": {"branch": "default"},
1143 1143 "modified": [],
1144 1144 "added": ["b"],
1145 1145 "removed": []
1146 1146 },
1147 1147 {
1148 1148 "rev": 0,
1149 1149 "node": "1e4e1b8f71e05681d422154f5421e385fec3454f",
1150 1150 "branch": "default",
1151 1151 "phase": "draft",
1152 1152 "user": "User Name <user@hostname>",
1153 1153 "date": [1000000, 0],
1154 1154 "desc": "line 1\nline 2",
1155 1155 "bookmarks": [],
1156 1156 "tags": [],
1157 1157 "parents": ["0000000000000000000000000000000000000000"],
1158 1158 "manifest": "a0c8bcbbb45c63b90b70ad007bf38961f64f2af0",
1159 1159 "extra": {"branch": "default"},
1160 1160 "modified": [],
1161 1161 "added": ["a"],
1162 1162 "removed": []
1163 1163 }
1164 1164 ]
1165 1165
1166 1166 Error if style not readable:
1167 1167
1168 1168 #if unix-permissions no-root
1169 1169 $ touch q
1170 1170 $ chmod 0 q
1171 1171 $ hg log --style ./q
1172 1172 abort: Permission denied: ./q
1173 1173 [255]
1174 1174 #endif
1175 1175
1176 1176 Error if no style:
1177 1177
1178 1178 $ hg log --style notexist
1179 1179 abort: style 'notexist' not found
1180 1180 (available styles: bisect, changelog, compact, default, phases, show, status, xml)
1181 1181 [255]
1182 1182
1183 1183 $ hg log -T list
1184 1184 available styles: bisect, changelog, compact, default, phases, show, status, xml
1185 1185 abort: specify a template
1186 1186 [255]
1187 1187
1188 1188 Error if style missing key:
1189 1189
1190 1190 $ echo 'q = q' > t
1191 1191 $ hg log --style ./t
1192 1192 abort: "changeset" not in template map
1193 1193 [255]
1194 1194
1195 1195 Error if style missing value:
1196 1196
1197 1197 $ echo 'changeset =' > t
1198 1198 $ hg log --style t
1199 1199 hg: parse error at t:1: missing value
1200 1200 [255]
1201 1201
1202 1202 Error if include fails:
1203 1203
1204 1204 $ echo 'changeset = q' >> t
1205 1205 #if unix-permissions no-root
1206 1206 $ hg log --style ./t
1207 1207 abort: template file ./q: Permission denied
1208 1208 [255]
1209 1209 $ rm -f q
1210 1210 #endif
1211 1211
1212 1212 Include works:
1213 1213
1214 1214 $ echo '{rev}' > q
1215 1215 $ hg log --style ./t
1216 1216 8
1217 1217 7
1218 1218 6
1219 1219 5
1220 1220 4
1221 1221 3
1222 1222 2
1223 1223 1
1224 1224 0
1225 1225
1226 1226 Check that recursive reference does not fall into RuntimeError (issue4758):
1227 1227
1228 1228 common mistake:
1229 1229
1230 1230 $ cat << EOF > issue4758
1231 1231 > changeset = '{changeset}\n'
1232 1232 > EOF
1233 1233 $ hg log --style ./issue4758
1234 1234 abort: recursive reference 'changeset' in template
1235 1235 [255]
1236 1236
1237 1237 circular reference:
1238 1238
1239 1239 $ cat << EOF > issue4758
1240 1240 > changeset = '{foo}'
1241 1241 > foo = '{changeset}'
1242 1242 > EOF
1243 1243 $ hg log --style ./issue4758
1244 1244 abort: recursive reference 'foo' in template
1245 1245 [255]
1246 1246
1247 1247 buildmap() -> gettemplate(), where no thunk was made:
1248 1248
1249 1249 $ cat << EOF > issue4758
1250 1250 > changeset = '{files % changeset}\n'
1251 1251 > EOF
1252 1252 $ hg log --style ./issue4758
1253 1253 abort: recursive reference 'changeset' in template
1254 1254 [255]
1255 1255
1256 1256 not a recursion if a keyword of the same name exists:
1257 1257
1258 1258 $ cat << EOF > issue4758
1259 1259 > changeset = '{tags % rev}'
1260 1260 > rev = '{rev} {tag}\n'
1261 1261 > EOF
1262 1262 $ hg log --style ./issue4758 -r tip
1263 1263 8 tip
1264 1264
1265 1265 Check that {phase} works correctly on parents:
1266 1266
1267 1267 $ cat << EOF > parentphase
1268 1268 > changeset_debug = '{rev} ({phase}):{parents}\n'
1269 1269 > parent = ' {rev} ({phase})'
1270 1270 > EOF
1271 1271 $ hg phase -r 5 --public
1272 1272 $ hg phase -r 7 --secret --force
1273 1273 $ hg log --debug -G --style ./parentphase
1274 1274 @ 8 (secret): 7 (secret) -1 (public)
1275 1275 |
1276 1276 o 7 (secret): -1 (public) -1 (public)
1277 1277
1278 1278 o 6 (draft): 5 (public) 4 (draft)
1279 1279 |\
1280 1280 | o 5 (public): 3 (public) -1 (public)
1281 1281 | |
1282 1282 o | 4 (draft): 3 (public) -1 (public)
1283 1283 |/
1284 1284 o 3 (public): 2 (public) -1 (public)
1285 1285 |
1286 1286 o 2 (public): 1 (public) -1 (public)
1287 1287 |
1288 1288 o 1 (public): 0 (public) -1 (public)
1289 1289 |
1290 1290 o 0 (public): -1 (public) -1 (public)
1291 1291
1292 1292
1293 1293 Missing non-standard names give no error (backward compatibility):
1294 1294
1295 1295 $ echo "changeset = '{c}'" > t
1296 1296 $ hg log --style ./t
1297 1297
1298 1298 Defining non-standard name works:
1299 1299
1300 1300 $ cat <<EOF > t
1301 1301 > changeset = '{c}'
1302 1302 > c = q
1303 1303 > EOF
1304 1304 $ hg log --style ./t
1305 1305 8
1306 1306 7
1307 1307 6
1308 1308 5
1309 1309 4
1310 1310 3
1311 1311 2
1312 1312 1
1313 1313 0
1314 1314
1315 1315 ui.style works:
1316 1316
1317 1317 $ echo '[ui]' > .hg/hgrc
1318 1318 $ echo 'style = t' >> .hg/hgrc
1319 1319 $ hg log
1320 1320 8
1321 1321 7
1322 1322 6
1323 1323 5
1324 1324 4
1325 1325 3
1326 1326 2
1327 1327 1
1328 1328 0
1329 1329
1330 1330
1331 1331 Issue338:
1332 1332
1333 1333 $ hg log --style=changelog > changelog
1334 1334
1335 1335 $ cat changelog
1336 1336 2020-01-01 test <test>
1337 1337
1338 1338 * fourth, second, third:
1339 1339 third
1340 1340 [95c24699272e] [tip]
1341 1341
1342 1342 1970-01-12 User Name <user@hostname>
1343 1343
1344 1344 * second:
1345 1345 second
1346 1346 [29114dbae42b]
1347 1347
1348 1348 1970-01-18 person <person>
1349 1349
1350 1350 * merge
1351 1351 [d41e714fe50d]
1352 1352
1353 1353 * d:
1354 1354 new head
1355 1355 [13207e5a10d9]
1356 1356
1357 1357 1970-01-17 person <person>
1358 1358
1359 1359 * new branch
1360 1360 [bbe44766e73d] <foo>
1361 1361
1362 1362 1970-01-16 person <person>
1363 1363
1364 1364 * c:
1365 1365 no user, no domain
1366 1366 [10e46f2dcbf4]
1367 1367
1368 1368 1970-01-14 other <other@place>
1369 1369
1370 1370 * c:
1371 1371 no person
1372 1372 [97054abb4ab8]
1373 1373
1374 1374 1970-01-13 A. N. Other <other@place>
1375 1375
1376 1376 * b:
1377 1377 other 1 other 2
1378 1378
1379 1379 other 3
1380 1380 [b608e9d1a3f0]
1381 1381
1382 1382 1970-01-12 User Name <user@hostname>
1383 1383
1384 1384 * a:
1385 1385 line 1 line 2
1386 1386 [1e4e1b8f71e0]
1387 1387
1388 1388
1389 1389 Issue2130: xml output for 'hg heads' is malformed
1390 1390
1391 1391 $ hg heads --style changelog
1392 1392 2020-01-01 test <test>
1393 1393
1394 1394 * fourth, second, third:
1395 1395 third
1396 1396 [95c24699272e] [tip]
1397 1397
1398 1398 1970-01-18 person <person>
1399 1399
1400 1400 * merge
1401 1401 [d41e714fe50d]
1402 1402
1403 1403 1970-01-17 person <person>
1404 1404
1405 1405 * new branch
1406 1406 [bbe44766e73d] <foo>
1407 1407
1408 1408
1409 1409 Keys work:
1410 1410
1411 1411 $ for key in author branch branches date desc file_adds file_dels file_mods \
1412 1412 > file_copies file_copies_switch files \
1413 1413 > manifest node parents rev tags diffstat extras \
1414 1414 > p1rev p2rev p1node p2node; do
1415 1415 > for mode in '' --verbose --debug; do
1416 1416 > hg log $mode --template "$key$mode: {$key}\n"
1417 1417 > done
1418 1418 > done
1419 1419 author: test
1420 1420 author: User Name <user@hostname>
1421 1421 author: person
1422 1422 author: person
1423 1423 author: person
1424 1424 author: person
1425 1425 author: other@place
1426 1426 author: A. N. Other <other@place>
1427 1427 author: User Name <user@hostname>
1428 1428 author--verbose: test
1429 1429 author--verbose: User Name <user@hostname>
1430 1430 author--verbose: person
1431 1431 author--verbose: person
1432 1432 author--verbose: person
1433 1433 author--verbose: person
1434 1434 author--verbose: other@place
1435 1435 author--verbose: A. N. Other <other@place>
1436 1436 author--verbose: User Name <user@hostname>
1437 1437 author--debug: test
1438 1438 author--debug: User Name <user@hostname>
1439 1439 author--debug: person
1440 1440 author--debug: person
1441 1441 author--debug: person
1442 1442 author--debug: person
1443 1443 author--debug: other@place
1444 1444 author--debug: A. N. Other <other@place>
1445 1445 author--debug: User Name <user@hostname>
1446 1446 branch: default
1447 1447 branch: default
1448 1448 branch: default
1449 1449 branch: default
1450 1450 branch: foo
1451 1451 branch: default
1452 1452 branch: default
1453 1453 branch: default
1454 1454 branch: default
1455 1455 branch--verbose: default
1456 1456 branch--verbose: default
1457 1457 branch--verbose: default
1458 1458 branch--verbose: default
1459 1459 branch--verbose: foo
1460 1460 branch--verbose: default
1461 1461 branch--verbose: default
1462 1462 branch--verbose: default
1463 1463 branch--verbose: default
1464 1464 branch--debug: default
1465 1465 branch--debug: default
1466 1466 branch--debug: default
1467 1467 branch--debug: default
1468 1468 branch--debug: foo
1469 1469 branch--debug: default
1470 1470 branch--debug: default
1471 1471 branch--debug: default
1472 1472 branch--debug: default
1473 1473 branches:
1474 1474 branches:
1475 1475 branches:
1476 1476 branches:
1477 1477 branches: foo
1478 1478 branches:
1479 1479 branches:
1480 1480 branches:
1481 1481 branches:
1482 1482 branches--verbose:
1483 1483 branches--verbose:
1484 1484 branches--verbose:
1485 1485 branches--verbose:
1486 1486 branches--verbose: foo
1487 1487 branches--verbose:
1488 1488 branches--verbose:
1489 1489 branches--verbose:
1490 1490 branches--verbose:
1491 1491 branches--debug:
1492 1492 branches--debug:
1493 1493 branches--debug:
1494 1494 branches--debug:
1495 1495 branches--debug: foo
1496 1496 branches--debug:
1497 1497 branches--debug:
1498 1498 branches--debug:
1499 1499 branches--debug:
1500 1500 date: 1577872860.00
1501 1501 date: 1000000.00
1502 1502 date: 1500001.00
1503 1503 date: 1500000.00
1504 1504 date: 1400000.00
1505 1505 date: 1300000.00
1506 1506 date: 1200000.00
1507 1507 date: 1100000.00
1508 1508 date: 1000000.00
1509 1509 date--verbose: 1577872860.00
1510 1510 date--verbose: 1000000.00
1511 1511 date--verbose: 1500001.00
1512 1512 date--verbose: 1500000.00
1513 1513 date--verbose: 1400000.00
1514 1514 date--verbose: 1300000.00
1515 1515 date--verbose: 1200000.00
1516 1516 date--verbose: 1100000.00
1517 1517 date--verbose: 1000000.00
1518 1518 date--debug: 1577872860.00
1519 1519 date--debug: 1000000.00
1520 1520 date--debug: 1500001.00
1521 1521 date--debug: 1500000.00
1522 1522 date--debug: 1400000.00
1523 1523 date--debug: 1300000.00
1524 1524 date--debug: 1200000.00
1525 1525 date--debug: 1100000.00
1526 1526 date--debug: 1000000.00
1527 1527 desc: third
1528 1528 desc: second
1529 1529 desc: merge
1530 1530 desc: new head
1531 1531 desc: new branch
1532 1532 desc: no user, no domain
1533 1533 desc: no person
1534 1534 desc: other 1
1535 1535 other 2
1536 1536
1537 1537 other 3
1538 1538 desc: line 1
1539 1539 line 2
1540 1540 desc--verbose: third
1541 1541 desc--verbose: second
1542 1542 desc--verbose: merge
1543 1543 desc--verbose: new head
1544 1544 desc--verbose: new branch
1545 1545 desc--verbose: no user, no domain
1546 1546 desc--verbose: no person
1547 1547 desc--verbose: other 1
1548 1548 other 2
1549 1549
1550 1550 other 3
1551 1551 desc--verbose: line 1
1552 1552 line 2
1553 1553 desc--debug: third
1554 1554 desc--debug: second
1555 1555 desc--debug: merge
1556 1556 desc--debug: new head
1557 1557 desc--debug: new branch
1558 1558 desc--debug: no user, no domain
1559 1559 desc--debug: no person
1560 1560 desc--debug: other 1
1561 1561 other 2
1562 1562
1563 1563 other 3
1564 1564 desc--debug: line 1
1565 1565 line 2
1566 1566 file_adds: fourth third
1567 1567 file_adds: second
1568 1568 file_adds:
1569 1569 file_adds: d
1570 1570 file_adds:
1571 1571 file_adds:
1572 1572 file_adds: c
1573 1573 file_adds: b
1574 1574 file_adds: a
1575 1575 file_adds--verbose: fourth third
1576 1576 file_adds--verbose: second
1577 1577 file_adds--verbose:
1578 1578 file_adds--verbose: d
1579 1579 file_adds--verbose:
1580 1580 file_adds--verbose:
1581 1581 file_adds--verbose: c
1582 1582 file_adds--verbose: b
1583 1583 file_adds--verbose: a
1584 1584 file_adds--debug: fourth third
1585 1585 file_adds--debug: second
1586 1586 file_adds--debug:
1587 1587 file_adds--debug: d
1588 1588 file_adds--debug:
1589 1589 file_adds--debug:
1590 1590 file_adds--debug: c
1591 1591 file_adds--debug: b
1592 1592 file_adds--debug: a
1593 1593 file_dels: second
1594 1594 file_dels:
1595 1595 file_dels:
1596 1596 file_dels:
1597 1597 file_dels:
1598 1598 file_dels:
1599 1599 file_dels:
1600 1600 file_dels:
1601 1601 file_dels:
1602 1602 file_dels--verbose: second
1603 1603 file_dels--verbose:
1604 1604 file_dels--verbose:
1605 1605 file_dels--verbose:
1606 1606 file_dels--verbose:
1607 1607 file_dels--verbose:
1608 1608 file_dels--verbose:
1609 1609 file_dels--verbose:
1610 1610 file_dels--verbose:
1611 1611 file_dels--debug: second
1612 1612 file_dels--debug:
1613 1613 file_dels--debug:
1614 1614 file_dels--debug:
1615 1615 file_dels--debug:
1616 1616 file_dels--debug:
1617 1617 file_dels--debug:
1618 1618 file_dels--debug:
1619 1619 file_dels--debug:
1620 1620 file_mods:
1621 1621 file_mods:
1622 1622 file_mods:
1623 1623 file_mods:
1624 1624 file_mods:
1625 1625 file_mods: c
1626 1626 file_mods:
1627 1627 file_mods:
1628 1628 file_mods:
1629 1629 file_mods--verbose:
1630 1630 file_mods--verbose:
1631 1631 file_mods--verbose:
1632 1632 file_mods--verbose:
1633 1633 file_mods--verbose:
1634 1634 file_mods--verbose: c
1635 1635 file_mods--verbose:
1636 1636 file_mods--verbose:
1637 1637 file_mods--verbose:
1638 1638 file_mods--debug:
1639 1639 file_mods--debug:
1640 1640 file_mods--debug:
1641 1641 file_mods--debug:
1642 1642 file_mods--debug:
1643 1643 file_mods--debug: c
1644 1644 file_mods--debug:
1645 1645 file_mods--debug:
1646 1646 file_mods--debug:
1647 1647 file_copies: fourth (second)
1648 1648 file_copies:
1649 1649 file_copies:
1650 1650 file_copies:
1651 1651 file_copies:
1652 1652 file_copies:
1653 1653 file_copies:
1654 1654 file_copies:
1655 1655 file_copies:
1656 1656 file_copies--verbose: fourth (second)
1657 1657 file_copies--verbose:
1658 1658 file_copies--verbose:
1659 1659 file_copies--verbose:
1660 1660 file_copies--verbose:
1661 1661 file_copies--verbose:
1662 1662 file_copies--verbose:
1663 1663 file_copies--verbose:
1664 1664 file_copies--verbose:
1665 1665 file_copies--debug: fourth (second)
1666 1666 file_copies--debug:
1667 1667 file_copies--debug:
1668 1668 file_copies--debug:
1669 1669 file_copies--debug:
1670 1670 file_copies--debug:
1671 1671 file_copies--debug:
1672 1672 file_copies--debug:
1673 1673 file_copies--debug:
1674 1674 file_copies_switch:
1675 1675 file_copies_switch:
1676 1676 file_copies_switch:
1677 1677 file_copies_switch:
1678 1678 file_copies_switch:
1679 1679 file_copies_switch:
1680 1680 file_copies_switch:
1681 1681 file_copies_switch:
1682 1682 file_copies_switch:
1683 1683 file_copies_switch--verbose:
1684 1684 file_copies_switch--verbose:
1685 1685 file_copies_switch--verbose:
1686 1686 file_copies_switch--verbose:
1687 1687 file_copies_switch--verbose:
1688 1688 file_copies_switch--verbose:
1689 1689 file_copies_switch--verbose:
1690 1690 file_copies_switch--verbose:
1691 1691 file_copies_switch--verbose:
1692 1692 file_copies_switch--debug:
1693 1693 file_copies_switch--debug:
1694 1694 file_copies_switch--debug:
1695 1695 file_copies_switch--debug:
1696 1696 file_copies_switch--debug:
1697 1697 file_copies_switch--debug:
1698 1698 file_copies_switch--debug:
1699 1699 file_copies_switch--debug:
1700 1700 file_copies_switch--debug:
1701 1701 files: fourth second third
1702 1702 files: second
1703 1703 files:
1704 1704 files: d
1705 1705 files:
1706 1706 files: c
1707 1707 files: c
1708 1708 files: b
1709 1709 files: a
1710 1710 files--verbose: fourth second third
1711 1711 files--verbose: second
1712 1712 files--verbose:
1713 1713 files--verbose: d
1714 1714 files--verbose:
1715 1715 files--verbose: c
1716 1716 files--verbose: c
1717 1717 files--verbose: b
1718 1718 files--verbose: a
1719 1719 files--debug: fourth second third
1720 1720 files--debug: second
1721 1721 files--debug:
1722 1722 files--debug: d
1723 1723 files--debug:
1724 1724 files--debug: c
1725 1725 files--debug: c
1726 1726 files--debug: b
1727 1727 files--debug: a
1728 1728 manifest: 6:94961b75a2da
1729 1729 manifest: 5:f2dbc354b94e
1730 1730 manifest: 4:4dc3def4f9b4
1731 1731 manifest: 4:4dc3def4f9b4
1732 1732 manifest: 3:cb5a1327723b
1733 1733 manifest: 3:cb5a1327723b
1734 1734 manifest: 2:6e0e82995c35
1735 1735 manifest: 1:4e8d705b1e53
1736 1736 manifest: 0:a0c8bcbbb45c
1737 1737 manifest--verbose: 6:94961b75a2da
1738 1738 manifest--verbose: 5:f2dbc354b94e
1739 1739 manifest--verbose: 4:4dc3def4f9b4
1740 1740 manifest--verbose: 4:4dc3def4f9b4
1741 1741 manifest--verbose: 3:cb5a1327723b
1742 1742 manifest--verbose: 3:cb5a1327723b
1743 1743 manifest--verbose: 2:6e0e82995c35
1744 1744 manifest--verbose: 1:4e8d705b1e53
1745 1745 manifest--verbose: 0:a0c8bcbbb45c
1746 1746 manifest--debug: 6:94961b75a2da554b4df6fb599e5bfc7d48de0c64
1747 1747 manifest--debug: 5:f2dbc354b94e5ec0b4f10680ee0cee816101d0bf
1748 1748 manifest--debug: 4:4dc3def4f9b4c6e8de820f6ee74737f91e96a216
1749 1749 manifest--debug: 4:4dc3def4f9b4c6e8de820f6ee74737f91e96a216
1750 1750 manifest--debug: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc
1751 1751 manifest--debug: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc
1752 1752 manifest--debug: 2:6e0e82995c35d0d57a52aca8da4e56139e06b4b1
1753 1753 manifest--debug: 1:4e8d705b1e53e3f9375e0e60dc7b525d8211fe55
1754 1754 manifest--debug: 0:a0c8bcbbb45c63b90b70ad007bf38961f64f2af0
1755 1755 node: 95c24699272ef57d062b8bccc32c878bf841784a
1756 1756 node: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1757 1757 node: d41e714fe50d9e4a5f11b4d595d543481b5f980b
1758 1758 node: 13207e5a10d9fd28ec424934298e176197f2c67f
1759 1759 node: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1760 1760 node: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1761 1761 node: 97054abb4ab824450e9164180baf491ae0078465
1762 1762 node: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1763 1763 node: 1e4e1b8f71e05681d422154f5421e385fec3454f
1764 1764 node--verbose: 95c24699272ef57d062b8bccc32c878bf841784a
1765 1765 node--verbose: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1766 1766 node--verbose: d41e714fe50d9e4a5f11b4d595d543481b5f980b
1767 1767 node--verbose: 13207e5a10d9fd28ec424934298e176197f2c67f
1768 1768 node--verbose: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1769 1769 node--verbose: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1770 1770 node--verbose: 97054abb4ab824450e9164180baf491ae0078465
1771 1771 node--verbose: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1772 1772 node--verbose: 1e4e1b8f71e05681d422154f5421e385fec3454f
1773 1773 node--debug: 95c24699272ef57d062b8bccc32c878bf841784a
1774 1774 node--debug: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1775 1775 node--debug: d41e714fe50d9e4a5f11b4d595d543481b5f980b
1776 1776 node--debug: 13207e5a10d9fd28ec424934298e176197f2c67f
1777 1777 node--debug: bbe44766e73d5f11ed2177f1838de10c53ef3e74
1778 1778 node--debug: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1779 1779 node--debug: 97054abb4ab824450e9164180baf491ae0078465
1780 1780 node--debug: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1781 1781 node--debug: 1e4e1b8f71e05681d422154f5421e385fec3454f
1782 1782 parents:
1783 1783 parents: -1:000000000000
1784 1784 parents: 5:13207e5a10d9 4:bbe44766e73d
1785 1785 parents: 3:10e46f2dcbf4
1786 1786 parents:
1787 1787 parents:
1788 1788 parents:
1789 1789 parents:
1790 1790 parents:
1791 1791 parents--verbose:
1792 1792 parents--verbose: -1:000000000000
1793 1793 parents--verbose: 5:13207e5a10d9 4:bbe44766e73d
1794 1794 parents--verbose: 3:10e46f2dcbf4
1795 1795 parents--verbose:
1796 1796 parents--verbose:
1797 1797 parents--verbose:
1798 1798 parents--verbose:
1799 1799 parents--verbose:
1800 1800 parents--debug: 7:29114dbae42b9f078cf2714dbe3a86bba8ec7453 -1:0000000000000000000000000000000000000000
1801 1801 parents--debug: -1:0000000000000000000000000000000000000000 -1:0000000000000000000000000000000000000000
1802 1802 parents--debug: 5:13207e5a10d9fd28ec424934298e176197f2c67f 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
1803 1803 parents--debug: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47 -1:0000000000000000000000000000000000000000
1804 1804 parents--debug: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47 -1:0000000000000000000000000000000000000000
1805 1805 parents--debug: 2:97054abb4ab824450e9164180baf491ae0078465 -1:0000000000000000000000000000000000000000
1806 1806 parents--debug: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965 -1:0000000000000000000000000000000000000000
1807 1807 parents--debug: 0:1e4e1b8f71e05681d422154f5421e385fec3454f -1:0000000000000000000000000000000000000000
1808 1808 parents--debug: -1:0000000000000000000000000000000000000000 -1:0000000000000000000000000000000000000000
1809 1809 rev: 8
1810 1810 rev: 7
1811 1811 rev: 6
1812 1812 rev: 5
1813 1813 rev: 4
1814 1814 rev: 3
1815 1815 rev: 2
1816 1816 rev: 1
1817 1817 rev: 0
1818 1818 rev--verbose: 8
1819 1819 rev--verbose: 7
1820 1820 rev--verbose: 6
1821 1821 rev--verbose: 5
1822 1822 rev--verbose: 4
1823 1823 rev--verbose: 3
1824 1824 rev--verbose: 2
1825 1825 rev--verbose: 1
1826 1826 rev--verbose: 0
1827 1827 rev--debug: 8
1828 1828 rev--debug: 7
1829 1829 rev--debug: 6
1830 1830 rev--debug: 5
1831 1831 rev--debug: 4
1832 1832 rev--debug: 3
1833 1833 rev--debug: 2
1834 1834 rev--debug: 1
1835 1835 rev--debug: 0
1836 1836 tags: tip
1837 1837 tags:
1838 1838 tags:
1839 1839 tags:
1840 1840 tags:
1841 1841 tags:
1842 1842 tags:
1843 1843 tags:
1844 1844 tags:
1845 1845 tags--verbose: tip
1846 1846 tags--verbose:
1847 1847 tags--verbose:
1848 1848 tags--verbose:
1849 1849 tags--verbose:
1850 1850 tags--verbose:
1851 1851 tags--verbose:
1852 1852 tags--verbose:
1853 1853 tags--verbose:
1854 1854 tags--debug: tip
1855 1855 tags--debug:
1856 1856 tags--debug:
1857 1857 tags--debug:
1858 1858 tags--debug:
1859 1859 tags--debug:
1860 1860 tags--debug:
1861 1861 tags--debug:
1862 1862 tags--debug:
1863 1863 diffstat: 3: +2/-1
1864 1864 diffstat: 1: +1/-0
1865 1865 diffstat: 0: +0/-0
1866 1866 diffstat: 1: +1/-0
1867 1867 diffstat: 0: +0/-0
1868 1868 diffstat: 1: +1/-0
1869 1869 diffstat: 1: +4/-0
1870 1870 diffstat: 1: +2/-0
1871 1871 diffstat: 1: +1/-0
1872 1872 diffstat--verbose: 3: +2/-1
1873 1873 diffstat--verbose: 1: +1/-0
1874 1874 diffstat--verbose: 0: +0/-0
1875 1875 diffstat--verbose: 1: +1/-0
1876 1876 diffstat--verbose: 0: +0/-0
1877 1877 diffstat--verbose: 1: +1/-0
1878 1878 diffstat--verbose: 1: +4/-0
1879 1879 diffstat--verbose: 1: +2/-0
1880 1880 diffstat--verbose: 1: +1/-0
1881 1881 diffstat--debug: 3: +2/-1
1882 1882 diffstat--debug: 1: +1/-0
1883 1883 diffstat--debug: 0: +0/-0
1884 1884 diffstat--debug: 1: +1/-0
1885 1885 diffstat--debug: 0: +0/-0
1886 1886 diffstat--debug: 1: +1/-0
1887 1887 diffstat--debug: 1: +4/-0
1888 1888 diffstat--debug: 1: +2/-0
1889 1889 diffstat--debug: 1: +1/-0
1890 1890 extras: branch=default
1891 1891 extras: branch=default
1892 1892 extras: branch=default
1893 1893 extras: branch=default
1894 1894 extras: branch=foo
1895 1895 extras: branch=default
1896 1896 extras: branch=default
1897 1897 extras: branch=default
1898 1898 extras: branch=default
1899 1899 extras--verbose: branch=default
1900 1900 extras--verbose: branch=default
1901 1901 extras--verbose: branch=default
1902 1902 extras--verbose: branch=default
1903 1903 extras--verbose: branch=foo
1904 1904 extras--verbose: branch=default
1905 1905 extras--verbose: branch=default
1906 1906 extras--verbose: branch=default
1907 1907 extras--verbose: branch=default
1908 1908 extras--debug: branch=default
1909 1909 extras--debug: branch=default
1910 1910 extras--debug: branch=default
1911 1911 extras--debug: branch=default
1912 1912 extras--debug: branch=foo
1913 1913 extras--debug: branch=default
1914 1914 extras--debug: branch=default
1915 1915 extras--debug: branch=default
1916 1916 extras--debug: branch=default
1917 1917 p1rev: 7
1918 1918 p1rev: -1
1919 1919 p1rev: 5
1920 1920 p1rev: 3
1921 1921 p1rev: 3
1922 1922 p1rev: 2
1923 1923 p1rev: 1
1924 1924 p1rev: 0
1925 1925 p1rev: -1
1926 1926 p1rev--verbose: 7
1927 1927 p1rev--verbose: -1
1928 1928 p1rev--verbose: 5
1929 1929 p1rev--verbose: 3
1930 1930 p1rev--verbose: 3
1931 1931 p1rev--verbose: 2
1932 1932 p1rev--verbose: 1
1933 1933 p1rev--verbose: 0
1934 1934 p1rev--verbose: -1
1935 1935 p1rev--debug: 7
1936 1936 p1rev--debug: -1
1937 1937 p1rev--debug: 5
1938 1938 p1rev--debug: 3
1939 1939 p1rev--debug: 3
1940 1940 p1rev--debug: 2
1941 1941 p1rev--debug: 1
1942 1942 p1rev--debug: 0
1943 1943 p1rev--debug: -1
1944 1944 p2rev: -1
1945 1945 p2rev: -1
1946 1946 p2rev: 4
1947 1947 p2rev: -1
1948 1948 p2rev: -1
1949 1949 p2rev: -1
1950 1950 p2rev: -1
1951 1951 p2rev: -1
1952 1952 p2rev: -1
1953 1953 p2rev--verbose: -1
1954 1954 p2rev--verbose: -1
1955 1955 p2rev--verbose: 4
1956 1956 p2rev--verbose: -1
1957 1957 p2rev--verbose: -1
1958 1958 p2rev--verbose: -1
1959 1959 p2rev--verbose: -1
1960 1960 p2rev--verbose: -1
1961 1961 p2rev--verbose: -1
1962 1962 p2rev--debug: -1
1963 1963 p2rev--debug: -1
1964 1964 p2rev--debug: 4
1965 1965 p2rev--debug: -1
1966 1966 p2rev--debug: -1
1967 1967 p2rev--debug: -1
1968 1968 p2rev--debug: -1
1969 1969 p2rev--debug: -1
1970 1970 p2rev--debug: -1
1971 1971 p1node: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1972 1972 p1node: 0000000000000000000000000000000000000000
1973 1973 p1node: 13207e5a10d9fd28ec424934298e176197f2c67f
1974 1974 p1node: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1975 1975 p1node: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1976 1976 p1node: 97054abb4ab824450e9164180baf491ae0078465
1977 1977 p1node: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1978 1978 p1node: 1e4e1b8f71e05681d422154f5421e385fec3454f
1979 1979 p1node: 0000000000000000000000000000000000000000
1980 1980 p1node--verbose: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1981 1981 p1node--verbose: 0000000000000000000000000000000000000000
1982 1982 p1node--verbose: 13207e5a10d9fd28ec424934298e176197f2c67f
1983 1983 p1node--verbose: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1984 1984 p1node--verbose: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1985 1985 p1node--verbose: 97054abb4ab824450e9164180baf491ae0078465
1986 1986 p1node--verbose: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1987 1987 p1node--verbose: 1e4e1b8f71e05681d422154f5421e385fec3454f
1988 1988 p1node--verbose: 0000000000000000000000000000000000000000
1989 1989 p1node--debug: 29114dbae42b9f078cf2714dbe3a86bba8ec7453
1990 1990 p1node--debug: 0000000000000000000000000000000000000000
1991 1991 p1node--debug: 13207e5a10d9fd28ec424934298e176197f2c67f
1992 1992 p1node--debug: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1993 1993 p1node--debug: 10e46f2dcbf4823578cf180f33ecf0b957964c47
1994 1994 p1node--debug: 97054abb4ab824450e9164180baf491ae0078465
1995 1995 p1node--debug: b608e9d1a3f0273ccf70fb85fd6866b3482bf965
1996 1996 p1node--debug: 1e4e1b8f71e05681d422154f5421e385fec3454f
1997 1997 p1node--debug: 0000000000000000000000000000000000000000
1998 1998 p2node: 0000000000000000000000000000000000000000
1999 1999 p2node: 0000000000000000000000000000000000000000
2000 2000 p2node: bbe44766e73d5f11ed2177f1838de10c53ef3e74
2001 2001 p2node: 0000000000000000000000000000000000000000
2002 2002 p2node: 0000000000000000000000000000000000000000
2003 2003 p2node: 0000000000000000000000000000000000000000
2004 2004 p2node: 0000000000000000000000000000000000000000
2005 2005 p2node: 0000000000000000000000000000000000000000
2006 2006 p2node: 0000000000000000000000000000000000000000
2007 2007 p2node--verbose: 0000000000000000000000000000000000000000
2008 2008 p2node--verbose: 0000000000000000000000000000000000000000
2009 2009 p2node--verbose: bbe44766e73d5f11ed2177f1838de10c53ef3e74
2010 2010 p2node--verbose: 0000000000000000000000000000000000000000
2011 2011 p2node--verbose: 0000000000000000000000000000000000000000
2012 2012 p2node--verbose: 0000000000000000000000000000000000000000
2013 2013 p2node--verbose: 0000000000000000000000000000000000000000
2014 2014 p2node--verbose: 0000000000000000000000000000000000000000
2015 2015 p2node--verbose: 0000000000000000000000000000000000000000
2016 2016 p2node--debug: 0000000000000000000000000000000000000000
2017 2017 p2node--debug: 0000000000000000000000000000000000000000
2018 2018 p2node--debug: bbe44766e73d5f11ed2177f1838de10c53ef3e74
2019 2019 p2node--debug: 0000000000000000000000000000000000000000
2020 2020 p2node--debug: 0000000000000000000000000000000000000000
2021 2021 p2node--debug: 0000000000000000000000000000000000000000
2022 2022 p2node--debug: 0000000000000000000000000000000000000000
2023 2023 p2node--debug: 0000000000000000000000000000000000000000
2024 2024 p2node--debug: 0000000000000000000000000000000000000000
2025 2025
2026 2026 Filters work:
2027 2027
2028 2028 $ hg log --template '{author|domain}\n'
2029 2029
2030 2030 hostname
2031 2031
2032 2032
2033 2033
2034 2034
2035 2035 place
2036 2036 place
2037 2037 hostname
2038 2038
2039 2039 $ hg log --template '{author|person}\n'
2040 2040 test
2041 2041 User Name
2042 2042 person
2043 2043 person
2044 2044 person
2045 2045 person
2046 2046 other
2047 2047 A. N. Other
2048 2048 User Name
2049 2049
2050 2050 $ hg log --template '{author|user}\n'
2051 2051 test
2052 2052 user
2053 2053 person
2054 2054 person
2055 2055 person
2056 2056 person
2057 2057 other
2058 2058 other
2059 2059 user
2060 2060
2061 2061 $ hg log --template '{date|date}\n'
2062 2062 Wed Jan 01 10:01:00 2020 +0000
2063 2063 Mon Jan 12 13:46:40 1970 +0000
2064 2064 Sun Jan 18 08:40:01 1970 +0000
2065 2065 Sun Jan 18 08:40:00 1970 +0000
2066 2066 Sat Jan 17 04:53:20 1970 +0000
2067 2067 Fri Jan 16 01:06:40 1970 +0000
2068 2068 Wed Jan 14 21:20:00 1970 +0000
2069 2069 Tue Jan 13 17:33:20 1970 +0000
2070 2070 Mon Jan 12 13:46:40 1970 +0000
2071 2071
2072 2072 $ hg log --template '{date|isodate}\n'
2073 2073 2020-01-01 10:01 +0000
2074 2074 1970-01-12 13:46 +0000
2075 2075 1970-01-18 08:40 +0000
2076 2076 1970-01-18 08:40 +0000
2077 2077 1970-01-17 04:53 +0000
2078 2078 1970-01-16 01:06 +0000
2079 2079 1970-01-14 21:20 +0000
2080 2080 1970-01-13 17:33 +0000
2081 2081 1970-01-12 13:46 +0000
2082 2082
2083 2083 $ hg log --template '{date|isodatesec}\n'
2084 2084 2020-01-01 10:01:00 +0000
2085 2085 1970-01-12 13:46:40 +0000
2086 2086 1970-01-18 08:40:01 +0000
2087 2087 1970-01-18 08:40:00 +0000
2088 2088 1970-01-17 04:53:20 +0000
2089 2089 1970-01-16 01:06:40 +0000
2090 2090 1970-01-14 21:20:00 +0000
2091 2091 1970-01-13 17:33:20 +0000
2092 2092 1970-01-12 13:46:40 +0000
2093 2093
2094 2094 $ hg log --template '{date|rfc822date}\n'
2095 2095 Wed, 01 Jan 2020 10:01:00 +0000
2096 2096 Mon, 12 Jan 1970 13:46:40 +0000
2097 2097 Sun, 18 Jan 1970 08:40:01 +0000
2098 2098 Sun, 18 Jan 1970 08:40:00 +0000
2099 2099 Sat, 17 Jan 1970 04:53:20 +0000
2100 2100 Fri, 16 Jan 1970 01:06:40 +0000
2101 2101 Wed, 14 Jan 1970 21:20:00 +0000
2102 2102 Tue, 13 Jan 1970 17:33:20 +0000
2103 2103 Mon, 12 Jan 1970 13:46:40 +0000
2104 2104
2105 2105 $ hg log --template '{desc|firstline}\n'
2106 2106 third
2107 2107 second
2108 2108 merge
2109 2109 new head
2110 2110 new branch
2111 2111 no user, no domain
2112 2112 no person
2113 2113 other 1
2114 2114 line 1
2115 2115
2116 2116 $ hg log --template '{node|short}\n'
2117 2117 95c24699272e
2118 2118 29114dbae42b
2119 2119 d41e714fe50d
2120 2120 13207e5a10d9
2121 2121 bbe44766e73d
2122 2122 10e46f2dcbf4
2123 2123 97054abb4ab8
2124 2124 b608e9d1a3f0
2125 2125 1e4e1b8f71e0
2126 2126
2127 2127 $ hg log --template '<changeset author="{author|xmlescape}"/>\n'
2128 2128 <changeset author="test"/>
2129 2129 <changeset author="User Name &lt;user@hostname&gt;"/>
2130 2130 <changeset author="person"/>
2131 2131 <changeset author="person"/>
2132 2132 <changeset author="person"/>
2133 2133 <changeset author="person"/>
2134 2134 <changeset author="other@place"/>
2135 2135 <changeset author="A. N. Other &lt;other@place&gt;"/>
2136 2136 <changeset author="User Name &lt;user@hostname&gt;"/>
2137 2137
2138 2138 $ hg log --template '{rev}: {children}\n'
2139 2139 8:
2140 2140 7: 8:95c24699272e
2141 2141 6:
2142 2142 5: 6:d41e714fe50d
2143 2143 4: 6:d41e714fe50d
2144 2144 3: 4:bbe44766e73d 5:13207e5a10d9
2145 2145 2: 3:10e46f2dcbf4
2146 2146 1: 2:97054abb4ab8
2147 2147 0: 1:b608e9d1a3f0
2148 2148
2149 2149 Formatnode filter works:
2150 2150
2151 2151 $ hg -q log -r 0 --template '{node|formatnode}\n'
2152 2152 1e4e1b8f71e0
2153 2153
2154 2154 $ hg log -r 0 --template '{node|formatnode}\n'
2155 2155 1e4e1b8f71e0
2156 2156
2157 2157 $ hg -v log -r 0 --template '{node|formatnode}\n'
2158 2158 1e4e1b8f71e0
2159 2159
2160 2160 $ hg --debug log -r 0 --template '{node|formatnode}\n'
2161 2161 1e4e1b8f71e05681d422154f5421e385fec3454f
2162 2162
2163 2163 Age filter:
2164 2164
2165 2165 $ hg init unstable-hash
2166 2166 $ cd unstable-hash
2167 2167 $ hg log --template '{date|age}\n' > /dev/null || exit 1
2168 2168
2169 2169 >>> from datetime import datetime, timedelta
2170 2170 >>> fp = open('a', 'w')
2171 2171 >>> n = datetime.now() + timedelta(366 * 7)
2172 2172 >>> fp.write('%d-%d-%d 00:00' % (n.year, n.month, n.day))
2173 2173 >>> fp.close()
2174 2174 $ hg add a
2175 2175 $ hg commit -m future -d "`cat a`"
2176 2176
2177 2177 $ hg log -l1 --template '{date|age}\n'
2178 2178 7 years from now
2179 2179
2180 2180 $ cd ..
2181 2181 $ rm -rf unstable-hash
2182 2182
2183 2183 Add a dummy commit to make up for the instability of the above:
2184 2184
2185 2185 $ echo a > a
2186 2186 $ hg add a
2187 2187 $ hg ci -m future
2188 2188
2189 2189 Count filter:
2190 2190
2191 2191 $ hg log -l1 --template '{node|count} {node|short|count}\n'
2192 2192 40 12
2193 2193
2194 2194 $ hg log -l1 --template '{revset("null^")|count} {revset(".")|count} {revset("0::3")|count}\n'
2195 2195 0 1 4
2196 2196
2197 2197 $ hg log -G --template '{rev}: children: {children|count}, \
2198 2198 > tags: {tags|count}, file_adds: {file_adds|count}, \
2199 2199 > ancestors: {revset("ancestors(%s)", rev)|count}'
2200 2200 @ 9: children: 0, tags: 1, file_adds: 1, ancestors: 3
2201 2201 |
2202 2202 o 8: children: 1, tags: 0, file_adds: 2, ancestors: 2
2203 2203 |
2204 2204 o 7: children: 1, tags: 0, file_adds: 1, ancestors: 1
2205 2205
2206 2206 o 6: children: 0, tags: 0, file_adds: 0, ancestors: 7
2207 2207 |\
2208 2208 | o 5: children: 1, tags: 0, file_adds: 1, ancestors: 5
2209 2209 | |
2210 2210 o | 4: children: 1, tags: 0, file_adds: 0, ancestors: 5
2211 2211 |/
2212 2212 o 3: children: 2, tags: 0, file_adds: 0, ancestors: 4
2213 2213 |
2214 2214 o 2: children: 1, tags: 0, file_adds: 1, ancestors: 3
2215 2215 |
2216 2216 o 1: children: 1, tags: 0, file_adds: 1, ancestors: 2
2217 2217 |
2218 2218 o 0: children: 1, tags: 0, file_adds: 1, ancestors: 1
2219 2219
2220 2220
2221 2221 Upper/lower filters:
2222 2222
2223 2223 $ hg log -r0 --template '{branch|upper}\n'
2224 2224 DEFAULT
2225 2225 $ hg log -r0 --template '{author|lower}\n'
2226 2226 user name <user@hostname>
2227 2227 $ hg log -r0 --template '{date|upper}\n'
2228 2228 abort: template filter 'upper' is not compatible with keyword 'date'
2229 2229 [255]
2230 2230
2231 2231 Add a commit that does all possible modifications at once
2232 2232
2233 2233 $ echo modify >> third
2234 2234 $ touch b
2235 2235 $ hg add b
2236 2236 $ hg mv fourth fifth
2237 2237 $ hg rm a
2238 2238 $ hg ci -m "Modify, add, remove, rename"
2239 2239
2240 2240 Check the status template
2241 2241
2242 2242 $ cat <<EOF >> $HGRCPATH
2243 2243 > [extensions]
2244 2244 > color=
2245 2245 > EOF
2246 2246
2247 2247 $ hg log -T status -r 10
2248 2248 changeset: 10:0f9759ec227a
2249 2249 tag: tip
2250 2250 user: test
2251 2251 date: Thu Jan 01 00:00:00 1970 +0000
2252 2252 summary: Modify, add, remove, rename
2253 2253 files:
2254 2254 M third
2255 2255 A b
2256 2256 A fifth
2257 2257 R a
2258 2258 R fourth
2259 2259
2260 2260 $ hg log -T status -C -r 10
2261 2261 changeset: 10:0f9759ec227a
2262 2262 tag: tip
2263 2263 user: test
2264 2264 date: Thu Jan 01 00:00:00 1970 +0000
2265 2265 summary: Modify, add, remove, rename
2266 2266 files:
2267 2267 M third
2268 2268 A b
2269 2269 A fifth
2270 2270 fourth
2271 2271 R a
2272 2272 R fourth
2273 2273
2274 2274 $ hg log -T status -C -r 10 -v
2275 2275 changeset: 10:0f9759ec227a
2276 2276 tag: tip
2277 2277 user: test
2278 2278 date: Thu Jan 01 00:00:00 1970 +0000
2279 2279 description:
2280 2280 Modify, add, remove, rename
2281 2281
2282 2282 files:
2283 2283 M third
2284 2284 A b
2285 2285 A fifth
2286 2286 fourth
2287 2287 R a
2288 2288 R fourth
2289 2289
2290 2290 $ hg log -T status -C -r 10 --debug
2291 2291 changeset: 10:0f9759ec227a4859c2014a345cd8a859022b7c6c
2292 2292 tag: tip
2293 2293 phase: secret
2294 2294 parent: 9:bf9dfba36635106d6a73ccc01e28b762da60e066
2295 2295 parent: -1:0000000000000000000000000000000000000000
2296 2296 manifest: 8:89dd546f2de0a9d6d664f58d86097eb97baba567
2297 2297 user: test
2298 2298 date: Thu Jan 01 00:00:00 1970 +0000
2299 2299 extra: branch=default
2300 2300 description:
2301 2301 Modify, add, remove, rename
2302 2302
2303 2303 files:
2304 2304 M third
2305 2305 A b
2306 2306 A fifth
2307 2307 fourth
2308 2308 R a
2309 2309 R fourth
2310 2310
2311 2311 $ hg log -T status -C -r 10 --quiet
2312 2312 10:0f9759ec227a
2313 2313 $ hg --color=debug log -T status -r 10
2314 2314 [log.changeset changeset.secret|changeset: 10:0f9759ec227a]
2315 2315 [log.tag|tag: tip]
2316 2316 [log.user|user: test]
2317 2317 [log.date|date: Thu Jan 01 00:00:00 1970 +0000]
2318 2318 [log.summary|summary: Modify, add, remove, rename]
2319 2319 [ui.note log.files|files:]
2320 2320 [status.modified|M third]
2321 2321 [status.added|A b]
2322 2322 [status.added|A fifth]
2323 2323 [status.removed|R a]
2324 2324 [status.removed|R fourth]
2325 2325
2326 2326 $ hg --color=debug log -T status -C -r 10
2327 2327 [log.changeset changeset.secret|changeset: 10:0f9759ec227a]
2328 2328 [log.tag|tag: tip]
2329 2329 [log.user|user: test]
2330 2330 [log.date|date: Thu Jan 01 00:00:00 1970 +0000]
2331 2331 [log.summary|summary: Modify, add, remove, rename]
2332 2332 [ui.note log.files|files:]
2333 2333 [status.modified|M third]
2334 2334 [status.added|A b]
2335 2335 [status.added|A fifth]
2336 2336 [status.copied| fourth]
2337 2337 [status.removed|R a]
2338 2338 [status.removed|R fourth]
2339 2339
2340 2340 $ hg --color=debug log -T status -C -r 10 -v
2341 2341 [log.changeset changeset.secret|changeset: 10:0f9759ec227a]
2342 2342 [log.tag|tag: tip]
2343 2343 [log.user|user: test]
2344 2344 [log.date|date: Thu Jan 01 00:00:00 1970 +0000]
2345 2345 [ui.note log.description|description:]
2346 2346 [ui.note log.description|Modify, add, remove, rename]
2347 2347
2348 2348 [ui.note log.files|files:]
2349 2349 [status.modified|M third]
2350 2350 [status.added|A b]
2351 2351 [status.added|A fifth]
2352 2352 [status.copied| fourth]
2353 2353 [status.removed|R a]
2354 2354 [status.removed|R fourth]
2355 2355
2356 2356 $ hg --color=debug log -T status -C -r 10 --debug
2357 2357 [log.changeset changeset.secret|changeset: 10:0f9759ec227a4859c2014a345cd8a859022b7c6c]
2358 2358 [log.tag|tag: tip]
2359 2359 [log.phase|phase: secret]
2360 2360 [log.parent changeset.secret|parent: 9:bf9dfba36635106d6a73ccc01e28b762da60e066]
2361 2361 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2362 2362 [ui.debug log.manifest|manifest: 8:89dd546f2de0a9d6d664f58d86097eb97baba567]
2363 2363 [log.user|user: test]
2364 2364 [log.date|date: Thu Jan 01 00:00:00 1970 +0000]
2365 2365 [ui.debug log.extra|extra: branch=default]
2366 2366 [ui.note log.description|description:]
2367 2367 [ui.note log.description|Modify, add, remove, rename]
2368 2368
2369 2369 [ui.note log.files|files:]
2370 2370 [status.modified|M third]
2371 2371 [status.added|A b]
2372 2372 [status.added|A fifth]
2373 2373 [status.copied| fourth]
2374 2374 [status.removed|R a]
2375 2375 [status.removed|R fourth]
2376 2376
2377 2377 $ hg --color=debug log -T status -C -r 10 --quiet
2378 2378 [log.node|10:0f9759ec227a]
2379 2379
2380 2380 Check the bisect template
2381 2381
2382 2382 $ hg bisect -g 1
2383 2383 $ hg bisect -b 3 --noupdate
2384 2384 Testing changeset 2:97054abb4ab8 (2 changesets remaining, ~1 tests)
2385 2385 $ hg log -T bisect -r 0:4
2386 2386 changeset: 0:1e4e1b8f71e0
2387 2387 bisect: good (implicit)
2388 2388 user: User Name <user@hostname>
2389 2389 date: Mon Jan 12 13:46:40 1970 +0000
2390 2390 summary: line 1
2391 2391
2392 2392 changeset: 1:b608e9d1a3f0
2393 2393 bisect: good
2394 2394 user: A. N. Other <other@place>
2395 2395 date: Tue Jan 13 17:33:20 1970 +0000
2396 2396 summary: other 1
2397 2397
2398 2398 changeset: 2:97054abb4ab8
2399 2399 bisect: untested
2400 2400 user: other@place
2401 2401 date: Wed Jan 14 21:20:00 1970 +0000
2402 2402 summary: no person
2403 2403
2404 2404 changeset: 3:10e46f2dcbf4
2405 2405 bisect: bad
2406 2406 user: person
2407 2407 date: Fri Jan 16 01:06:40 1970 +0000
2408 2408 summary: no user, no domain
2409 2409
2410 2410 changeset: 4:bbe44766e73d
2411 2411 bisect: bad (implicit)
2412 2412 branch: foo
2413 2413 user: person
2414 2414 date: Sat Jan 17 04:53:20 1970 +0000
2415 2415 summary: new branch
2416 2416
2417 2417 $ hg log --debug -T bisect -r 0:4
2418 2418 changeset: 0:1e4e1b8f71e05681d422154f5421e385fec3454f
2419 2419 bisect: good (implicit)
2420 2420 phase: public
2421 2421 parent: -1:0000000000000000000000000000000000000000
2422 2422 parent: -1:0000000000000000000000000000000000000000
2423 2423 manifest: 0:a0c8bcbbb45c63b90b70ad007bf38961f64f2af0
2424 2424 user: User Name <user@hostname>
2425 2425 date: Mon Jan 12 13:46:40 1970 +0000
2426 2426 files+: a
2427 2427 extra: branch=default
2428 2428 description:
2429 2429 line 1
2430 2430 line 2
2431 2431
2432 2432
2433 2433 changeset: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965
2434 2434 bisect: good
2435 2435 phase: public
2436 2436 parent: 0:1e4e1b8f71e05681d422154f5421e385fec3454f
2437 2437 parent: -1:0000000000000000000000000000000000000000
2438 2438 manifest: 1:4e8d705b1e53e3f9375e0e60dc7b525d8211fe55
2439 2439 user: A. N. Other <other@place>
2440 2440 date: Tue Jan 13 17:33:20 1970 +0000
2441 2441 files+: b
2442 2442 extra: branch=default
2443 2443 description:
2444 2444 other 1
2445 2445 other 2
2446 2446
2447 2447 other 3
2448 2448
2449 2449
2450 2450 changeset: 2:97054abb4ab824450e9164180baf491ae0078465
2451 2451 bisect: untested
2452 2452 phase: public
2453 2453 parent: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965
2454 2454 parent: -1:0000000000000000000000000000000000000000
2455 2455 manifest: 2:6e0e82995c35d0d57a52aca8da4e56139e06b4b1
2456 2456 user: other@place
2457 2457 date: Wed Jan 14 21:20:00 1970 +0000
2458 2458 files+: c
2459 2459 extra: branch=default
2460 2460 description:
2461 2461 no person
2462 2462
2463 2463
2464 2464 changeset: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47
2465 2465 bisect: bad
2466 2466 phase: public
2467 2467 parent: 2:97054abb4ab824450e9164180baf491ae0078465
2468 2468 parent: -1:0000000000000000000000000000000000000000
2469 2469 manifest: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc
2470 2470 user: person
2471 2471 date: Fri Jan 16 01:06:40 1970 +0000
2472 2472 files: c
2473 2473 extra: branch=default
2474 2474 description:
2475 2475 no user, no domain
2476 2476
2477 2477
2478 2478 changeset: 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
2479 2479 bisect: bad (implicit)
2480 2480 branch: foo
2481 2481 phase: draft
2482 2482 parent: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47
2483 2483 parent: -1:0000000000000000000000000000000000000000
2484 2484 manifest: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc
2485 2485 user: person
2486 2486 date: Sat Jan 17 04:53:20 1970 +0000
2487 2487 extra: branch=foo
2488 2488 description:
2489 2489 new branch
2490 2490
2491 2491
2492 2492 $ hg log -v -T bisect -r 0:4
2493 2493 changeset: 0:1e4e1b8f71e0
2494 2494 bisect: good (implicit)
2495 2495 user: User Name <user@hostname>
2496 2496 date: Mon Jan 12 13:46:40 1970 +0000
2497 2497 files: a
2498 2498 description:
2499 2499 line 1
2500 2500 line 2
2501 2501
2502 2502
2503 2503 changeset: 1:b608e9d1a3f0
2504 2504 bisect: good
2505 2505 user: A. N. Other <other@place>
2506 2506 date: Tue Jan 13 17:33:20 1970 +0000
2507 2507 files: b
2508 2508 description:
2509 2509 other 1
2510 2510 other 2
2511 2511
2512 2512 other 3
2513 2513
2514 2514
2515 2515 changeset: 2:97054abb4ab8
2516 2516 bisect: untested
2517 2517 user: other@place
2518 2518 date: Wed Jan 14 21:20:00 1970 +0000
2519 2519 files: c
2520 2520 description:
2521 2521 no person
2522 2522
2523 2523
2524 2524 changeset: 3:10e46f2dcbf4
2525 2525 bisect: bad
2526 2526 user: person
2527 2527 date: Fri Jan 16 01:06:40 1970 +0000
2528 2528 files: c
2529 2529 description:
2530 2530 no user, no domain
2531 2531
2532 2532
2533 2533 changeset: 4:bbe44766e73d
2534 2534 bisect: bad (implicit)
2535 2535 branch: foo
2536 2536 user: person
2537 2537 date: Sat Jan 17 04:53:20 1970 +0000
2538 2538 description:
2539 2539 new branch
2540 2540
2541 2541
2542 2542 $ hg --color=debug log -T bisect -r 0:4
2543 2543 [log.changeset changeset.public|changeset: 0:1e4e1b8f71e0]
2544 2544 [log.bisect bisect.good|bisect: good (implicit)]
2545 2545 [log.user|user: User Name <user@hostname>]
2546 2546 [log.date|date: Mon Jan 12 13:46:40 1970 +0000]
2547 2547 [log.summary|summary: line 1]
2548 2548
2549 2549 [log.changeset changeset.public|changeset: 1:b608e9d1a3f0]
2550 2550 [log.bisect bisect.good|bisect: good]
2551 2551 [log.user|user: A. N. Other <other@place>]
2552 2552 [log.date|date: Tue Jan 13 17:33:20 1970 +0000]
2553 2553 [log.summary|summary: other 1]
2554 2554
2555 2555 [log.changeset changeset.public|changeset: 2:97054abb4ab8]
2556 2556 [log.bisect bisect.untested|bisect: untested]
2557 2557 [log.user|user: other@place]
2558 2558 [log.date|date: Wed Jan 14 21:20:00 1970 +0000]
2559 2559 [log.summary|summary: no person]
2560 2560
2561 2561 [log.changeset changeset.public|changeset: 3:10e46f2dcbf4]
2562 2562 [log.bisect bisect.bad|bisect: bad]
2563 2563 [log.user|user: person]
2564 2564 [log.date|date: Fri Jan 16 01:06:40 1970 +0000]
2565 2565 [log.summary|summary: no user, no domain]
2566 2566
2567 2567 [log.changeset changeset.draft|changeset: 4:bbe44766e73d]
2568 2568 [log.bisect bisect.bad|bisect: bad (implicit)]
2569 2569 [log.branch|branch: foo]
2570 2570 [log.user|user: person]
2571 2571 [log.date|date: Sat Jan 17 04:53:20 1970 +0000]
2572 2572 [log.summary|summary: new branch]
2573 2573
2574 2574 $ hg --color=debug log --debug -T bisect -r 0:4
2575 2575 [log.changeset changeset.public|changeset: 0:1e4e1b8f71e05681d422154f5421e385fec3454f]
2576 2576 [log.bisect bisect.good|bisect: good (implicit)]
2577 2577 [log.phase|phase: public]
2578 2578 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2579 2579 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2580 2580 [ui.debug log.manifest|manifest: 0:a0c8bcbbb45c63b90b70ad007bf38961f64f2af0]
2581 2581 [log.user|user: User Name <user@hostname>]
2582 2582 [log.date|date: Mon Jan 12 13:46:40 1970 +0000]
2583 2583 [ui.debug log.files|files+: a]
2584 2584 [ui.debug log.extra|extra: branch=default]
2585 2585 [ui.note log.description|description:]
2586 2586 [ui.note log.description|line 1
2587 2587 line 2]
2588 2588
2589 2589
2590 2590 [log.changeset changeset.public|changeset: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965]
2591 2591 [log.bisect bisect.good|bisect: good]
2592 2592 [log.phase|phase: public]
2593 2593 [log.parent changeset.public|parent: 0:1e4e1b8f71e05681d422154f5421e385fec3454f]
2594 2594 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2595 2595 [ui.debug log.manifest|manifest: 1:4e8d705b1e53e3f9375e0e60dc7b525d8211fe55]
2596 2596 [log.user|user: A. N. Other <other@place>]
2597 2597 [log.date|date: Tue Jan 13 17:33:20 1970 +0000]
2598 2598 [ui.debug log.files|files+: b]
2599 2599 [ui.debug log.extra|extra: branch=default]
2600 2600 [ui.note log.description|description:]
2601 2601 [ui.note log.description|other 1
2602 2602 other 2
2603 2603
2604 2604 other 3]
2605 2605
2606 2606
2607 2607 [log.changeset changeset.public|changeset: 2:97054abb4ab824450e9164180baf491ae0078465]
2608 2608 [log.bisect bisect.untested|bisect: untested]
2609 2609 [log.phase|phase: public]
2610 2610 [log.parent changeset.public|parent: 1:b608e9d1a3f0273ccf70fb85fd6866b3482bf965]
2611 2611 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2612 2612 [ui.debug log.manifest|manifest: 2:6e0e82995c35d0d57a52aca8da4e56139e06b4b1]
2613 2613 [log.user|user: other@place]
2614 2614 [log.date|date: Wed Jan 14 21:20:00 1970 +0000]
2615 2615 [ui.debug log.files|files+: c]
2616 2616 [ui.debug log.extra|extra: branch=default]
2617 2617 [ui.note log.description|description:]
2618 2618 [ui.note log.description|no person]
2619 2619
2620 2620
2621 2621 [log.changeset changeset.public|changeset: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47]
2622 2622 [log.bisect bisect.bad|bisect: bad]
2623 2623 [log.phase|phase: public]
2624 2624 [log.parent changeset.public|parent: 2:97054abb4ab824450e9164180baf491ae0078465]
2625 2625 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2626 2626 [ui.debug log.manifest|manifest: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc]
2627 2627 [log.user|user: person]
2628 2628 [log.date|date: Fri Jan 16 01:06:40 1970 +0000]
2629 2629 [ui.debug log.files|files: c]
2630 2630 [ui.debug log.extra|extra: branch=default]
2631 2631 [ui.note log.description|description:]
2632 2632 [ui.note log.description|no user, no domain]
2633 2633
2634 2634
2635 2635 [log.changeset changeset.draft|changeset: 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74]
2636 2636 [log.bisect bisect.bad|bisect: bad (implicit)]
2637 2637 [log.branch|branch: foo]
2638 2638 [log.phase|phase: draft]
2639 2639 [log.parent changeset.public|parent: 3:10e46f2dcbf4823578cf180f33ecf0b957964c47]
2640 2640 [log.parent changeset.public|parent: -1:0000000000000000000000000000000000000000]
2641 2641 [ui.debug log.manifest|manifest: 3:cb5a1327723bada42f117e4c55a303246eaf9ccc]
2642 2642 [log.user|user: person]
2643 2643 [log.date|date: Sat Jan 17 04:53:20 1970 +0000]
2644 2644 [ui.debug log.extra|extra: branch=foo]
2645 2645 [ui.note log.description|description:]
2646 2646 [ui.note log.description|new branch]
2647 2647
2648 2648
2649 2649 $ hg --color=debug log -v -T bisect -r 0:4
2650 2650 [log.changeset changeset.public|changeset: 0:1e4e1b8f71e0]
2651 2651 [log.bisect bisect.good|bisect: good (implicit)]
2652 2652 [log.user|user: User Name <user@hostname>]
2653 2653 [log.date|date: Mon Jan 12 13:46:40 1970 +0000]
2654 2654 [ui.note log.files|files: a]
2655 2655 [ui.note log.description|description:]
2656 2656 [ui.note log.description|line 1
2657 2657 line 2]
2658 2658
2659 2659
2660 2660 [log.changeset changeset.public|changeset: 1:b608e9d1a3f0]
2661 2661 [log.bisect bisect.good|bisect: good]
2662 2662 [log.user|user: A. N. Other <other@place>]
2663 2663 [log.date|date: Tue Jan 13 17:33:20 1970 +0000]
2664 2664 [ui.note log.files|files: b]
2665 2665 [ui.note log.description|description:]
2666 2666 [ui.note log.description|other 1
2667 2667 other 2
2668 2668
2669 2669 other 3]
2670 2670
2671 2671
2672 2672 [log.changeset changeset.public|changeset: 2:97054abb4ab8]
2673 2673 [log.bisect bisect.untested|bisect: untested]
2674 2674 [log.user|user: other@place]
2675 2675 [log.date|date: Wed Jan 14 21:20:00 1970 +0000]
2676 2676 [ui.note log.files|files: c]
2677 2677 [ui.note log.description|description:]
2678 2678 [ui.note log.description|no person]
2679 2679
2680 2680
2681 2681 [log.changeset changeset.public|changeset: 3:10e46f2dcbf4]
2682 2682 [log.bisect bisect.bad|bisect: bad]
2683 2683 [log.user|user: person]
2684 2684 [log.date|date: Fri Jan 16 01:06:40 1970 +0000]
2685 2685 [ui.note log.files|files: c]
2686 2686 [ui.note log.description|description:]
2687 2687 [ui.note log.description|no user, no domain]
2688 2688
2689 2689
2690 2690 [log.changeset changeset.draft|changeset: 4:bbe44766e73d]
2691 2691 [log.bisect bisect.bad|bisect: bad (implicit)]
2692 2692 [log.branch|branch: foo]
2693 2693 [log.user|user: person]
2694 2694 [log.date|date: Sat Jan 17 04:53:20 1970 +0000]
2695 2695 [ui.note log.description|description:]
2696 2696 [ui.note log.description|new branch]
2697 2697
2698 2698
2699 2699 $ hg bisect --reset
2700 2700
2701 2701 Error on syntax:
2702 2702
2703 2703 $ echo 'x = "f' >> t
2704 2704 $ hg log
2705 2705 hg: parse error at t:3: unmatched quotes
2706 2706 [255]
2707 2707
2708 2708 $ hg log -T '{date'
2709 2709 hg: parse error at 1: unterminated template expansion
2710 2710 [255]
2711 2711
2712 2712 Behind the scenes, this will throw TypeError
2713 2713
2714 2714 $ hg log -l 3 --template '{date|obfuscate}\n'
2715 2715 abort: template filter 'obfuscate' is not compatible with keyword 'date'
2716 2716 [255]
2717 2717
2718 2718 Behind the scenes, this will throw a ValueError
2719 2719
2720 2720 $ hg log -l 3 --template 'line: {desc|shortdate}\n'
2721 2721 abort: template filter 'shortdate' is not compatible with keyword 'desc'
2722 2722 [255]
2723 2723
2724 2724 Behind the scenes, this will throw AttributeError
2725 2725
2726 2726 $ hg log -l 3 --template 'line: {date|escape}\n'
2727 2727 abort: template filter 'escape' is not compatible with keyword 'date'
2728 2728 [255]
2729 2729
2730 2730 $ hg log -l 3 --template 'line: {extras|localdate}\n'
2731 2731 hg: parse error: localdate expects a date information
2732 2732 [255]
2733 2733
2734 2734 Behind the scenes, this will throw ValueError
2735 2735
2736 2736 $ hg tip --template '{author|email|date}\n'
2737 2737 hg: parse error: date expects a date information
2738 2738 [255]
2739 2739
2740 2740 $ hg tip -T '{author|email|shortdate}\n'
2741 2741 abort: template filter 'shortdate' is not compatible with keyword 'author'
2742 2742 [255]
2743 2743
2744 2744 $ hg tip -T '{get(extras, "branch")|shortdate}\n'
2745 2745 abort: incompatible use of template filter 'shortdate'
2746 2746 [255]
2747 2747
2748 2748 Error in nested template:
2749 2749
2750 2750 $ hg log -T '{"date'
2751 2751 hg: parse error at 2: unterminated string
2752 2752 [255]
2753 2753
2754 2754 $ hg log -T '{"foo{date|?}"}'
2755 2755 hg: parse error at 11: syntax error
2756 2756 [255]
2757 2757
2758 2758 Thrown an error if a template function doesn't exist
2759 2759
2760 2760 $ hg tip --template '{foo()}\n'
2761 2761 hg: parse error: unknown function 'foo'
2762 2762 [255]
2763 2763
2764 2764 Pass generator object created by template function to filter
2765 2765
2766 2766 $ hg log -l 1 --template '{if(author, author)|user}\n'
2767 2767 test
2768 2768
2769 2769 Test index keyword:
2770 2770
2771 2771 $ hg log -l 2 -T '{index + 10}{files % " {index}:{file}"}\n'
2772 2772 10 0:a 1:b 2:fifth 3:fourth 4:third
2773 2773 11 0:a
2774 2774
2775 2775 $ hg branches -T '{index} {branch}\n'
2776 2776 0 default
2777 2777 1 foo
2778 2778
2779 2779 Test diff function:
2780 2780
2781 2781 $ hg diff -c 8
2782 2782 diff -r 29114dbae42b -r 95c24699272e fourth
2783 2783 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2784 2784 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
2785 2785 @@ -0,0 +1,1 @@
2786 2786 +second
2787 2787 diff -r 29114dbae42b -r 95c24699272e second
2788 2788 --- a/second Mon Jan 12 13:46:40 1970 +0000
2789 2789 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
2790 2790 @@ -1,1 +0,0 @@
2791 2791 -second
2792 2792 diff -r 29114dbae42b -r 95c24699272e third
2793 2793 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2794 2794 +++ b/third Wed Jan 01 10:01:00 2020 +0000
2795 2795 @@ -0,0 +1,1 @@
2796 2796 +third
2797 2797
2798 2798 $ hg log -r 8 -T "{diff()}"
2799 2799 diff -r 29114dbae42b -r 95c24699272e fourth
2800 2800 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2801 2801 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
2802 2802 @@ -0,0 +1,1 @@
2803 2803 +second
2804 2804 diff -r 29114dbae42b -r 95c24699272e second
2805 2805 --- a/second Mon Jan 12 13:46:40 1970 +0000
2806 2806 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
2807 2807 @@ -1,1 +0,0 @@
2808 2808 -second
2809 2809 diff -r 29114dbae42b -r 95c24699272e third
2810 2810 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2811 2811 +++ b/third Wed Jan 01 10:01:00 2020 +0000
2812 2812 @@ -0,0 +1,1 @@
2813 2813 +third
2814 2814
2815 2815 $ hg log -r 8 -T "{diff('glob:f*')}"
2816 2816 diff -r 29114dbae42b -r 95c24699272e fourth
2817 2817 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2818 2818 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
2819 2819 @@ -0,0 +1,1 @@
2820 2820 +second
2821 2821
2822 2822 $ hg log -r 8 -T "{diff('', 'glob:f*')}"
2823 2823 diff -r 29114dbae42b -r 95c24699272e second
2824 2824 --- a/second Mon Jan 12 13:46:40 1970 +0000
2825 2825 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
2826 2826 @@ -1,1 +0,0 @@
2827 2827 -second
2828 2828 diff -r 29114dbae42b -r 95c24699272e third
2829 2829 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2830 2830 +++ b/third Wed Jan 01 10:01:00 2020 +0000
2831 2831 @@ -0,0 +1,1 @@
2832 2832 +third
2833 2833
2834 2834 $ hg log -r 8 -T "{diff('FOURTH'|lower)}"
2835 2835 diff -r 29114dbae42b -r 95c24699272e fourth
2836 2836 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2837 2837 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
2838 2838 @@ -0,0 +1,1 @@
2839 2839 +second
2840 2840
2841 2841 $ cd ..
2842 2842
2843 2843
2844 2844 latesttag:
2845 2845
2846 2846 $ hg init latesttag
2847 2847 $ cd latesttag
2848 2848
2849 2849 $ echo a > file
2850 2850 $ hg ci -Am a -d '0 0'
2851 2851 adding file
2852 2852
2853 2853 $ echo b >> file
2854 2854 $ hg ci -m b -d '1 0'
2855 2855
2856 2856 $ echo c >> head1
2857 2857 $ hg ci -Am h1c -d '2 0'
2858 2858 adding head1
2859 2859
2860 2860 $ hg update -q 1
2861 2861 $ echo d >> head2
2862 2862 $ hg ci -Am h2d -d '3 0'
2863 2863 adding head2
2864 2864 created new head
2865 2865
2866 2866 $ echo e >> head2
2867 2867 $ hg ci -m h2e -d '4 0'
2868 2868
2869 2869 $ hg merge -q
2870 2870 $ hg ci -m merge -d '5 -3600'
2871 2871
2872 2872 No tag set:
2873 2873
2874 2874 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
2875 2875 5: null+5
2876 2876 4: null+4
2877 2877 3: null+3
2878 2878 2: null+3
2879 2879 1: null+2
2880 2880 0: null+1
2881 2881
2882 2882 One common tag: longest path wins:
2883 2883
2884 2884 $ hg tag -r 1 -m t1 -d '6 0' t1
2885 2885 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
2886 2886 6: t1+4
2887 2887 5: t1+3
2888 2888 4: t1+2
2889 2889 3: t1+1
2890 2890 2: t1+1
2891 2891 1: t1+0
2892 2892 0: null+1
2893 2893
2894 2894 One ancestor tag: more recent wins:
2895 2895
2896 2896 $ hg tag -r 2 -m t2 -d '7 0' t2
2897 2897 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
2898 2898 7: t2+3
2899 2899 6: t2+2
2900 2900 5: t2+1
2901 2901 4: t1+2
2902 2902 3: t1+1
2903 2903 2: t2+0
2904 2904 1: t1+0
2905 2905 0: null+1
2906 2906
2907 2907 Two branch tags: more recent wins:
2908 2908
2909 2909 $ hg tag -r 3 -m t3 -d '8 0' t3
2910 2910 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
2911 2911 8: t3+5
2912 2912 7: t3+4
2913 2913 6: t3+3
2914 2914 5: t3+2
2915 2915 4: t3+1
2916 2916 3: t3+0
2917 2917 2: t2+0
2918 2918 1: t1+0
2919 2919 0: null+1
2920 2920
2921 2921 Merged tag overrides:
2922 2922
2923 2923 $ hg tag -r 5 -m t5 -d '9 0' t5
2924 2924 $ hg tag -r 3 -m at3 -d '10 0' at3
2925 2925 $ hg log --template '{rev}: {latesttag}+{latesttagdistance}\n'
2926 2926 10: t5+5
2927 2927 9: t5+4
2928 2928 8: t5+3
2929 2929 7: t5+2
2930 2930 6: t5+1
2931 2931 5: t5+0
2932 2932 4: at3:t3+1
2933 2933 3: at3:t3+0
2934 2934 2: t2+0
2935 2935 1: t1+0
2936 2936 0: null+1
2937 2937
2938 2938 $ hg log --template "{rev}: {latesttag % '{tag}+{distance},{changes} '}\n"
2939 2939 10: t5+5,5
2940 2940 9: t5+4,4
2941 2941 8: t5+3,3
2942 2942 7: t5+2,2
2943 2943 6: t5+1,1
2944 2944 5: t5+0,0
2945 2945 4: at3+1,1 t3+1,1
2946 2946 3: at3+0,0 t3+0,0
2947 2947 2: t2+0,0
2948 2948 1: t1+0,0
2949 2949 0: null+1,1
2950 2950
2951 2951 $ hg log --template "{rev}: {latesttag('re:^t[13]$') % '{tag}, C: {changes}, D: {distance}'}\n"
2952 2952 10: t3, C: 8, D: 7
2953 2953 9: t3, C: 7, D: 6
2954 2954 8: t3, C: 6, D: 5
2955 2955 7: t3, C: 5, D: 4
2956 2956 6: t3, C: 4, D: 3
2957 2957 5: t3, C: 3, D: 2
2958 2958 4: t3, C: 1, D: 1
2959 2959 3: t3, C: 0, D: 0
2960 2960 2: t1, C: 1, D: 1
2961 2961 1: t1, C: 0, D: 0
2962 2962 0: null, C: 1, D: 1
2963 2963
2964 2964 $ cd ..
2965 2965
2966 2966
2967 2967 Style path expansion: issue1948 - ui.style option doesn't work on OSX
2968 2968 if it is a relative path
2969 2969
2970 2970 $ mkdir -p home/styles
2971 2971
2972 2972 $ cat > home/styles/teststyle <<EOF
2973 2973 > changeset = 'test {rev}:{node|short}\n'
2974 2974 > EOF
2975 2975
2976 2976 $ HOME=`pwd`/home; export HOME
2977 2977
2978 2978 $ cat > latesttag/.hg/hgrc <<EOF
2979 2979 > [ui]
2980 2980 > style = ~/styles/teststyle
2981 2981 > EOF
2982 2982
2983 2983 $ hg -R latesttag tip
2984 2984 test 10:9b4a630e5f5f
2985 2985
2986 2986 Test recursive showlist template (issue1989):
2987 2987
2988 2988 $ cat > style1989 <<EOF
2989 2989 > changeset = '{file_mods}{manifest}{extras}'
2990 2990 > file_mod = 'M|{author|person}\n'
2991 2991 > manifest = '{rev},{author}\n'
2992 2992 > extra = '{key}: {author}\n'
2993 2993 > EOF
2994 2994
2995 2995 $ hg -R latesttag log -r tip --style=style1989
2996 2996 M|test
2997 2997 10,test
2998 2998 branch: test
2999 2999
3000 3000 Test new-style inline templating:
3001 3001
3002 3002 $ hg log -R latesttag -r tip --template 'modified files: {file_mods % " {file}\n"}\n'
3003 3003 modified files: .hgtags
3004 3004
3005 3005
3006 3006 $ hg log -R latesttag -r tip -T '{rev % "a"}\n'
3007 3007 hg: parse error: keyword 'rev' is not iterable
3008 3008 [255]
3009 3009 $ hg log -R latesttag -r tip -T '{get(extras, "unknown") % "a"}\n'
3010 3010 hg: parse error: None is not iterable
3011 3011 [255]
3012 3012
3013 3013 Test the sub function of templating for expansion:
3014 3014
3015 3015 $ hg log -R latesttag -r 10 --template '{sub("[0-9]", "x", "{rev}")}\n'
3016 3016 xx
3017 3017
3018 3018 $ hg log -R latesttag -r 10 -T '{sub("[", "x", rev)}\n'
3019 3019 hg: parse error: sub got an invalid pattern: [
3020 3020 [255]
3021 3021 $ hg log -R latesttag -r 10 -T '{sub("[0-9]", r"\1", rev)}\n'
3022 3022 hg: parse error: sub got an invalid replacement: \1
3023 3023 [255]
3024 3024
3025 3025 Test the strip function with chars specified:
3026 3026
3027 3027 $ hg log -R latesttag --template '{desc}\n'
3028 3028 at3
3029 3029 t5
3030 3030 t3
3031 3031 t2
3032 3032 t1
3033 3033 merge
3034 3034 h2e
3035 3035 h2d
3036 3036 h1c
3037 3037 b
3038 3038 a
3039 3039
3040 3040 $ hg log -R latesttag --template '{strip(desc, "te")}\n'
3041 3041 at3
3042 3042 5
3043 3043 3
3044 3044 2
3045 3045 1
3046 3046 merg
3047 3047 h2
3048 3048 h2d
3049 3049 h1c
3050 3050 b
3051 3051 a
3052 3052
3053 3053 Test date format:
3054 3054
3055 3055 $ hg log -R latesttag --template 'date: {date(date, "%y %m %d %S %z")}\n'
3056 3056 date: 70 01 01 10 +0000
3057 3057 date: 70 01 01 09 +0000
3058 3058 date: 70 01 01 08 +0000
3059 3059 date: 70 01 01 07 +0000
3060 3060 date: 70 01 01 06 +0000
3061 3061 date: 70 01 01 05 +0100
3062 3062 date: 70 01 01 04 +0000
3063 3063 date: 70 01 01 03 +0000
3064 3064 date: 70 01 01 02 +0000
3065 3065 date: 70 01 01 01 +0000
3066 3066 date: 70 01 01 00 +0000
3067 3067
3068 3068 Test invalid date:
3069 3069
3070 3070 $ hg log -R latesttag -T '{date(rev)}\n'
3071 3071 hg: parse error: date expects a date information
3072 3072 [255]
3073 3073
3074 3074 Test integer literal:
3075 3075
3076 3076 $ hg debugtemplate -v '{(0)}\n'
3077 3077 (template
3078 3078 (group
3079 3079 ('integer', '0'))
3080 3080 ('string', '\n'))
3081 3081 0
3082 3082 $ hg debugtemplate -v '{(123)}\n'
3083 3083 (template
3084 3084 (group
3085 3085 ('integer', '123'))
3086 3086 ('string', '\n'))
3087 3087 123
3088 3088 $ hg debugtemplate -v '{(-4)}\n'
3089 3089 (template
3090 3090 (group
3091 3091 (negate
3092 3092 ('integer', '4')))
3093 3093 ('string', '\n'))
3094 3094 -4
3095 3095 $ hg debugtemplate '{(-)}\n'
3096 3096 hg: parse error at 3: not a prefix: )
3097 3097 [255]
3098 3098 $ hg debugtemplate '{(-a)}\n'
3099 3099 hg: parse error: negation needs an integer argument
3100 3100 [255]
3101 3101
3102 3102 top-level integer literal is interpreted as symbol (i.e. variable name):
3103 3103
3104 3104 $ hg debugtemplate -D 1=one -v '{1}\n'
3105 3105 (template
3106 3106 ('integer', '1')
3107 3107 ('string', '\n'))
3108 3108 one
3109 3109 $ hg debugtemplate -D 1=one -v '{if("t", "{1}")}\n'
3110 3110 (template
3111 3111 (func
3112 3112 ('symbol', 'if')
3113 3113 (list
3114 3114 ('string', 't')
3115 3115 (template
3116 3116 ('integer', '1'))))
3117 3117 ('string', '\n'))
3118 3118 one
3119 3119 $ hg debugtemplate -D 1=one -v '{1|stringify}\n'
3120 3120 (template
3121 3121 (|
3122 3122 ('integer', '1')
3123 3123 ('symbol', 'stringify'))
3124 3124 ('string', '\n'))
3125 3125 one
3126 3126
3127 3127 unless explicit symbol is expected:
3128 3128
3129 3129 $ hg log -Ra -r0 -T '{desc|1}\n'
3130 3130 hg: parse error: expected a symbol, got 'integer'
3131 3131 [255]
3132 3132 $ hg log -Ra -r0 -T '{1()}\n'
3133 3133 hg: parse error: expected a symbol, got 'integer'
3134 3134 [255]
3135 3135
3136 3136 Test string literal:
3137 3137
3138 3138 $ hg debugtemplate -Ra -r0 -v '{"string with no template fragment"}\n'
3139 3139 (template
3140 3140 ('string', 'string with no template fragment')
3141 3141 ('string', '\n'))
3142 3142 string with no template fragment
3143 3143 $ hg debugtemplate -Ra -r0 -v '{"template: {rev}"}\n'
3144 3144 (template
3145 3145 (template
3146 3146 ('string', 'template: ')
3147 3147 ('symbol', 'rev'))
3148 3148 ('string', '\n'))
3149 3149 template: 0
3150 3150 $ hg debugtemplate -Ra -r0 -v '{r"rawstring: {rev}"}\n'
3151 3151 (template
3152 3152 ('string', 'rawstring: {rev}')
3153 3153 ('string', '\n'))
3154 3154 rawstring: {rev}
3155 3155 $ hg debugtemplate -Ra -r0 -v '{files % r"rawstring: {file}"}\n'
3156 3156 (template
3157 3157 (%
3158 3158 ('symbol', 'files')
3159 3159 ('string', 'rawstring: {file}'))
3160 3160 ('string', '\n'))
3161 3161 rawstring: {file}
3162 3162
3163 3163 Test string escaping:
3164 3164
3165 3165 $ hg log -R latesttag -r 0 --template '>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
3166 3166 >
3167 3167 <>\n<[>
3168 3168 <>\n<]>
3169 3169 <>\n<
3170 3170
3171 3171 $ hg log -R latesttag -r 0 \
3172 3172 > --config ui.logtemplate='>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
3173 3173 >
3174 3174 <>\n<[>
3175 3175 <>\n<]>
3176 3176 <>\n<
3177 3177
3178 3178 $ hg log -R latesttag -r 0 -T esc \
3179 3179 > --config templates.esc='>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
3180 3180 >
3181 3181 <>\n<[>
3182 3182 <>\n<]>
3183 3183 <>\n<
3184 3184
3185 3185 $ cat <<'EOF' > esctmpl
3186 3186 > changeset = '>\n<>\\n<{if(rev, "[>\n<>\\n<]")}>\n<>\\n<\n'
3187 3187 > EOF
3188 3188 $ hg log -R latesttag -r 0 --style ./esctmpl
3189 3189 >
3190 3190 <>\n<[>
3191 3191 <>\n<]>
3192 3192 <>\n<
3193 3193
3194 3194 Test string escaping of quotes:
3195 3195
3196 3196 $ hg log -Ra -r0 -T '{"\""}\n'
3197 3197 "
3198 3198 $ hg log -Ra -r0 -T '{"\\\""}\n'
3199 3199 \"
3200 3200 $ hg log -Ra -r0 -T '{r"\""}\n'
3201 3201 \"
3202 3202 $ hg log -Ra -r0 -T '{r"\\\""}\n'
3203 3203 \\\"
3204 3204
3205 3205
3206 3206 $ hg log -Ra -r0 -T '{"\""}\n'
3207 3207 "
3208 3208 $ hg log -Ra -r0 -T '{"\\\""}\n'
3209 3209 \"
3210 3210 $ hg log -Ra -r0 -T '{r"\""}\n'
3211 3211 \"
3212 3212 $ hg log -Ra -r0 -T '{r"\\\""}\n'
3213 3213 \\\"
3214 3214
3215 3215 Test exception in quoted template. single backslash before quotation mark is
3216 3216 stripped before parsing:
3217 3217
3218 3218 $ cat <<'EOF' > escquotetmpl
3219 3219 > changeset = "\" \\" \\\" \\\\" {files % \"{file}\"}\n"
3220 3220 > EOF
3221 3221 $ cd latesttag
3222 3222 $ hg log -r 2 --style ../escquotetmpl
3223 3223 " \" \" \\" head1
3224 3224
3225 3225 $ hg log -r 2 -T esc --config templates.esc='"{\"valid\"}\n"'
3226 3226 valid
3227 3227 $ hg log -r 2 -T esc --config templates.esc="'"'{\'"'"'valid\'"'"'}\n'"'"
3228 3228 valid
3229 3229
3230 3230 Test compatibility with 2.9.2-3.4 of escaped quoted strings in nested
3231 3231 _evalifliteral() templates (issue4733):
3232 3232
3233 3233 $ hg log -r 2 -T '{if(rev, "\"{rev}")}\n'
3234 3234 "2
3235 3235 $ hg log -r 2 -T '{if(rev, "{if(rev, \"\\\"{rev}\")}")}\n'
3236 3236 "2
3237 3237 $ hg log -r 2 -T '{if(rev, "{if(rev, \"{if(rev, \\\"\\\\\\\"{rev}\\\")}\")}")}\n'
3238 3238 "2
3239 3239
3240 3240 $ hg log -r 2 -T '{if(rev, "\\\"")}\n'
3241 3241 \"
3242 3242 $ hg log -r 2 -T '{if(rev, "{if(rev, \"\\\\\\\"\")}")}\n'
3243 3243 \"
3244 3244 $ hg log -r 2 -T '{if(rev, "{if(rev, \"{if(rev, \\\"\\\\\\\\\\\\\\\"\\\")}\")}")}\n'
3245 3245 \"
3246 3246
3247 3247 $ hg log -r 2 -T '{if(rev, r"\\\"")}\n'
3248 3248 \\\"
3249 3249 $ hg log -r 2 -T '{if(rev, "{if(rev, r\"\\\\\\\"\")}")}\n'
3250 3250 \\\"
3251 3251 $ hg log -r 2 -T '{if(rev, "{if(rev, \"{if(rev, r\\\"\\\\\\\\\\\\\\\"\\\")}\")}")}\n'
3252 3252 \\\"
3253 3253
3254 3254 escaped single quotes and errors:
3255 3255
3256 3256 $ hg log -r 2 -T "{if(rev, '{if(rev, \'foo\')}')}"'\n'
3257 3257 foo
3258 3258 $ hg log -r 2 -T "{if(rev, '{if(rev, r\'foo\')}')}"'\n'
3259 3259 foo
3260 3260 $ hg log -r 2 -T '{if(rev, "{if(rev, \")}")}\n'
3261 3261 hg: parse error at 21: unterminated string
3262 3262 [255]
3263 3263 $ hg log -r 2 -T '{if(rev, \"\\"")}\n'
3264 3264 hg: parse error: trailing \ in string
3265 3265 [255]
3266 3266 $ hg log -r 2 -T '{if(rev, r\"\\"")}\n'
3267 3267 hg: parse error: trailing \ in string
3268 3268 [255]
3269 3269
3270 3270 $ cd ..
3271 3271
3272 3272 Test leading backslashes:
3273 3273
3274 3274 $ cd latesttag
3275 3275 $ hg log -r 2 -T '\{rev} {files % "\{file}"}\n'
3276 3276 {rev} {file}
3277 3277 $ hg log -r 2 -T '\\{rev} {files % "\\{file}"}\n'
3278 3278 \2 \head1
3279 3279 $ hg log -r 2 -T '\\\{rev} {files % "\\\{file}"}\n'
3280 3280 \{rev} \{file}
3281 3281 $ cd ..
3282 3282
3283 3283 Test leading backslashes in "if" expression (issue4714):
3284 3284
3285 3285 $ cd latesttag
3286 3286 $ hg log -r 2 -T '{if("1", "\{rev}")} {if("1", r"\{rev}")}\n'
3287 3287 {rev} \{rev}
3288 3288 $ hg log -r 2 -T '{if("1", "\\{rev}")} {if("1", r"\\{rev}")}\n'
3289 3289 \2 \\{rev}
3290 3290 $ hg log -r 2 -T '{if("1", "\\\{rev}")} {if("1", r"\\\{rev}")}\n'
3291 3291 \{rev} \\\{rev}
3292 3292 $ cd ..
3293 3293
3294 3294 "string-escape"-ed "\x5c\x786e" becomes r"\x6e" (once) or r"n" (twice)
3295 3295
3296 3296 $ hg log -R a -r 0 --template '{if("1", "\x5c\x786e", "NG")}\n'
3297 3297 \x6e
3298 3298 $ hg log -R a -r 0 --template '{if("1", r"\x5c\x786e", "NG")}\n'
3299 3299 \x5c\x786e
3300 3300 $ hg log -R a -r 0 --template '{if("", "NG", "\x5c\x786e")}\n'
3301 3301 \x6e
3302 3302 $ hg log -R a -r 0 --template '{if("", "NG", r"\x5c\x786e")}\n'
3303 3303 \x5c\x786e
3304 3304
3305 3305 $ hg log -R a -r 2 --template '{ifeq("no perso\x6e", desc, "\x5c\x786e", "NG")}\n'
3306 3306 \x6e
3307 3307 $ hg log -R a -r 2 --template '{ifeq(r"no perso\x6e", desc, "NG", r"\x5c\x786e")}\n'
3308 3308 \x5c\x786e
3309 3309 $ hg log -R a -r 2 --template '{ifeq(desc, "no perso\x6e", "\x5c\x786e", "NG")}\n'
3310 3310 \x6e
3311 3311 $ hg log -R a -r 2 --template '{ifeq(desc, r"no perso\x6e", "NG", r"\x5c\x786e")}\n'
3312 3312 \x5c\x786e
3313 3313
3314 3314 $ hg log -R a -r 8 --template '{join(files, "\n")}\n'
3315 3315 fourth
3316 3316 second
3317 3317 third
3318 3318 $ hg log -R a -r 8 --template '{join(files, r"\n")}\n'
3319 3319 fourth\nsecond\nthird
3320 3320
3321 3321 $ hg log -R a -r 2 --template '{rstdoc("1st\n\n2nd", "htm\x6c")}'
3322 3322 <p>
3323 3323 1st
3324 3324 </p>
3325 3325 <p>
3326 3326 2nd
3327 3327 </p>
3328 3328 $ hg log -R a -r 2 --template '{rstdoc(r"1st\n\n2nd", "html")}'
3329 3329 <p>
3330 3330 1st\n\n2nd
3331 3331 </p>
3332 3332 $ hg log -R a -r 2 --template '{rstdoc("1st\n\n2nd", r"htm\x6c")}'
3333 3333 1st
3334 3334
3335 3335 2nd
3336 3336
3337 3337 $ hg log -R a -r 2 --template '{strip(desc, "\x6e")}\n'
3338 3338 o perso
3339 3339 $ hg log -R a -r 2 --template '{strip(desc, r"\x6e")}\n'
3340 3340 no person
3341 3341 $ hg log -R a -r 2 --template '{strip("no perso\x6e", "\x6e")}\n'
3342 3342 o perso
3343 3343 $ hg log -R a -r 2 --template '{strip(r"no perso\x6e", r"\x6e")}\n'
3344 3344 no perso
3345 3345
3346 3346 $ hg log -R a -r 2 --template '{sub("\\x6e", "\x2d", desc)}\n'
3347 3347 -o perso-
3348 3348 $ hg log -R a -r 2 --template '{sub(r"\\x6e", "-", desc)}\n'
3349 3349 no person
3350 3350 $ hg log -R a -r 2 --template '{sub("n", r"\x2d", desc)}\n'
3351 3351 \x2do perso\x2d
3352 3352 $ hg log -R a -r 2 --template '{sub("n", "\x2d", "no perso\x6e")}\n'
3353 3353 -o perso-
3354 3354 $ hg log -R a -r 2 --template '{sub("n", r"\x2d", r"no perso\x6e")}\n'
3355 3355 \x2do perso\x6e
3356 3356
3357 3357 $ hg log -R a -r 8 --template '{files % "{file}\n"}'
3358 3358 fourth
3359 3359 second
3360 3360 third
3361 3361
3362 3362 Test string escaping in nested expression:
3363 3363
3364 3364 $ hg log -R a -r 8 --template '{ifeq(r"\x6e", if("1", "\x5c\x786e"), join(files, "\x5c\x786e"))}\n'
3365 3365 fourth\x6esecond\x6ethird
3366 3366 $ hg log -R a -r 8 --template '{ifeq(if("1", r"\x6e"), "\x5c\x786e", join(files, "\x5c\x786e"))}\n'
3367 3367 fourth\x6esecond\x6ethird
3368 3368
3369 3369 $ hg log -R a -r 8 --template '{join(files, ifeq(branch, "default", "\x5c\x786e"))}\n'
3370 3370 fourth\x6esecond\x6ethird
3371 3371 $ hg log -R a -r 8 --template '{join(files, ifeq(branch, "default", r"\x5c\x786e"))}\n'
3372 3372 fourth\x5c\x786esecond\x5c\x786ethird
3373 3373
3374 3374 $ hg log -R a -r 3:4 --template '{rev}:{sub(if("1", "\x6e"), ifeq(branch, "foo", r"\x5c\x786e", "\x5c\x786e"), desc)}\n'
3375 3375 3:\x6eo user, \x6eo domai\x6e
3376 3376 4:\x5c\x786eew bra\x5c\x786ech
3377 3377
3378 3378 Test quotes in nested expression are evaluated just like a $(command)
3379 3379 substitution in POSIX shells:
3380 3380
3381 3381 $ hg log -R a -r 8 -T '{"{"{rev}:{node|short}"}"}\n'
3382 3382 8:95c24699272e
3383 3383 $ hg log -R a -r 8 -T '{"{"\{{rev}} \"{node|short}\""}"}\n'
3384 3384 {8} "95c24699272e"
3385 3385
3386 3386 Test recursive evaluation:
3387 3387
3388 3388 $ hg init r
3389 3389 $ cd r
3390 3390 $ echo a > a
3391 3391 $ hg ci -Am '{rev}'
3392 3392 adding a
3393 3393 $ hg log -r 0 --template '{if(rev, desc)}\n'
3394 3394 {rev}
3395 3395 $ hg log -r 0 --template '{if(rev, "{author} {rev}")}\n'
3396 3396 test 0
3397 3397
3398 3398 $ hg branch -q 'text.{rev}'
3399 3399 $ echo aa >> aa
3400 3400 $ hg ci -u '{node|short}' -m 'desc to be wrapped desc to be wrapped'
3401 3401
3402 3402 $ hg log -l1 --template '{fill(desc, "20", author, branch)}'
3403 3403 {node|short}desc to
3404 3404 text.{rev}be wrapped
3405 3405 text.{rev}desc to be
3406 3406 text.{rev}wrapped (no-eol)
3407 3407 $ hg log -l1 --template '{fill(desc, "20", "{node|short}:", "text.{rev}:")}'
3408 3408 bcc7ff960b8e:desc to
3409 3409 text.1:be wrapped
3410 3410 text.1:desc to be
3411 3411 text.1:wrapped (no-eol)
3412 3412 $ hg log -l1 -T '{fill(desc, date, "", "")}\n'
3413 3413 hg: parse error: fill expects an integer width
3414 3414 [255]
3415 3415
3416 3416 $ COLUMNS=25 hg log -l1 --template '{fill(desc, termwidth, "{node|short}:", "termwidth.{rev}:")}'
3417 3417 bcc7ff960b8e:desc to be
3418 3418 termwidth.1:wrapped desc
3419 3419 termwidth.1:to be wrapped (no-eol)
3420 3420
3421 3421 $ hg log -l 1 --template '{sub(r"[0-9]", "-", author)}'
3422 3422 {node|short} (no-eol)
3423 3423 $ hg log -l 1 --template '{sub(r"[0-9]", "-", "{node|short}")}'
3424 3424 bcc-ff---b-e (no-eol)
3425 3425
3426 3426 $ cat >> .hg/hgrc <<EOF
3427 3427 > [extensions]
3428 3428 > color=
3429 3429 > [color]
3430 3430 > mode=ansi
3431 3431 > text.{rev} = red
3432 3432 > text.1 = green
3433 3433 > EOF
3434 3434 $ hg log --color=always -l 1 --template '{label(branch, "text\n")}'
3435 3435 \x1b[0;31mtext\x1b[0m (esc)
3436 3436 $ hg log --color=always -l 1 --template '{label("text.{rev}", "text\n")}'
3437 3437 \x1b[0;32mtext\x1b[0m (esc)
3438 3438
3439 3439 color effect can be specified without quoting:
3440 3440
3441 3441 $ hg log --color=always -l 1 --template '{label(red, "text\n")}'
3442 3442 \x1b[0;31mtext\x1b[0m (esc)
3443 3443
3444 3444 color effects can be nested (issue5413)
3445 3445
3446 3446 $ hg debugtemplate --color=always \
3447 3447 > '{label(red, "red{label(magenta, "ma{label(cyan, "cyan")}{label(yellow, "yellow")}genta")}")}\n'
3448 3448 \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)
3449 3449
3450 3450 pad() should interact well with color codes (issue5416)
3451 3451
3452 3452 $ hg debugtemplate --color=always \
3453 3453 > '{pad(label(red, "red"), 5, label(cyan, "-"))}\n'
3454 3454 \x1b[0;31mred\x1b[0m\x1b[0;36m-\x1b[0m\x1b[0;36m-\x1b[0m (esc)
3455 3455
3456 3456 label should be no-op if color is disabled:
3457 3457
3458 3458 $ hg log --color=never -l 1 --template '{label(red, "text\n")}'
3459 3459 text
3460 3460 $ hg log --config extensions.color=! -l 1 --template '{label(red, "text\n")}'
3461 3461 text
3462 3462
3463 3463 Test branches inside if statement:
3464 3464
3465 3465 $ hg log -r 0 --template '{if(branches, "yes", "no")}\n'
3466 3466 no
3467 3467
3468 3468 Test dict constructor:
3469 3469
3470 3470 $ hg log -r 0 -T '{dict(y=node|short, x=rev)}\n'
3471 3471 y=f7769ec2ab97 x=0
3472 3472 $ hg log -r 0 -T '{dict(x=rev, y=node|short) % "{key}={value}\n"}'
3473 3473 x=0
3474 3474 y=f7769ec2ab97
3475 3475 $ hg log -r 0 -T '{dict(x=rev, y=node|short)|json}\n'
3476 3476 {"x": 0, "y": "f7769ec2ab97"}
3477 3477 $ hg log -r 0 -T '{dict()|json}\n'
3478 3478 {}
3479 3479
3480 3480 $ hg log -r 0 -T '{dict(rev, node=node|short)}\n'
3481 3481 rev=0 node=f7769ec2ab97
3482 3482 $ hg log -r 0 -T '{dict(rev, node|short)}\n'
3483 3483 rev=0 node=f7769ec2ab97
3484 3484
3485 3485 $ hg log -r 0 -T '{dict(rev, rev=rev)}\n'
3486 3486 hg: parse error: duplicated dict key 'rev' inferred
3487 3487 [255]
3488 3488 $ hg log -r 0 -T '{dict(node, node|short)}\n'
3489 3489 hg: parse error: duplicated dict key 'node' inferred
3490 3490 [255]
3491 3491 $ hg log -r 0 -T '{dict(1 + 2)}'
3492 3492 hg: parse error: dict key cannot be inferred
3493 3493 [255]
3494 3494
3495 3495 $ hg log -r 0 -T '{dict(x=rev, x=node)}'
3496 3496 hg: parse error: dict got multiple values for keyword argument 'x'
3497 3497 [255]
3498 3498
3499 3499 Test get function:
3500 3500
3501 3501 $ hg log -r 0 --template '{get(extras, "branch")}\n'
3502 3502 default
3503 3503 $ hg log -r 0 --template '{get(extras, "br{"anch"}")}\n'
3504 3504 default
3505 3505 $ hg log -r 0 --template '{get(files, "should_fail")}\n'
3506 3506 hg: parse error: get() expects a dict as first argument
3507 3507 [255]
3508 3508
3509 3509 Test json filter applied to hybrid object:
3510 3510
3511 3511 $ hg log -r0 -T '{files|json}\n'
3512 3512 ["a"]
3513 3513 $ hg log -r0 -T '{extras|json}\n'
3514 3514 {"branch": "default"}
3515 3515
3516 3516 Test localdate(date, tz) function:
3517 3517
3518 3518 $ TZ=JST-09 hg log -r0 -T '{date|localdate|isodate}\n'
3519 3519 1970-01-01 09:00 +0900
3520 3520 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "UTC")|isodate}\n'
3521 3521 1970-01-01 00:00 +0000
3522 3522 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "blahUTC")|isodate}\n'
3523 3523 hg: parse error: localdate expects a timezone
3524 3524 [255]
3525 3525 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "+0200")|isodate}\n'
3526 3526 1970-01-01 02:00 +0200
3527 3527 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "0")|isodate}\n'
3528 3528 1970-01-01 00:00 +0000
3529 3529 $ TZ=JST-09 hg log -r0 -T '{localdate(date, 0)|isodate}\n'
3530 3530 1970-01-01 00:00 +0000
3531 3531 $ hg log -r0 -T '{localdate(date, "invalid")|isodate}\n'
3532 3532 hg: parse error: localdate expects a timezone
3533 3533 [255]
3534 3534 $ hg log -r0 -T '{localdate(date, date)|isodate}\n'
3535 3535 hg: parse error: localdate expects a timezone
3536 3536 [255]
3537 3537
3538 3538 Test shortest(node) function:
3539 3539
3540 3540 $ echo b > b
3541 3541 $ hg ci -qAm b
3542 3542 $ hg log --template '{shortest(node)}\n'
3543 3543 e777
3544 3544 bcc7
3545 3545 f776
3546 3546 $ hg log --template '{shortest(node, 10)}\n'
3547 3547 e777603221
3548 3548 bcc7ff960b
3549 3549 f7769ec2ab
3550 3550 $ hg log --template '{node|shortest}\n' -l1
3551 3551 e777
3552 3552
3553 3553 $ hg log -r 0 -T '{shortest(node, "1{"0"}")}\n'
3554 3554 f7769ec2ab
3555 3555 $ hg log -r 0 -T '{shortest(node, "not an int")}\n'
3556 3556 hg: parse error: shortest() expects an integer minlength
3557 3557 [255]
3558 3558
3559 3559 $ hg log -r 'wdir()' -T '{node|shortest}\n'
3560 3560 ffff
3561 3561
3562 3562 $ cd ..
3563 3563
3564 3564 Test shortest(node) with the repo having short hash collision:
3565 3565
3566 3566 $ hg init hashcollision
3567 3567 $ cd hashcollision
3568 3568 $ cat <<EOF >> .hg/hgrc
3569 3569 > [experimental]
3570 3570 > stabilization = createmarkers
3571 3571 > EOF
3572 3572 $ echo 0 > a
3573 3573 $ hg ci -qAm 0
3574 3574 $ for i in 17 129 248 242 480 580 617 1057 2857 4025; do
3575 3575 > hg up -q 0
3576 3576 > echo $i > a
3577 3577 > hg ci -qm $i
3578 3578 > done
3579 3579 $ hg up -q null
3580 3580 $ hg log -r0: -T '{rev}:{node}\n'
3581 3581 0:b4e73ffab476aa0ee32ed81ca51e07169844bc6a
3582 3582 1:11424df6dc1dd4ea255eae2b58eaca7831973bbc
3583 3583 2:11407b3f1b9c3e76a79c1ec5373924df096f0499
3584 3584 3:11dd92fe0f39dfdaacdaa5f3997edc533875cfc4
3585 3585 4:10776689e627b465361ad5c296a20a487e153ca4
3586 3586 5:a00be79088084cb3aff086ab799f8790e01a976b
3587 3587 6:a0b0acd79b4498d0052993d35a6a748dd51d13e6
3588 3588 7:a0457b3450b8e1b778f1163b31a435802987fe5d
3589 3589 8:c56256a09cd28e5764f32e8e2810d0f01e2e357a
3590 3590 9:c5623987d205cd6d9d8389bfc40fff9dbb670b48
3591 3591 10:c562ddd9c94164376c20b86b0b4991636a3bf84f
3592 3592 $ hg debugobsolete a00be79088084cb3aff086ab799f8790e01a976b
3593 3593 obsoleted 1 changesets
3594 3594 $ hg debugobsolete c5623987d205cd6d9d8389bfc40fff9dbb670b48
3595 3595 obsoleted 1 changesets
3596 3596 $ hg debugobsolete c562ddd9c94164376c20b86b0b4991636a3bf84f
3597 3597 obsoleted 1 changesets
3598 3598
3599 3599 nodes starting with '11' (we don't have the revision number '11' though)
3600 3600
3601 3601 $ hg log -r 1:3 -T '{rev}:{shortest(node, 0)}\n'
3602 3602 1:1142
3603 3603 2:1140
3604 3604 3:11d
3605 3605
3606 3606 '5:a00' is hidden, but still we have two nodes starting with 'a0'
3607 3607
3608 3608 $ hg log -r 6:7 -T '{rev}:{shortest(node, 0)}\n'
3609 3609 6:a0b
3610 3610 7:a04
3611 3611
3612 3612 node '10' conflicts with the revision number '10' even if it is hidden
3613 3613 (we could exclude hidden revision numbers, but currently we don't)
3614 3614
3615 3615 $ hg log -r 4 -T '{rev}:{shortest(node, 0)}\n'
3616 3616 4:107
3617 3617 $ hg log -r 4 -T '{rev}:{shortest(node, 0)}\n' --hidden
3618 3618 4:107
3619 3619
3620 3620 node 'c562' should be unique if the other 'c562' nodes are hidden
3621 3621 (but we don't try the slow path to filter out hidden nodes for now)
3622 3622
3623 3623 $ hg log -r 8 -T '{rev}:{node|shortest}\n'
3624 3624 8:c5625
3625 3625 $ hg log -r 8:10 -T '{rev}:{node|shortest}\n' --hidden
3626 3626 8:c5625
3627 3627 9:c5623
3628 3628 10:c562d
3629 3629
3630 3630 $ cd ..
3631 3631
3632 3632 Test pad function
3633 3633
3634 3634 $ cd r
3635 3635
3636 3636 $ hg log --template '{pad(rev, 20)} {author|user}\n'
3637 3637 2 test
3638 3638 1 {node|short}
3639 3639 0 test
3640 3640
3641 3641 $ hg log --template '{pad(rev, 20, " ", True)} {author|user}\n'
3642 3642 2 test
3643 3643 1 {node|short}
3644 3644 0 test
3645 3645
3646 3646 $ hg log --template '{pad(rev, 20, "-", False)} {author|user}\n'
3647 3647 2------------------- test
3648 3648 1------------------- {node|short}
3649 3649 0------------------- test
3650 3650
3651 3651 Test template string in pad function
3652 3652
3653 3653 $ hg log -r 0 -T '{pad("\{{rev}}", 10)} {author|user}\n'
3654 3654 {0} test
3655 3655
3656 3656 $ hg log -r 0 -T '{pad(r"\{rev}", 10)} {author|user}\n'
3657 3657 \{rev} test
3658 3658
3659 3659 Test width argument passed to pad function
3660 3660
3661 3661 $ hg log -r 0 -T '{pad(rev, "1{"0"}")} {author|user}\n'
3662 3662 0 test
3663 3663 $ hg log -r 0 -T '{pad(rev, "not an int")}\n'
3664 3664 hg: parse error: pad() expects an integer width
3665 3665 [255]
3666 3666
3667 3667 Test invalid fillchar passed to pad function
3668 3668
3669 3669 $ hg log -r 0 -T '{pad(rev, 10, "")}\n'
3670 3670 hg: parse error: pad() expects a single fill character
3671 3671 [255]
3672 3672 $ hg log -r 0 -T '{pad(rev, 10, "--")}\n'
3673 3673 hg: parse error: pad() expects a single fill character
3674 3674 [255]
3675 3675
3676 3676 Test boolean argument passed to pad function
3677 3677
3678 3678 no crash
3679 3679
3680 3680 $ hg log -r 0 -T '{pad(rev, 10, "-", "f{"oo"}")}\n'
3681 3681 ---------0
3682 3682
3683 3683 string/literal
3684 3684
3685 3685 $ hg log -r 0 -T '{pad(rev, 10, "-", "false")}\n'
3686 3686 ---------0
3687 3687 $ hg log -r 0 -T '{pad(rev, 10, "-", false)}\n'
3688 3688 0---------
3689 3689 $ hg log -r 0 -T '{pad(rev, 10, "-", "")}\n'
3690 3690 0---------
3691 3691
3692 3692 unknown keyword is evaluated to ''
3693 3693
3694 3694 $ hg log -r 0 -T '{pad(rev, 10, "-", unknownkeyword)}\n'
3695 3695 0---------
3696 3696
3697 3697 Test separate function
3698 3698
3699 3699 $ hg log -r 0 -T '{separate("-", "", "a", "b", "", "", "c", "")}\n'
3700 3700 a-b-c
3701 3701 $ hg log -r 0 -T '{separate(" ", "{rev}:{node|short}", author|user, branch)}\n'
3702 3702 0:f7769ec2ab97 test default
3703 3703 $ hg log -r 0 --color=always -T '{separate(" ", "a", label(red, "b"), "c", label(red, ""), "d")}\n'
3704 3704 a \x1b[0;31mb\x1b[0m c d (esc)
3705 3705
3706 3706 Test boolean expression/literal passed to if function
3707 3707
3708 3708 $ hg log -r 0 -T '{if(rev, "rev 0 is True")}\n'
3709 3709 rev 0 is True
3710 3710 $ hg log -r 0 -T '{if(0, "literal 0 is True as well")}\n'
3711 3711 literal 0 is True as well
3712 3712 $ hg log -r 0 -T '{if("", "", "empty string is False")}\n'
3713 3713 empty string is False
3714 3714 $ hg log -r 0 -T '{if(revset(r"0 - 0"), "", "empty list is False")}\n'
3715 3715 empty list is False
3716 3716 $ hg log -r 0 -T '{if(true, "true is True")}\n'
3717 3717 true is True
3718 3718 $ hg log -r 0 -T '{if(false, "", "false is False")}\n'
3719 3719 false is False
3720 3720 $ hg log -r 0 -T '{if("false", "non-empty string is True")}\n'
3721 3721 non-empty string is True
3722 3722
3723 3723 Test ifcontains function
3724 3724
3725 3725 $ hg log --template '{rev} {ifcontains(rev, "2 two 0", "is in the string", "is not")}\n'
3726 3726 2 is in the string
3727 3727 1 is not
3728 3728 0 is in the string
3729 3729
3730 3730 $ hg log -T '{rev} {ifcontains(rev, "2 two{" 0"}", "is in the string", "is not")}\n'
3731 3731 2 is in the string
3732 3732 1 is not
3733 3733 0 is in the string
3734 3734
3735 3735 $ hg log --template '{rev} {ifcontains("a", file_adds, "added a", "did not add a")}\n'
3736 3736 2 did not add a
3737 3737 1 did not add a
3738 3738 0 added a
3739 3739
3740 3740 $ hg log --debug -T '{rev}{ifcontains(1, parents, " is parent of 1")}\n'
3741 3741 2 is parent of 1
3742 3742 1
3743 3743 0
3744 3744
3745 3745 Test revset function
3746 3746
3747 3747 $ hg log --template '{rev} {ifcontains(rev, revset("."), "current rev", "not current rev")}\n'
3748 3748 2 current rev
3749 3749 1 not current rev
3750 3750 0 not current rev
3751 3751
3752 3752 $ hg log --template '{rev} {ifcontains(rev, revset(". + .^"), "match rev", "not match rev")}\n'
3753 3753 2 match rev
3754 3754 1 match rev
3755 3755 0 not match rev
3756 3756
3757 3757 $ hg log --template '{rev} Parents: {revset("parents(%s)", rev)}\n'
3758 3758 2 Parents: 1
3759 3759 1 Parents: 0
3760 3760 0 Parents:
3761 3761
3762 3762 $ cat >> .hg/hgrc <<EOF
3763 3763 > [revsetalias]
3764 3764 > myparents(\$1) = parents(\$1)
3765 3765 > EOF
3766 3766 $ hg log --template '{rev} Parents: {revset("myparents(%s)", rev)}\n'
3767 3767 2 Parents: 1
3768 3768 1 Parents: 0
3769 3769 0 Parents:
3770 3770
3771 3771 $ hg log --template 'Rev: {rev}\n{revset("::%s", rev) % "Ancestor: {revision}\n"}\n'
3772 3772 Rev: 2
3773 3773 Ancestor: 0
3774 3774 Ancestor: 1
3775 3775 Ancestor: 2
3776 3776
3777 3777 Rev: 1
3778 3778 Ancestor: 0
3779 3779 Ancestor: 1
3780 3780
3781 3781 Rev: 0
3782 3782 Ancestor: 0
3783 3783
3784 3784 $ hg log --template '{revset("TIP"|lower)}\n' -l1
3785 3785 2
3786 3786
3787 3787 $ hg log -T '{revset("%s", "t{"ip"}")}\n' -l1
3788 3788 2
3789 3789
3790 3790 a list template is evaluated for each item of revset/parents
3791 3791
3792 3792 $ hg log -T '{rev} p: {revset("p1(%s)", rev) % "{rev}:{node|short}"}\n'
3793 3793 2 p: 1:bcc7ff960b8e
3794 3794 1 p: 0:f7769ec2ab97
3795 3795 0 p:
3796 3796
3797 3797 $ hg log --debug -T '{rev} p:{parents % " {rev}:{node|short}"}\n'
3798 3798 2 p: 1:bcc7ff960b8e -1:000000000000
3799 3799 1 p: 0:f7769ec2ab97 -1:000000000000
3800 3800 0 p: -1:000000000000 -1:000000000000
3801 3801
3802 3802 therefore, 'revcache' should be recreated for each rev
3803 3803
3804 3804 $ hg log -T '{rev} {file_adds}\np {revset("p1(%s)", rev) % "{file_adds}"}\n'
3805 3805 2 aa b
3806 3806 p
3807 3807 1
3808 3808 p a
3809 3809 0 a
3810 3810 p
3811 3811
3812 3812 $ hg log --debug -T '{rev} {file_adds}\np {parents % "{file_adds}"}\n'
3813 3813 2 aa b
3814 3814 p
3815 3815 1
3816 3816 p a
3817 3817 0 a
3818 3818 p
3819 3819
3820 3820 a revset item must be evaluated as an integer revision, not an offset from tip
3821 3821
3822 3822 $ hg log -l 1 -T '{revset("null") % "{rev}:{node|short}"}\n'
3823 3823 -1:000000000000
3824 3824 $ hg log -l 1 -T '{revset("%s", "null") % "{rev}:{node|short}"}\n'
3825 3825 -1:000000000000
3826 3826
3827 3827 join() should pick '{rev}' from revset items:
3828 3828
3829 3829 $ hg log -R ../a -T '{join(revset("parents(%d)", rev), ", ")}\n' -r6
3830 3830 4, 5
3831 3831
3832 3832 on the other hand, parents are formatted as '{rev}:{node|formatnode}' by
3833 3833 default. join() should agree with the default formatting:
3834 3834
3835 3835 $ hg log -R ../a -T '{join(parents, ", ")}\n' -r6
3836 3836 5:13207e5a10d9, 4:bbe44766e73d
3837 3837
3838 3838 $ hg log -R ../a -T '{join(parents, ",\n")}\n' -r6 --debug
3839 3839 5:13207e5a10d9fd28ec424934298e176197f2c67f,
3840 3840 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
3841 3841
3842 3842 Test files function
3843 3843
3844 3844 $ hg log -T "{rev}\n{join(files('*'), '\n')}\n"
3845 3845 2
3846 3846 a
3847 3847 aa
3848 3848 b
3849 3849 1
3850 3850 a
3851 3851 0
3852 3852 a
3853 3853
3854 3854 $ hg log -T "{rev}\n{join(files('aa'), '\n')}\n"
3855 3855 2
3856 3856 aa
3857 3857 1
3858 3858
3859 3859 0
3860 3860
3861 3861
3862 3862 Test relpath function
3863 3863
3864 3864 $ hg log -r0 -T '{files % "{file|relpath}\n"}'
3865 3865 a
3866 3866 $ cd ..
3867 3867 $ hg log -R r -r0 -T '{files % "{file|relpath}\n"}'
3868 3868 r/a
3869 3869 $ cd r
3870 3870
3871 3871 Test active bookmark templating
3872 3872
3873 3873 $ hg book foo
3874 3874 $ hg book bar
3875 3875 $ hg log --template "{rev} {bookmarks % '{bookmark}{ifeq(bookmark, active, \"*\")} '}\n"
3876 3876 2 bar* foo
3877 3877 1
3878 3878 0
3879 3879 $ hg log --template "{rev} {activebookmark}\n"
3880 3880 2 bar
3881 3881 1
3882 3882 0
3883 3883 $ hg bookmarks --inactive bar
3884 3884 $ hg log --template "{rev} {activebookmark}\n"
3885 3885 2
3886 3886 1
3887 3887 0
3888 3888 $ hg book -r1 baz
3889 3889 $ hg log --template "{rev} {join(bookmarks, ' ')}\n"
3890 3890 2 bar foo
3891 3891 1 baz
3892 3892 0
3893 3893 $ hg log --template "{rev} {ifcontains('foo', bookmarks, 't', 'f')}\n"
3894 3894 2 t
3895 3895 1 f
3896 3896 0 f
3897 3897
3898 3898 Test namespaces dict
3899 3899
3900 3900 $ hg --config extensions.revnamesext=$TESTDIR/revnamesext.py log -T '{rev}\n{namespaces % " {namespace} color={colorname} builtin={builtin}\n {join(names, ",")}\n"}\n'
3901 3901 2
3902 3902 bookmarks color=bookmark builtin=True
3903 3903 bar,foo
3904 3904 tags color=tag builtin=True
3905 3905 tip
3906 3906 branches color=branch builtin=True
3907 3907 text.{rev}
3908 3908 revnames color=revname builtin=False
3909 3909 r2
3910 3910
3911 3911 1
3912 3912 bookmarks color=bookmark builtin=True
3913 3913 baz
3914 3914 tags color=tag builtin=True
3915 3915
3916 3916 branches color=branch builtin=True
3917 3917 text.{rev}
3918 3918 revnames color=revname builtin=False
3919 3919 r1
3920 3920
3921 3921 0
3922 3922 bookmarks color=bookmark builtin=True
3923 3923
3924 3924 tags color=tag builtin=True
3925 3925
3926 3926 branches color=branch builtin=True
3927 3927 default
3928 3928 revnames color=revname builtin=False
3929 3929 r0
3930 3930
3931 3931 $ hg log -r2 -T '{namespaces % "{namespace}: {names}\n"}'
3932 3932 bookmarks: bar foo
3933 3933 tags: tip
3934 3934 branches: text.{rev}
3935 3935 $ hg log -r2 -T '{namespaces % "{namespace}:\n{names % " {name}\n"}"}'
3936 3936 bookmarks:
3937 3937 bar
3938 3938 foo
3939 3939 tags:
3940 3940 tip
3941 3941 branches:
3942 3942 text.{rev}
3943 3943 $ hg log -r2 -T '{get(namespaces, "bookmarks") % "{name}\n"}'
3944 3944 bar
3945 3945 foo
3946 3946
3947 3947 Test stringify on sub expressions
3948 3948
3949 3949 $ cd ..
3950 3950 $ hg log -R a -r 8 --template '{join(files, if("1", if("1", ", ")))}\n'
3951 3951 fourth, second, third
3952 3952 $ hg log -R a -r 8 --template '{strip(if("1", if("1", "-abc-")), if("1", if("1", "-")))}\n'
3953 3953 abc
3954 3954
3955 3955 Test splitlines
3956 3956
3957 3957 $ hg log -Gv -R a --template "{splitlines(desc) % 'foo {line}\n'}"
3958 3958 @ foo Modify, add, remove, rename
3959 3959 |
3960 3960 o foo future
3961 3961 |
3962 3962 o foo third
3963 3963 |
3964 3964 o foo second
3965 3965
3966 3966 o foo merge
3967 3967 |\
3968 3968 | o foo new head
3969 3969 | |
3970 3970 o | foo new branch
3971 3971 |/
3972 3972 o foo no user, no domain
3973 3973 |
3974 3974 o foo no person
3975 3975 |
3976 3976 o foo other 1
3977 3977 | foo other 2
3978 3978 | foo
3979 3979 | foo other 3
3980 3980 o foo line 1
3981 3981 foo line 2
3982 3982
3983 3983 $ hg log -R a -r0 -T '{desc|splitlines}\n'
3984 3984 line 1 line 2
3985 3985 $ hg log -R a -r0 -T '{join(desc|splitlines, "|")}\n'
3986 3986 line 1|line 2
3987 3987
3988 3988 Test startswith
3989 3989 $ hg log -Gv -R a --template "{startswith(desc)}"
3990 3990 hg: parse error: startswith expects two arguments
3991 3991 [255]
3992 3992
3993 3993 $ hg log -Gv -R a --template "{startswith('line', desc)}"
3994 3994 @
3995 3995 |
3996 3996 o
3997 3997 |
3998 3998 o
3999 3999 |
4000 4000 o
4001 4001
4002 4002 o
4003 4003 |\
4004 4004 | o
4005 4005 | |
4006 4006 o |
4007 4007 |/
4008 4008 o
4009 4009 |
4010 4010 o
4011 4011 |
4012 4012 o
4013 4013 |
4014 4014 o line 1
4015 4015 line 2
4016 4016
4017 4017 Test bad template with better error message
4018 4018
4019 4019 $ hg log -Gv -R a --template '{desc|user()}'
4020 4020 hg: parse error: expected a symbol, got 'func'
4021 4021 [255]
4022 4022
4023 4023 Test word function (including index out of bounds graceful failure)
4024 4024
4025 4025 $ hg log -Gv -R a --template "{word('1', desc)}"
4026 4026 @ add,
4027 4027 |
4028 4028 o
4029 4029 |
4030 4030 o
4031 4031 |
4032 4032 o
4033 4033
4034 4034 o
4035 4035 |\
4036 4036 | o head
4037 4037 | |
4038 4038 o | branch
4039 4039 |/
4040 4040 o user,
4041 4041 |
4042 4042 o person
4043 4043 |
4044 4044 o 1
4045 4045 |
4046 4046 o 1
4047 4047
4048 4048
4049 4049 Test word third parameter used as splitter
4050 4050
4051 4051 $ hg log -Gv -R a --template "{word('0', desc, 'o')}"
4052 4052 @ M
4053 4053 |
4054 4054 o future
4055 4055 |
4056 4056 o third
4057 4057 |
4058 4058 o sec
4059 4059
4060 4060 o merge
4061 4061 |\
4062 4062 | o new head
4063 4063 | |
4064 4064 o | new branch
4065 4065 |/
4066 4066 o n
4067 4067 |
4068 4068 o n
4069 4069 |
4070 4070 o
4071 4071 |
4072 4072 o line 1
4073 4073 line 2
4074 4074
4075 4075 Test word error messages for not enough and too many arguments
4076 4076
4077 4077 $ hg log -Gv -R a --template "{word('0')}"
4078 4078 hg: parse error: word expects two or three arguments, got 1
4079 4079 [255]
4080 4080
4081 4081 $ hg log -Gv -R a --template "{word('0', desc, 'o', 'h', 'b', 'o', 'y')}"
4082 4082 hg: parse error: word expects two or three arguments, got 7
4083 4083 [255]
4084 4084
4085 4085 Test word for integer literal
4086 4086
4087 4087 $ hg log -R a --template "{word(2, desc)}\n" -r0
4088 4088 line
4089 4089
4090 4090 Test word for invalid numbers
4091 4091
4092 4092 $ hg log -Gv -R a --template "{word('a', desc)}"
4093 4093 hg: parse error: word expects an integer index
4094 4094 [255]
4095 4095
4096 4096 Test word for out of range
4097 4097
4098 4098 $ hg log -R a --template "{word(10000, desc)}"
4099 4099 $ hg log -R a --template "{word(-10000, desc)}"
4100 4100
4101 4101 Test indent and not adding to empty lines
4102 4102
4103 4103 $ hg log -T "-----\n{indent(desc, '>> ', ' > ')}\n" -r 0:1 -R a
4104 4104 -----
4105 4105 > line 1
4106 4106 >> line 2
4107 4107 -----
4108 4108 > other 1
4109 4109 >> other 2
4110 4110
4111 4111 >> other 3
4112 4112
4113 4113 Test with non-strings like dates
4114 4114
4115 4115 $ hg log -T "{indent(date, ' ')}\n" -r 2:3 -R a
4116 4116 1200000.00
4117 4117 1300000.00
4118 4118
4119 4119 Test broken string escapes:
4120 4120
4121 4121 $ hg log -T "bogus\\" -R a
4122 4122 hg: parse error: trailing \ in string
4123 4123 [255]
4124 4124 $ hg log -T "\\xy" -R a
4125 4125 hg: parse error: invalid \x escape
4126 4126 [255]
4127 4127
4128 4128 json filter should escape HTML tags so that the output can be embedded in hgweb:
4129 4129
4130 4130 $ hg log -T "{'<foo@example.org>'|json}\n" -R a -l1
4131 4131 "\u003cfoo@example.org\u003e"
4132 4132
4133 4133 Templater supports aliases of symbol and func() styles:
4134 4134
4135 4135 $ hg clone -q a aliases
4136 4136 $ cd aliases
4137 4137 $ cat <<EOF >> .hg/hgrc
4138 4138 > [templatealias]
4139 4139 > r = rev
4140 4140 > rn = "{r}:{node|short}"
4141 4141 > status(c, files) = files % "{c} {file}\n"
4142 4142 > utcdate(d) = localdate(d, "UTC")
4143 4143 > EOF
4144 4144
4145 4145 $ hg debugtemplate -vr0 '{rn} {utcdate(date)|isodate}\n'
4146 4146 (template
4147 4147 ('symbol', 'rn')
4148 4148 ('string', ' ')
4149 4149 (|
4150 4150 (func
4151 4151 ('symbol', 'utcdate')
4152 4152 ('symbol', 'date'))
4153 4153 ('symbol', 'isodate'))
4154 4154 ('string', '\n'))
4155 4155 * expanded:
4156 4156 (template
4157 4157 (template
4158 4158 ('symbol', 'rev')
4159 4159 ('string', ':')
4160 4160 (|
4161 4161 ('symbol', 'node')
4162 4162 ('symbol', 'short')))
4163 4163 ('string', ' ')
4164 4164 (|
4165 4165 (func
4166 4166 ('symbol', 'localdate')
4167 4167 (list
4168 4168 ('symbol', 'date')
4169 4169 ('string', 'UTC')))
4170 4170 ('symbol', 'isodate'))
4171 4171 ('string', '\n'))
4172 4172 0:1e4e1b8f71e0 1970-01-12 13:46 +0000
4173 4173
4174 4174 $ hg debugtemplate -vr0 '{status("A", file_adds)}'
4175 4175 (template
4176 4176 (func
4177 4177 ('symbol', 'status')
4178 4178 (list
4179 4179 ('string', 'A')
4180 4180 ('symbol', 'file_adds'))))
4181 4181 * expanded:
4182 4182 (template
4183 4183 (%
4184 4184 ('symbol', 'file_adds')
4185 4185 (template
4186 4186 ('string', 'A')
4187 4187 ('string', ' ')
4188 4188 ('symbol', 'file')
4189 4189 ('string', '\n'))))
4190 4190 A a
4191 4191
4192 4192 A unary function alias can be called as a filter:
4193 4193
4194 4194 $ hg debugtemplate -vr0 '{date|utcdate|isodate}\n'
4195 4195 (template
4196 4196 (|
4197 4197 (|
4198 4198 ('symbol', 'date')
4199 4199 ('symbol', 'utcdate'))
4200 4200 ('symbol', 'isodate'))
4201 4201 ('string', '\n'))
4202 4202 * expanded:
4203 4203 (template
4204 4204 (|
4205 4205 (func
4206 4206 ('symbol', 'localdate')
4207 4207 (list
4208 4208 ('symbol', 'date')
4209 4209 ('string', 'UTC')))
4210 4210 ('symbol', 'isodate'))
4211 4211 ('string', '\n'))
4212 4212 1970-01-12 13:46 +0000
4213 4213
4214 4214 Aliases should be applied only to command arguments and templates in hgrc.
4215 4215 Otherwise, our stock styles and web templates could be corrupted:
4216 4216
4217 4217 $ hg log -r0 -T '{rn} {utcdate(date)|isodate}\n'
4218 4218 0:1e4e1b8f71e0 1970-01-12 13:46 +0000
4219 4219
4220 4220 $ hg log -r0 --config ui.logtemplate='"{rn} {utcdate(date)|isodate}\n"'
4221 4221 0:1e4e1b8f71e0 1970-01-12 13:46 +0000
4222 4222
4223 4223 $ cat <<EOF > tmpl
4224 4224 > changeset = 'nothing expanded:{rn}\n'
4225 4225 > EOF
4226 4226 $ hg log -r0 --style ./tmpl
4227 4227 nothing expanded:
4228 4228
4229 4229 Aliases in formatter:
4230 4230
4231 4231 $ hg branches -T '{pad(branch, 7)} {rn}\n'
4232 4232 default 6:d41e714fe50d
4233 4233 foo 4:bbe44766e73d
4234 4234
4235 4235 Aliases should honor HGPLAIN:
4236 4236
4237 4237 $ HGPLAIN= hg log -r0 -T 'nothing expanded:{rn}\n'
4238 4238 nothing expanded:
4239 4239 $ HGPLAINEXCEPT=templatealias hg log -r0 -T '{rn}\n'
4240 4240 0:1e4e1b8f71e0
4241 4241
4242 4242 Unparsable alias:
4243 4243
4244 4244 $ hg debugtemplate --config templatealias.bad='x(' -v '{bad}'
4245 4245 (template
4246 4246 ('symbol', 'bad'))
4247 4247 abort: bad definition of template alias "bad": at 2: not a prefix: end
4248 4248 [255]
4249 4249 $ hg log --config templatealias.bad='x(' -T '{bad}'
4250 4250 abort: bad definition of template alias "bad": at 2: not a prefix: end
4251 4251 [255]
4252 4252
4253 4253 $ cd ..
4254 4254
4255 4255 Set up repository for non-ascii encoding tests:
4256 4256
4257 4257 $ hg init nonascii
4258 4258 $ cd nonascii
4259 4259 $ $PYTHON <<EOF
4260 4260 > open('latin1', 'w').write('\xe9')
4261 4261 > open('utf-8', 'w').write('\xc3\xa9')
4262 4262 > EOF
4263 4263 $ HGENCODING=utf-8 hg branch -q `cat utf-8`
4264 4264 $ HGENCODING=utf-8 hg ci -qAm "non-ascii branch: `cat utf-8`" utf-8
4265 4265
4266 4266 json filter should try round-trip conversion to utf-8:
4267 4267
4268 4268 $ HGENCODING=ascii hg log -T "{branch|json}\n" -r0
4269 4269 "\u00e9"
4270 4270 $ HGENCODING=ascii hg log -T "{desc|json}\n" -r0
4271 4271 "non-ascii branch: \u00e9"
4272 4272
4273 4273 json filter takes input as utf-8b:
4274 4274
4275 4275 $ HGENCODING=ascii hg log -T "{'`cat utf-8`'|json}\n" -l1
4276 4276 "\u00e9"
4277 4277 $ HGENCODING=ascii hg log -T "{'`cat latin1`'|json}\n" -l1
4278 4278 "\udce9"
4279 4279
4280 4280 utf8 filter:
4281 4281
4282 4282 $ HGENCODING=ascii hg log -T "round-trip: {branch|utf8|hex}\n" -r0
4283 4283 round-trip: c3a9
4284 4284 $ HGENCODING=latin1 hg log -T "decoded: {'`cat latin1`'|utf8|hex}\n" -l1
4285 4285 decoded: c3a9
4286 4286 $ HGENCODING=ascii hg log -T "replaced: {'`cat latin1`'|utf8|hex}\n" -l1
4287 4287 abort: decoding near * (glob)
4288 4288 [255]
4289 4289 $ hg log -T "invalid type: {rev|utf8}\n" -r0
4290 4290 abort: template filter 'utf8' is not compatible with keyword 'rev'
4291 4291 [255]
4292 4292
4293 4293 pad width:
4294 4294
4295 4295 $ HGENCODING=utf-8 hg debugtemplate "{pad('`cat utf-8`', 2, '-')}\n"
4296 4296 \xc3\xa9- (esc)
4297 4297
4298 4298 $ cd ..
4299 4299
4300 4300 Test that template function in extension is registered as expected
4301 4301
4302 4302 $ cd a
4303 4303
4304 4304 $ cat <<EOF > $TESTTMP/customfunc.py
4305 4305 > from mercurial import registrar
4306 4306 >
4307 4307 > templatefunc = registrar.templatefunc()
4308 4308 >
4309 4309 > @templatefunc('custom()')
4310 4310 > def custom(context, mapping, args):
4311 4311 > return 'custom'
4312 4312 > EOF
4313 4313 $ cat <<EOF > .hg/hgrc
4314 4314 > [extensions]
4315 4315 > customfunc = $TESTTMP/customfunc.py
4316 4316 > EOF
4317 4317
4318 4318 $ hg log -r . -T "{custom()}\n" --config customfunc.enabled=true
4319 4319 custom
4320 4320
4321 4321 $ cd ..
4322
4323 Test 'graphwidth' in 'hg log' on various topologies. The key here is that the
4324 printed graphwidths 3, 5, 7, etc. should all line up in their respective
4325 columns. We don't care about other aspects of the graph rendering here.
4326
4327 $ hg init graphwidth
4328 $ cd graphwidth
4329
4330 $ wrappabletext="a a a a a a a a a a a a"
4331
4332 $ printf "first\n" > file
4333 $ hg add file
4334 $ hg commit -m "$wrappabletext"
4335
4336 $ printf "first\nsecond\n" > file
4337 $ hg commit -m "$wrappabletext"
4338
4339 $ hg checkout 0
4340 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
4341 $ printf "third\nfirst\n" > file
4342 $ hg commit -m "$wrappabletext"
4343 created new head
4344
4345 $ hg merge
4346 merging file
4347 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
4348 (branch merge, don't forget to commit)
4349
4350 $ hg log --graph -T "{graphwidth}"
4351 @ 3
4352 |
4353 | @ 5
4354 |/
4355 o 3
4356
4357 $ hg commit -m "$wrappabletext"
4358
4359 $ hg log --graph -T "{graphwidth}"
4360 @ 5
4361 |\
4362 | o 5
4363 | |
4364 o | 5
4365 |/
4366 o 3
4367
4368
4369 $ hg checkout 0
4370 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
4371 $ printf "third\nfirst\nsecond\n" > file
4372 $ hg commit -m "$wrappabletext"
4373 created new head
4374
4375 $ hg log --graph -T "{graphwidth}"
4376 @ 3
4377 |
4378 | o 7
4379 | |\
4380 +---o 7
4381 | |
4382 | o 5
4383 |/
4384 o 3
4385
4386
4387 $ hg log --graph -T "{graphwidth}" -r 3
4388 o 5
4389 |\
4390 ~ ~
4391
4392 $ hg log --graph -T "{graphwidth}" -r 1
4393 o 3
4394 |
4395 ~
4396
4397 $ hg merge
4398 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
4399 (branch merge, don't forget to commit)
4400 $ hg commit -m "$wrappabletext"
4401
4402 $ printf "seventh\n" >> file
4403 $ hg commit -m "$wrappabletext"
4404
4405 $ hg log --graph -T "{graphwidth}"
4406 @ 3
4407 |
4408 o 5
4409 |\
4410 | o 5
4411 | |
4412 o | 7
4413 |\ \
4414 | o | 7
4415 | |/
4416 o / 5
4417 |/
4418 o 3
4419
4420
4421 The point of graphwidth is to allow wrapping that accounts for the space taken
4422 by the graph.
4423
4424 $ COLUMNS=10 hg log --graph -T "{fill(desc, termwidth - graphwidth)}"
4425 @ a a a a
4426 | a a a a
4427 | a a a a
4428 o a a a
4429 |\ a a a
4430 | | a a a
4431 | | a a a
4432 | o a a a
4433 | | a a a
4434 | | a a a
4435 | | a a a
4436 o | a a
4437 |\ \ a a
4438 | | | a a
4439 | | | a a
4440 | | | a a
4441 | | | a a
4442 | o | a a
4443 | |/ a a
4444 | | a a
4445 | | a a
4446 | | a a
4447 | | a a
4448 o | a a a
4449 |/ a a a
4450 | a a a
4451 | a a a
4452 o a a a a
4453 a a a a
4454 a a a a
4455
4456 Something tricky happens when there are elided nodes; the next drawn row of
4457 edges can be more than one column wider, but the graph width only increases by
4458 one column. The remaining columns are added in between the nodes.
4459
4460 $ hg log --graph -T "{graphwidth}" -r "0|2|4|5"
4461 o 5
4462 |\
4463 | \
4464 | :\
4465 o : : 7
4466 :/ /
4467 : o 5
4468 :/
4469 o 3
4470
4471
4472 $ cd ..
4473
General Comments 0
You need to be logged in to leave comments. Login now