##// END OF EJS Templates
logcmdutil: use the same data as {file*} template keywords (issue6642)...
av6 -
r50210:2f326ea1 stable
parent child Browse files
Show More
@@ -1,1287 +1,1285
1 1 # logcmdutil.py - utility for log-like commands
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@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 itertools
11 11 import os
12 12 import posixpath
13 13
14 14 from .i18n import _
15 15 from .node import nullrev, wdirrev
16 16
17 17 from .thirdparty import attr
18 18
19 19 from . import (
20 20 dagop,
21 21 error,
22 22 formatter,
23 23 graphmod,
24 24 match as matchmod,
25 25 mdiff,
26 26 merge,
27 27 patch,
28 28 pathutil,
29 29 pycompat,
30 30 revset,
31 31 revsetlang,
32 32 scmutil,
33 33 smartset,
34 34 templatekw,
35 35 templater,
36 36 util,
37 37 )
38 38 from .utils import (
39 39 dateutil,
40 40 stringutil,
41 41 )
42 42
43 43
44 44 if pycompat.TYPE_CHECKING:
45 45 from typing import (
46 46 Any,
47 47 Callable,
48 48 Dict,
49 49 Optional,
50 50 Sequence,
51 51 Tuple,
52 52 )
53 53
54 54 for t in (Any, Callable, Dict, Optional, Tuple):
55 55 assert t
56 56
57 57
58 58 def getlimit(opts):
59 59 """get the log limit according to option -l/--limit"""
60 60 limit = opts.get(b'limit')
61 61 if limit:
62 62 try:
63 63 limit = int(limit)
64 64 except ValueError:
65 65 raise error.InputError(_(b'limit must be a positive integer'))
66 66 if limit <= 0:
67 67 raise error.InputError(_(b'limit must be positive'))
68 68 else:
69 69 limit = None
70 70 return limit
71 71
72 72
73 73 def diff_parent(ctx):
74 74 """get the context object to use as parent when diffing
75 75
76 76
77 77 If diff.merge is enabled, an overlayworkingctx of the auto-merged parents will be returned.
78 78 """
79 79 repo = ctx.repo()
80 80 if repo.ui.configbool(b"diff", b"merge") and ctx.p2().rev() != nullrev:
81 81 # avoid cycle context -> subrepo -> cmdutil -> logcmdutil
82 82 from . import context
83 83
84 84 wctx = context.overlayworkingctx(repo)
85 85 wctx.setbase(ctx.p1())
86 86 with repo.ui.configoverride(
87 87 {
88 88 (
89 89 b"ui",
90 90 b"forcemerge",
91 91 ): b"internal:merge3-lie-about-conflicts",
92 92 },
93 93 b"merge-diff",
94 94 ):
95 95 with repo.ui.silent():
96 96 merge.merge(ctx.p2(), wc=wctx)
97 97 return wctx
98 98 else:
99 99 return ctx.p1()
100 100
101 101
102 102 def diffordiffstat(
103 103 ui,
104 104 repo,
105 105 diffopts,
106 106 ctx1,
107 107 ctx2,
108 108 match,
109 109 changes=None,
110 110 stat=False,
111 111 fp=None,
112 112 graphwidth=0,
113 113 prefix=b'',
114 114 root=b'',
115 115 listsubrepos=False,
116 116 hunksfilterfn=None,
117 117 ):
118 118 '''show diff or diffstat.'''
119 119 if root:
120 120 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
121 121 else:
122 122 relroot = b''
123 123 copysourcematch = None
124 124
125 125 def compose(f, g):
126 126 return lambda x: f(g(x))
127 127
128 128 def pathfn(f):
129 129 return posixpath.join(prefix, f)
130 130
131 131 if relroot != b'':
132 132 # XXX relative roots currently don't work if the root is within a
133 133 # subrepo
134 134 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
135 135 uirelroot = uipathfn(pathfn(relroot))
136 136 relroot += b'/'
137 137 for matchroot in match.files():
138 138 if not matchroot.startswith(relroot):
139 139 ui.warn(
140 140 _(b'warning: %s not inside relative root %s\n')
141 141 % (uipathfn(pathfn(matchroot)), uirelroot)
142 142 )
143 143
144 144 relrootmatch = scmutil.match(ctx2, pats=[relroot], default=b'path')
145 145 match = matchmod.intersectmatchers(match, relrootmatch)
146 146 copysourcematch = relrootmatch
147 147
148 148 checkroot = repo.ui.configbool(
149 149 b'devel', b'all-warnings'
150 150 ) or repo.ui.configbool(b'devel', b'check-relroot')
151 151
152 152 def relrootpathfn(f):
153 153 if checkroot and not f.startswith(relroot):
154 154 raise AssertionError(
155 155 b"file %s doesn't start with relroot %s" % (f, relroot)
156 156 )
157 157 return f[len(relroot) :]
158 158
159 159 pathfn = compose(relrootpathfn, pathfn)
160 160
161 161 if stat:
162 162 diffopts = diffopts.copy(context=0, noprefix=False)
163 163 width = 80
164 164 if not ui.plain():
165 165 width = ui.termwidth() - graphwidth
166 166 # If an explicit --root was given, don't respect ui.relative-paths
167 167 if not relroot:
168 168 pathfn = compose(scmutil.getuipathfn(repo), pathfn)
169 169
170 170 chunks = ctx2.diff(
171 171 ctx1,
172 172 match,
173 173 changes,
174 174 opts=diffopts,
175 175 pathfn=pathfn,
176 176 copysourcematch=copysourcematch,
177 177 hunksfilterfn=hunksfilterfn,
178 178 )
179 179
180 180 if fp is not None or ui.canwritewithoutlabels():
181 181 out = fp or ui
182 182 if stat:
183 183 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
184 184 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
185 185 out.write(chunk)
186 186 else:
187 187 if stat:
188 188 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
189 189 else:
190 190 chunks = patch.difflabel(
191 191 lambda chunks, **kwargs: chunks, chunks, opts=diffopts
192 192 )
193 193 if ui.canbatchlabeledwrites():
194 194
195 195 def gen():
196 196 for chunk, label in chunks:
197 197 yield ui.label(chunk, label=label)
198 198
199 199 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
200 200 ui.write(chunk)
201 201 else:
202 202 for chunk, label in chunks:
203 203 ui.write(chunk, label=label)
204 204
205 205 node2 = ctx2.node()
206 206 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
207 207 tempnode2 = node2
208 208 try:
209 209 if node2 is not None:
210 210 tempnode2 = ctx2.substate[subpath][1]
211 211 except KeyError:
212 212 # A subrepo that existed in node1 was deleted between node1 and
213 213 # node2 (inclusive). Thus, ctx2's substate won't contain that
214 214 # subpath. The best we can do is to ignore it.
215 215 tempnode2 = None
216 216 submatch = matchmod.subdirmatcher(subpath, match)
217 217 subprefix = repo.wvfs.reljoin(prefix, subpath)
218 218 if listsubrepos or match.exact(subpath) or any(submatch.files()):
219 219 sub.diff(
220 220 ui,
221 221 diffopts,
222 222 tempnode2,
223 223 submatch,
224 224 changes=changes,
225 225 stat=stat,
226 226 fp=fp,
227 227 prefix=subprefix,
228 228 )
229 229
230 230
231 231 class changesetdiffer(object):
232 232 """Generate diff of changeset with pre-configured filtering functions"""
233 233
234 234 def _makefilematcher(self, ctx):
235 235 return scmutil.matchall(ctx.repo())
236 236
237 237 def _makehunksfilter(self, ctx):
238 238 return None
239 239
240 240 def showdiff(self, ui, ctx, diffopts, graphwidth=0, stat=False):
241 241 diffordiffstat(
242 242 ui,
243 243 ctx.repo(),
244 244 diffopts,
245 245 diff_parent(ctx),
246 246 ctx,
247 247 match=self._makefilematcher(ctx),
248 248 stat=stat,
249 249 graphwidth=graphwidth,
250 250 hunksfilterfn=self._makehunksfilter(ctx),
251 251 )
252 252
253 253
254 254 def changesetlabels(ctx):
255 255 labels = [b'log.changeset', b'changeset.%s' % ctx.phasestr()]
256 256 if ctx.obsolete():
257 257 labels.append(b'changeset.obsolete')
258 258 if ctx.isunstable():
259 259 labels.append(b'changeset.unstable')
260 260 for instability in ctx.instabilities():
261 261 labels.append(b'instability.%s' % instability)
262 262 return b' '.join(labels)
263 263
264 264
265 265 class changesetprinter(object):
266 266 '''show changeset information when templating not requested.'''
267 267
268 268 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
269 269 self.ui = ui
270 270 self.repo = repo
271 271 self.buffered = buffered
272 272 self._differ = differ or changesetdiffer()
273 273 self._diffopts = patch.diffallopts(ui, diffopts)
274 274 self._includestat = diffopts and diffopts.get(b'stat')
275 275 self._includediff = diffopts and diffopts.get(b'patch')
276 276 self.header = {}
277 277 self.hunk = {}
278 278 self.lastheader = None
279 279 self.footer = None
280 280 self._columns = templatekw.getlogcolumns()
281 281
282 282 def flush(self, ctx):
283 283 rev = ctx.rev()
284 284 if rev in self.header:
285 285 h = self.header[rev]
286 286 if h != self.lastheader:
287 287 self.lastheader = h
288 288 self.ui.write(h)
289 289 del self.header[rev]
290 290 if rev in self.hunk:
291 291 self.ui.write(self.hunk[rev])
292 292 del self.hunk[rev]
293 293
294 294 def close(self):
295 295 if self.footer:
296 296 self.ui.write(self.footer)
297 297
298 298 def show(self, ctx, copies=None, **props):
299 299 props = pycompat.byteskwargs(props)
300 300 if self.buffered:
301 301 self.ui.pushbuffer(labeled=True)
302 302 self._show(ctx, copies, props)
303 303 self.hunk[ctx.rev()] = self.ui.popbuffer()
304 304 else:
305 305 self._show(ctx, copies, props)
306 306
307 307 def _show(self, ctx, copies, props):
308 308 '''show a single changeset or file revision'''
309 309 changenode = ctx.node()
310 310 graphwidth = props.get(b'graphwidth', 0)
311 311
312 312 if self.ui.quiet:
313 313 self.ui.write(
314 314 b"%s\n" % scmutil.formatchangeid(ctx), label=b'log.node'
315 315 )
316 316 return
317 317
318 318 columns = self._columns
319 319 self.ui.write(
320 320 columns[b'changeset'] % scmutil.formatchangeid(ctx),
321 321 label=changesetlabels(ctx),
322 322 )
323 323
324 324 # branches are shown first before any other names due to backwards
325 325 # compatibility
326 326 branch = ctx.branch()
327 327 # don't show the default branch name
328 328 if branch != b'default':
329 329 self.ui.write(columns[b'branch'] % branch, label=b'log.branch')
330 330
331 331 for nsname, ns in pycompat.iteritems(self.repo.names):
332 332 # branches has special logic already handled above, so here we just
333 333 # skip it
334 334 if nsname == b'branches':
335 335 continue
336 336 # we will use the templatename as the color name since those two
337 337 # should be the same
338 338 for name in ns.names(self.repo, changenode):
339 339 self.ui.write(ns.logfmt % name, label=b'log.%s' % ns.colorname)
340 340 if self.ui.debugflag:
341 341 self.ui.write(
342 342 columns[b'phase'] % ctx.phasestr(), label=b'log.phase'
343 343 )
344 344 for pctx in scmutil.meaningfulparents(self.repo, ctx):
345 345 label = b'log.parent changeset.%s' % pctx.phasestr()
346 346 self.ui.write(
347 347 columns[b'parent'] % scmutil.formatchangeid(pctx), label=label
348 348 )
349 349
350 350 if self.ui.debugflag:
351 351 mnode = ctx.manifestnode()
352 352 if mnode is None:
353 353 mnode = self.repo.nodeconstants.wdirid
354 354 mrev = wdirrev
355 355 else:
356 356 mrev = self.repo.manifestlog.rev(mnode)
357 357 self.ui.write(
358 358 columns[b'manifest']
359 359 % scmutil.formatrevnode(self.ui, mrev, mnode),
360 360 label=b'ui.debug log.manifest',
361 361 )
362 362 self.ui.write(columns[b'user'] % ctx.user(), label=b'log.user')
363 363 self.ui.write(
364 364 columns[b'date'] % dateutil.datestr(ctx.date()), label=b'log.date'
365 365 )
366 366
367 367 if ctx.isunstable():
368 368 instabilities = ctx.instabilities()
369 369 self.ui.write(
370 370 columns[b'instability'] % b', '.join(instabilities),
371 371 label=b'log.instability',
372 372 )
373 373
374 374 elif ctx.obsolete():
375 375 self._showobsfate(ctx)
376 376
377 377 self._exthook(ctx)
378 378
379 379 if self.ui.debugflag:
380 files = ctx.p1().status(ctx)
381 380 for key, value in zip(
382 381 [b'files', b'files+', b'files-'],
383 [files.modified, files.added, files.removed],
382 [ctx.filesmodified(), ctx.filesadded(), ctx.filesremoved()],
384 383 ):
385 384 if value:
386 385 self.ui.write(
387 386 columns[key] % b" ".join(value),
388 387 label=b'ui.debug log.files',
389 388 )
390 389 elif ctx.files() and self.ui.verbose:
391 390 self.ui.write(
392 391 columns[b'files'] % b" ".join(ctx.files()),
393 392 label=b'ui.note log.files',
394 393 )
395 394 if copies and self.ui.verbose:
396 395 copies = [b'%s (%s)' % c for c in copies]
397 396 self.ui.write(
398 397 columns[b'copies'] % b' '.join(copies),
399 398 label=b'ui.note log.copies',
400 399 )
401 400
402 401 extra = ctx.extra()
403 402 if extra and self.ui.debugflag:
404 403 for key, value in sorted(extra.items()):
405 404 self.ui.write(
406 405 columns[b'extra'] % (key, stringutil.escapestr(value)),
407 406 label=b'ui.debug log.extra',
408 407 )
409 408
410 409 description = ctx.description().strip()
411 410 if description:
412 411 if self.ui.verbose:
413 412 self.ui.write(
414 413 _(b"description:\n"), label=b'ui.note log.description'
415 414 )
416 415 self.ui.write(description, label=b'ui.note log.description')
417 416 self.ui.write(b"\n\n")
418 417 else:
419 418 self.ui.write(
420 419 columns[b'summary'] % description.splitlines()[0],
421 420 label=b'log.summary',
422 421 )
423 422 self.ui.write(b"\n")
424 423
425 424 self._showpatch(ctx, graphwidth)
426 425
427 426 def _showobsfate(self, ctx):
428 427 # TODO: do not depend on templater
429 428 tres = formatter.templateresources(self.repo.ui, self.repo)
430 429 t = formatter.maketemplater(
431 430 self.repo.ui,
432 431 b'{join(obsfate, "\n")}',
433 432 defaults=templatekw.keywords,
434 433 resources=tres,
435 434 )
436 435 obsfate = t.renderdefault({b'ctx': ctx}).splitlines()
437 436
438 437 if obsfate:
439 438 for obsfateline in obsfate:
440 439 self.ui.write(
441 440 self._columns[b'obsolete'] % obsfateline,
442 441 label=b'log.obsfate',
443 442 )
444 443
445 444 def _exthook(self, ctx):
446 445 """empty method used by extension as a hook point"""
447 446
448 447 def _showpatch(self, ctx, graphwidth=0):
449 448 if self._includestat:
450 449 self._differ.showdiff(
451 450 self.ui, ctx, self._diffopts, graphwidth, stat=True
452 451 )
453 452 if self._includestat and self._includediff:
454 453 self.ui.write(b"\n")
455 454 if self._includediff:
456 455 self._differ.showdiff(
457 456 self.ui, ctx, self._diffopts, graphwidth, stat=False
458 457 )
459 458 if self._includestat or self._includediff:
460 459 self.ui.write(b"\n")
461 460
462 461
463 462 class changesetformatter(changesetprinter):
464 463 """Format changeset information by generic formatter"""
465 464
466 465 def __init__(
467 466 self, ui, repo, fm, differ=None, diffopts=None, buffered=False
468 467 ):
469 468 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
470 469 self._diffopts = patch.difffeatureopts(ui, diffopts, git=True)
471 470 self._fm = fm
472 471
473 472 def close(self):
474 473 self._fm.end()
475 474
476 475 def _show(self, ctx, copies, props):
477 476 '''show a single changeset or file revision'''
478 477 fm = self._fm
479 478 fm.startitem()
480 479 fm.context(ctx=ctx)
481 480 fm.data(rev=scmutil.intrev(ctx), node=fm.hexfunc(scmutil.binnode(ctx)))
482 481
483 482 datahint = fm.datahint()
484 483 if self.ui.quiet and not datahint:
485 484 return
486 485
487 486 fm.data(
488 487 branch=ctx.branch(),
489 488 phase=ctx.phasestr(),
490 489 user=ctx.user(),
491 490 date=fm.formatdate(ctx.date()),
492 491 desc=ctx.description(),
493 492 bookmarks=fm.formatlist(ctx.bookmarks(), name=b'bookmark'),
494 493 tags=fm.formatlist(ctx.tags(), name=b'tag'),
495 494 parents=fm.formatlist(
496 495 [fm.hexfunc(c.node()) for c in ctx.parents()], name=b'node'
497 496 ),
498 497 )
499 498
500 499 if self.ui.debugflag or b'manifest' in datahint:
501 500 fm.data(
502 501 manifest=fm.hexfunc(
503 502 ctx.manifestnode() or self.repo.nodeconstants.wdirid
504 503 )
505 504 )
506 505 if self.ui.debugflag or b'extra' in datahint:
507 506 fm.data(extra=fm.formatdict(ctx.extra()))
508 507
509 508 if (
510 509 self.ui.debugflag
511 510 or b'modified' in datahint
512 511 or b'added' in datahint
513 512 or b'removed' in datahint
514 513 ):
515 files = ctx.p1().status(ctx)
516 514 fm.data(
517 modified=fm.formatlist(files.modified, name=b'file'),
518 added=fm.formatlist(files.added, name=b'file'),
519 removed=fm.formatlist(files.removed, name=b'file'),
515 modified=fm.formatlist(ctx.filesmodified(), name=b'file'),
516 added=fm.formatlist(ctx.filesadded(), name=b'file'),
517 removed=fm.formatlist(ctx.filesremoved(), name=b'file'),
520 518 )
521 519
522 520 verbose = not self.ui.debugflag and self.ui.verbose
523 521 if verbose or b'files' in datahint:
524 522 fm.data(files=fm.formatlist(ctx.files(), name=b'file'))
525 523 if verbose and copies or b'copies' in datahint:
526 524 fm.data(
527 525 copies=fm.formatdict(copies or {}, key=b'name', value=b'source')
528 526 )
529 527
530 528 if self._includestat or b'diffstat' in datahint:
531 529 self.ui.pushbuffer()
532 530 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=True)
533 531 fm.data(diffstat=self.ui.popbuffer())
534 532 if self._includediff or b'diff' in datahint:
535 533 self.ui.pushbuffer()
536 534 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False)
537 535 fm.data(diff=self.ui.popbuffer())
538 536
539 537
540 538 class changesettemplater(changesetprinter):
541 539 """format changeset information.
542 540
543 541 Note: there are a variety of convenience functions to build a
544 542 changesettemplater for common cases. See functions such as:
545 543 maketemplater, changesetdisplayer, buildcommittemplate, or other
546 544 functions that use changesest_templater.
547 545 """
548 546
549 547 # Arguments before "buffered" used to be positional. Consider not
550 548 # adding/removing arguments before "buffered" to not break callers.
551 549 def __init__(
552 550 self, ui, repo, tmplspec, differ=None, diffopts=None, buffered=False
553 551 ):
554 552 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
555 553 # tres is shared with _graphnodeformatter()
556 554 self._tresources = tres = formatter.templateresources(ui, repo)
557 555 self.t = formatter.loadtemplater(
558 556 ui,
559 557 tmplspec,
560 558 defaults=templatekw.keywords,
561 559 resources=tres,
562 560 cache=templatekw.defaulttempl,
563 561 )
564 562 self._counter = itertools.count()
565 563
566 564 self._tref = tmplspec.ref
567 565 self._parts = {
568 566 b'header': b'',
569 567 b'footer': b'',
570 568 tmplspec.ref: tmplspec.ref,
571 569 b'docheader': b'',
572 570 b'docfooter': b'',
573 571 b'separator': b'',
574 572 }
575 573 if tmplspec.mapfile:
576 574 # find correct templates for current mode, for backward
577 575 # compatibility with 'log -v/-q/--debug' using a mapfile
578 576 tmplmodes = [
579 577 (True, b''),
580 578 (self.ui.verbose, b'_verbose'),
581 579 (self.ui.quiet, b'_quiet'),
582 580 (self.ui.debugflag, b'_debug'),
583 581 ]
584 582 for mode, postfix in tmplmodes:
585 583 for t in self._parts:
586 584 cur = t + postfix
587 585 if mode and cur in self.t:
588 586 self._parts[t] = cur
589 587 else:
590 588 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
591 589 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
592 590 self._parts.update(m)
593 591
594 592 if self._parts[b'docheader']:
595 593 self.ui.write(self.t.render(self._parts[b'docheader'], {}))
596 594
597 595 def close(self):
598 596 if self._parts[b'docfooter']:
599 597 if not self.footer:
600 598 self.footer = b""
601 599 self.footer += self.t.render(self._parts[b'docfooter'], {})
602 600 return super(changesettemplater, self).close()
603 601
604 602 def _show(self, ctx, copies, props):
605 603 '''show a single changeset or file revision'''
606 604 props = props.copy()
607 605 props[b'ctx'] = ctx
608 606 props[b'index'] = index = next(self._counter)
609 607 props[b'revcache'] = {b'copies': copies}
610 608 graphwidth = props.get(b'graphwidth', 0)
611 609
612 610 # write separator, which wouldn't work well with the header part below
613 611 # since there's inherently a conflict between header (across items) and
614 612 # separator (per item)
615 613 if self._parts[b'separator'] and index > 0:
616 614 self.ui.write(self.t.render(self._parts[b'separator'], {}))
617 615
618 616 # write header
619 617 if self._parts[b'header']:
620 618 h = self.t.render(self._parts[b'header'], props)
621 619 if self.buffered:
622 620 self.header[ctx.rev()] = h
623 621 else:
624 622 if self.lastheader != h:
625 623 self.lastheader = h
626 624 self.ui.write(h)
627 625
628 626 # write changeset metadata, then patch if requested
629 627 key = self._parts[self._tref]
630 628 self.ui.write(self.t.render(key, props))
631 629 self._exthook(ctx)
632 630 self._showpatch(ctx, graphwidth)
633 631
634 632 if self._parts[b'footer']:
635 633 if not self.footer:
636 634 self.footer = self.t.render(self._parts[b'footer'], props)
637 635
638 636
639 637 def templatespec(tmpl, mapfile):
640 638 assert not (tmpl and mapfile)
641 639 if mapfile:
642 640 return formatter.mapfile_templatespec(b'changeset', mapfile)
643 641 else:
644 642 return formatter.literal_templatespec(tmpl)
645 643
646 644
647 645 def _lookuptemplate(ui, tmpl, style):
648 646 """Find the template matching the given template spec or style
649 647
650 648 See formatter.lookuptemplate() for details.
651 649 """
652 650
653 651 # ui settings
654 652 if not tmpl and not style: # template are stronger than style
655 653 tmpl = ui.config(b'command-templates', b'log')
656 654 if tmpl:
657 655 return formatter.literal_templatespec(templater.unquotestring(tmpl))
658 656 else:
659 657 style = util.expandpath(ui.config(b'ui', b'style'))
660 658
661 659 if not tmpl and style:
662 660 mapfile = style
663 661 fp = None
664 662 if not os.path.split(mapfile)[0]:
665 663 (mapname, fp) = templater.try_open_template(
666 664 b'map-cmdline.' + mapfile
667 665 ) or templater.try_open_template(mapfile)
668 666 if mapname:
669 667 mapfile = mapname
670 668 return formatter.mapfile_templatespec(b'changeset', mapfile, fp)
671 669
672 670 return formatter.lookuptemplate(ui, b'changeset', tmpl)
673 671
674 672
675 673 def maketemplater(ui, repo, tmpl, buffered=False):
676 674 """Create a changesettemplater from a literal template 'tmpl'
677 675 byte-string."""
678 676 spec = formatter.literal_templatespec(tmpl)
679 677 return changesettemplater(ui, repo, spec, buffered=buffered)
680 678
681 679
682 680 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
683 681 """show one changeset using template or regular display.
684 682
685 683 Display format will be the first non-empty hit of:
686 684 1. option 'template'
687 685 2. option 'style'
688 686 3. [command-templates] setting 'log'
689 687 4. [ui] setting 'style'
690 688 If all of these values are either the unset or the empty string,
691 689 regular display via changesetprinter() is done.
692 690 """
693 691 postargs = (differ, opts, buffered)
694 692 spec = _lookuptemplate(ui, opts.get(b'template'), opts.get(b'style'))
695 693
696 694 # machine-readable formats have slightly different keyword set than
697 695 # plain templates, which are handled by changesetformatter.
698 696 # note that {b'pickle', b'debug'} can also be added to the list if needed.
699 697 if spec.ref in {b'cbor', b'json'}:
700 698 fm = ui.formatter(b'log', opts)
701 699 return changesetformatter(ui, repo, fm, *postargs)
702 700
703 701 if not spec.ref and not spec.tmpl and not spec.mapfile:
704 702 return changesetprinter(ui, repo, *postargs)
705 703
706 704 return changesettemplater(ui, repo, spec, *postargs)
707 705
708 706
709 707 @attr.s
710 708 class walkopts(object):
711 709 """Options to configure a set of revisions and file matcher factory
712 710 to scan revision/file history
713 711 """
714 712
715 713 # raw command-line parameters, which a matcher will be built from
716 714 pats = attr.ib()
717 715 opts = attr.ib()
718 716
719 717 # a list of revset expressions to be traversed; if follow, it specifies
720 718 # the start revisions
721 719 revspec = attr.ib()
722 720
723 721 # miscellaneous queries to filter revisions (see "hg help log" for details)
724 722 bookmarks = attr.ib(default=attr.Factory(list))
725 723 branches = attr.ib(default=attr.Factory(list))
726 724 date = attr.ib(default=None)
727 725 keywords = attr.ib(default=attr.Factory(list))
728 726 no_merges = attr.ib(default=False)
729 727 only_merges = attr.ib(default=False)
730 728 prune_ancestors = attr.ib(default=attr.Factory(list))
731 729 users = attr.ib(default=attr.Factory(list))
732 730
733 731 # miscellaneous matcher arguments
734 732 include_pats = attr.ib(default=attr.Factory(list))
735 733 exclude_pats = attr.ib(default=attr.Factory(list))
736 734
737 735 # 0: no follow, 1: follow first, 2: follow both parents
738 736 follow = attr.ib(default=0)
739 737
740 738 # do not attempt filelog-based traversal, which may be fast but cannot
741 739 # include revisions where files were removed
742 740 force_changelog_traversal = attr.ib(default=False)
743 741
744 742 # filter revisions by file patterns, which should be disabled only if
745 743 # you want to include revisions where files were unmodified
746 744 filter_revisions_by_pats = attr.ib(default=True)
747 745
748 746 # sort revisions prior to traversal: 'desc', 'topo', or None
749 747 sort_revisions = attr.ib(default=None)
750 748
751 749 # limit number of changes displayed; None means unlimited
752 750 limit = attr.ib(default=None)
753 751
754 752
755 753 def parseopts(ui, pats, opts):
756 754 # type: (Any, Sequence[bytes], Dict[bytes, Any]) -> walkopts
757 755 """Parse log command options into walkopts
758 756
759 757 The returned walkopts will be passed in to getrevs() or makewalker().
760 758 """
761 759 if opts.get(b'follow_first'):
762 760 follow = 1
763 761 elif opts.get(b'follow'):
764 762 follow = 2
765 763 else:
766 764 follow = 0
767 765
768 766 if opts.get(b'graph'):
769 767 if ui.configbool(b'experimental', b'log.topo'):
770 768 sort_revisions = b'topo'
771 769 else:
772 770 sort_revisions = b'desc'
773 771 else:
774 772 sort_revisions = None
775 773
776 774 return walkopts(
777 775 pats=pats,
778 776 opts=opts,
779 777 revspec=opts.get(b'rev', []),
780 778 bookmarks=opts.get(b'bookmark', []),
781 779 # branch and only_branch are really aliases and must be handled at
782 780 # the same time
783 781 branches=opts.get(b'branch', []) + opts.get(b'only_branch', []),
784 782 date=opts.get(b'date'),
785 783 keywords=opts.get(b'keyword', []),
786 784 no_merges=bool(opts.get(b'no_merges')),
787 785 only_merges=bool(opts.get(b'only_merges')),
788 786 prune_ancestors=opts.get(b'prune', []),
789 787 users=opts.get(b'user', []),
790 788 include_pats=opts.get(b'include', []),
791 789 exclude_pats=opts.get(b'exclude', []),
792 790 follow=follow,
793 791 force_changelog_traversal=bool(opts.get(b'removed')),
794 792 sort_revisions=sort_revisions,
795 793 limit=getlimit(opts),
796 794 )
797 795
798 796
799 797 def _makematcher(repo, revs, wopts):
800 798 """Build matcher and expanded patterns from log options
801 799
802 800 If --follow, revs are the revisions to follow from.
803 801
804 802 Returns (match, pats, slowpath) where
805 803 - match: a matcher built from the given pats and -I/-X opts
806 804 - pats: patterns used (globs are expanded on Windows)
807 805 - slowpath: True if patterns aren't as simple as scanning filelogs
808 806 """
809 807 # pats/include/exclude are passed to match.match() directly in
810 808 # _matchfiles() revset, but a log-like command should build its matcher
811 809 # with scmutil.match(). The difference is input pats are globbed on
812 810 # platforms without shell expansion (windows).
813 811 wctx = repo[None]
814 812 match, pats = scmutil.matchandpats(wctx, wopts.pats, wopts.opts)
815 813 slowpath = match.anypats() or (
816 814 not match.always() and wopts.force_changelog_traversal
817 815 )
818 816 if not slowpath:
819 817 if wopts.follow and wopts.revspec:
820 818 # There may be the case that a path doesn't exist in some (but
821 819 # not all) of the specified start revisions, but let's consider
822 820 # the path is valid. Missing files will be warned by the matcher.
823 821 startctxs = [repo[r] for r in revs]
824 822 for f in match.files():
825 823 found = False
826 824 for c in startctxs:
827 825 if f in c:
828 826 found = True
829 827 elif c.hasdir(f):
830 828 # If a directory exists in any of the start revisions,
831 829 # take the slow path.
832 830 found = slowpath = True
833 831 if not found:
834 832 raise error.StateError(
835 833 _(
836 834 b'cannot follow file not in any of the specified '
837 835 b'revisions: "%s"'
838 836 )
839 837 % f
840 838 )
841 839 elif wopts.follow:
842 840 for f in match.files():
843 841 if f not in wctx:
844 842 # If the file exists, it may be a directory, so let it
845 843 # take the slow path.
846 844 if os.path.exists(repo.wjoin(f)):
847 845 slowpath = True
848 846 continue
849 847 else:
850 848 raise error.StateError(
851 849 _(
852 850 b'cannot follow file not in parent '
853 851 b'revision: "%s"'
854 852 )
855 853 % f
856 854 )
857 855 filelog = repo.file(f)
858 856 if not filelog:
859 857 # A file exists in wdir but not in history, which means
860 858 # the file isn't committed yet.
861 859 raise error.StateError(
862 860 _(b'cannot follow nonexistent file: "%s"') % f
863 861 )
864 862 else:
865 863 for f in match.files():
866 864 filelog = repo.file(f)
867 865 if not filelog:
868 866 # A zero count may be a directory or deleted file, so
869 867 # try to find matching entries on the slow path.
870 868 slowpath = True
871 869
872 870 # We decided to fall back to the slowpath because at least one
873 871 # of the paths was not a file. Check to see if at least one of them
874 872 # existed in history - in that case, we'll continue down the
875 873 # slowpath; otherwise, we can turn off the slowpath
876 874 if slowpath:
877 875 for path in match.files():
878 876 if not path or path in repo.store:
879 877 break
880 878 else:
881 879 slowpath = False
882 880
883 881 return match, pats, slowpath
884 882
885 883
886 884 def _fileancestors(repo, revs, match, followfirst):
887 885 fctxs = []
888 886 for r in revs:
889 887 ctx = repo[r]
890 888 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
891 889
892 890 # When displaying a revision with --patch --follow FILE, we have
893 891 # to know which file of the revision must be diffed. With
894 892 # --follow, we want the names of the ancestors of FILE in the
895 893 # revision, stored in "fcache". "fcache" is populated as a side effect
896 894 # of the graph traversal.
897 895 fcache = {}
898 896
899 897 def filematcher(ctx):
900 898 return scmutil.matchfiles(repo, fcache.get(scmutil.intrev(ctx), []))
901 899
902 900 def revgen():
903 901 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
904 902 fcache[rev] = [c.path() for c in cs]
905 903 yield rev
906 904
907 905 return smartset.generatorset(revgen(), iterasc=False), filematcher
908 906
909 907
910 908 def _makenofollowfilematcher(repo, pats, opts):
911 909 '''hook for extensions to override the filematcher for non-follow cases'''
912 910 return None
913 911
914 912
915 913 def revsingle(repo, revspec, default=b'.', localalias=None):
916 914 """Resolves user-provided revset(s) into a single revision.
917 915
918 916 This just wraps the lower-level scmutil.revsingle() in order to raise an
919 917 exception indicating user error.
920 918 """
921 919 try:
922 920 return scmutil.revsingle(repo, revspec, default, localalias)
923 921 except error.RepoLookupError as e:
924 922 raise error.InputError(e.args[0], hint=e.hint)
925 923
926 924
927 925 def revpair(repo, revs):
928 926 """Resolves user-provided revset(s) into two revisions.
929 927
930 928 This just wraps the lower-level scmutil.revpair() in order to raise an
931 929 exception indicating user error.
932 930 """
933 931 try:
934 932 return scmutil.revpair(repo, revs)
935 933 except error.RepoLookupError as e:
936 934 raise error.InputError(e.args[0], hint=e.hint)
937 935
938 936
939 937 def revrange(repo, specs, localalias=None):
940 938 """Resolves user-provided revset(s).
941 939
942 940 This just wraps the lower-level scmutil.revrange() in order to raise an
943 941 exception indicating user error.
944 942 """
945 943 try:
946 944 return scmutil.revrange(repo, specs, localalias)
947 945 except error.RepoLookupError as e:
948 946 raise error.InputError(e.args[0], hint=e.hint)
949 947
950 948
951 949 _opt2logrevset = {
952 950 b'no_merges': (b'not merge()', None),
953 951 b'only_merges': (b'merge()', None),
954 952 b'_matchfiles': (None, b'_matchfiles(%ps)'),
955 953 b'date': (b'date(%s)', None),
956 954 b'branch': (b'branch(%s)', b'%lr'),
957 955 b'_patslog': (b'filelog(%s)', b'%lr'),
958 956 b'keyword': (b'keyword(%s)', b'%lr'),
959 957 b'prune': (b'ancestors(%s)', b'not %lr'),
960 958 b'user': (b'user(%s)', b'%lr'),
961 959 }
962 960
963 961
964 962 def _makerevset(repo, wopts, slowpath):
965 963 """Return a revset string built from log options and file patterns"""
966 964 opts = {
967 965 b'branch': [b'literal:' + repo.lookupbranch(b) for b in wopts.branches],
968 966 b'date': wopts.date,
969 967 b'keyword': wopts.keywords,
970 968 b'no_merges': wopts.no_merges,
971 969 b'only_merges': wopts.only_merges,
972 970 b'prune': wopts.prune_ancestors,
973 971 b'user': [b'literal:' + v for v in wopts.users],
974 972 }
975 973
976 974 if wopts.filter_revisions_by_pats and slowpath:
977 975 # pats/include/exclude cannot be represented as separate
978 976 # revset expressions as their filtering logic applies at file
979 977 # level. For instance "-I a -X b" matches a revision touching
980 978 # "a" and "b" while "file(a) and not file(b)" does
981 979 # not. Besides, filesets are evaluated against the working
982 980 # directory.
983 981 matchargs = [b'r:', b'd:relpath']
984 982 for p in wopts.pats:
985 983 matchargs.append(b'p:' + p)
986 984 for p in wopts.include_pats:
987 985 matchargs.append(b'i:' + p)
988 986 for p in wopts.exclude_pats:
989 987 matchargs.append(b'x:' + p)
990 988 opts[b'_matchfiles'] = matchargs
991 989 elif wopts.filter_revisions_by_pats and not wopts.follow:
992 990 opts[b'_patslog'] = list(wopts.pats)
993 991
994 992 expr = []
995 993 for op, val in sorted(pycompat.iteritems(opts)):
996 994 if not val:
997 995 continue
998 996 revop, listop = _opt2logrevset[op]
999 997 if revop and b'%' not in revop:
1000 998 expr.append(revop)
1001 999 elif not listop:
1002 1000 expr.append(revsetlang.formatspec(revop, val))
1003 1001 else:
1004 1002 if revop:
1005 1003 val = [revsetlang.formatspec(revop, v) for v in val]
1006 1004 expr.append(revsetlang.formatspec(listop, val))
1007 1005
1008 1006 if wopts.bookmarks:
1009 1007 expr.append(
1010 1008 revsetlang.formatspec(
1011 1009 b'%lr',
1012 1010 [scmutil.format_bookmark_revspec(v) for v in wopts.bookmarks],
1013 1011 )
1014 1012 )
1015 1013
1016 1014 if expr:
1017 1015 expr = b'(' + b' and '.join(expr) + b')'
1018 1016 else:
1019 1017 expr = None
1020 1018 return expr
1021 1019
1022 1020
1023 1021 def _initialrevs(repo, wopts):
1024 1022 """Return the initial set of revisions to be filtered or followed"""
1025 1023 if wopts.revspec:
1026 1024 revs = revrange(repo, wopts.revspec)
1027 1025 elif wopts.follow and repo.dirstate.p1() == repo.nullid:
1028 1026 revs = smartset.baseset()
1029 1027 elif wopts.follow:
1030 1028 revs = repo.revs(b'.')
1031 1029 else:
1032 1030 revs = smartset.spanset(repo)
1033 1031 revs.reverse()
1034 1032 return revs
1035 1033
1036 1034
1037 1035 def makewalker(repo, wopts):
1038 1036 # type: (Any, walkopts) -> Tuple[smartset.abstractsmartset, Optional[Callable[[Any], matchmod.basematcher]]]
1039 1037 """Build (revs, makefilematcher) to scan revision/file history
1040 1038
1041 1039 - revs is the smartset to be traversed.
1042 1040 - makefilematcher is a function to map ctx to a matcher for that revision
1043 1041 """
1044 1042 revs = _initialrevs(repo, wopts)
1045 1043 if not revs:
1046 1044 return smartset.baseset(), None
1047 1045 # TODO: might want to merge slowpath with wopts.force_changelog_traversal
1048 1046 match, pats, slowpath = _makematcher(repo, revs, wopts)
1049 1047 wopts = attr.evolve(wopts, pats=pats)
1050 1048
1051 1049 filematcher = None
1052 1050 if wopts.follow:
1053 1051 if slowpath or match.always():
1054 1052 revs = dagop.revancestors(repo, revs, followfirst=wopts.follow == 1)
1055 1053 else:
1056 1054 assert not wopts.force_changelog_traversal
1057 1055 revs, filematcher = _fileancestors(
1058 1056 repo, revs, match, followfirst=wopts.follow == 1
1059 1057 )
1060 1058 revs.reverse()
1061 1059 if filematcher is None:
1062 1060 filematcher = _makenofollowfilematcher(repo, wopts.pats, wopts.opts)
1063 1061 if filematcher is None:
1064 1062
1065 1063 def filematcher(ctx):
1066 1064 return match
1067 1065
1068 1066 expr = _makerevset(repo, wopts, slowpath)
1069 1067 if wopts.sort_revisions:
1070 1068 assert wopts.sort_revisions in {b'topo', b'desc'}
1071 1069 if wopts.sort_revisions == b'topo':
1072 1070 if not revs.istopo():
1073 1071 revs = dagop.toposort(revs, repo.changelog.parentrevs)
1074 1072 # TODO: try to iterate the set lazily
1075 1073 revs = revset.baseset(list(revs), istopo=True)
1076 1074 elif not (revs.isdescending() or revs.istopo()):
1077 1075 # User-specified revs might be unsorted
1078 1076 revs.sort(reverse=True)
1079 1077 if expr:
1080 1078 matcher = revset.match(None, expr)
1081 1079 revs = matcher(repo, revs)
1082 1080 if wopts.limit is not None:
1083 1081 revs = revs.slice(0, wopts.limit)
1084 1082
1085 1083 return revs, filematcher
1086 1084
1087 1085
1088 1086 def getrevs(repo, wopts):
1089 1087 # type: (Any, walkopts) -> Tuple[smartset.abstractsmartset, Optional[changesetdiffer]]
1090 1088 """Return (revs, differ) where revs is a smartset
1091 1089
1092 1090 differ is a changesetdiffer with pre-configured file matcher.
1093 1091 """
1094 1092 revs, filematcher = makewalker(repo, wopts)
1095 1093 if not revs:
1096 1094 return revs, None
1097 1095 differ = changesetdiffer()
1098 1096 differ._makefilematcher = filematcher
1099 1097 return revs, differ
1100 1098
1101 1099
1102 1100 def _parselinerangeopt(repo, opts):
1103 1101 """Parse --line-range log option and return a list of tuples (filename,
1104 1102 (fromline, toline)).
1105 1103 """
1106 1104 linerangebyfname = []
1107 1105 for pat in opts.get(b'line_range', []):
1108 1106 try:
1109 1107 pat, linerange = pat.rsplit(b',', 1)
1110 1108 except ValueError:
1111 1109 raise error.InputError(
1112 1110 _(b'malformatted line-range pattern %s') % pat
1113 1111 )
1114 1112 try:
1115 1113 fromline, toline = map(int, linerange.split(b':'))
1116 1114 except ValueError:
1117 1115 raise error.InputError(_(b"invalid line range for %s") % pat)
1118 1116 msg = _(b"line range pattern '%s' must match exactly one file") % pat
1119 1117 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
1120 1118 linerangebyfname.append(
1121 1119 (fname, util.processlinerange(fromline, toline))
1122 1120 )
1123 1121 return linerangebyfname
1124 1122
1125 1123
1126 1124 def getlinerangerevs(repo, userrevs, opts):
1127 1125 """Return (revs, differ).
1128 1126
1129 1127 "revs" are revisions obtained by processing "line-range" log options and
1130 1128 walking block ancestors of each specified file/line-range.
1131 1129
1132 1130 "differ" is a changesetdiffer with pre-configured file matcher and hunks
1133 1131 filter.
1134 1132 """
1135 1133 wctx = repo[None]
1136 1134
1137 1135 # Two-levels map of "rev -> file ctx -> [line range]".
1138 1136 linerangesbyrev = {}
1139 1137 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
1140 1138 if fname not in wctx:
1141 1139 raise error.StateError(
1142 1140 _(b'cannot follow file not in parent revision: "%s"') % fname
1143 1141 )
1144 1142 fctx = wctx.filectx(fname)
1145 1143 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
1146 1144 rev = fctx.introrev()
1147 1145 if rev is None:
1148 1146 rev = wdirrev
1149 1147 if rev not in userrevs:
1150 1148 continue
1151 1149 linerangesbyrev.setdefault(rev, {}).setdefault(
1152 1150 fctx.path(), []
1153 1151 ).append(linerange)
1154 1152
1155 1153 def nofilterhunksfn(fctx, hunks):
1156 1154 return hunks
1157 1155
1158 1156 def hunksfilter(ctx):
1159 1157 fctxlineranges = linerangesbyrev.get(scmutil.intrev(ctx))
1160 1158 if fctxlineranges is None:
1161 1159 return nofilterhunksfn
1162 1160
1163 1161 def filterfn(fctx, hunks):
1164 1162 lineranges = fctxlineranges.get(fctx.path())
1165 1163 if lineranges is not None:
1166 1164 for hr, lines in hunks:
1167 1165 if hr is None: # binary
1168 1166 yield hr, lines
1169 1167 continue
1170 1168 if any(mdiff.hunkinrange(hr[2:], lr) for lr in lineranges):
1171 1169 yield hr, lines
1172 1170 else:
1173 1171 for hunk in hunks:
1174 1172 yield hunk
1175 1173
1176 1174 return filterfn
1177 1175
1178 1176 def filematcher(ctx):
1179 1177 files = list(linerangesbyrev.get(scmutil.intrev(ctx), []))
1180 1178 return scmutil.matchfiles(repo, files)
1181 1179
1182 1180 revs = sorted(linerangesbyrev, reverse=True)
1183 1181
1184 1182 differ = changesetdiffer()
1185 1183 differ._makefilematcher = filematcher
1186 1184 differ._makehunksfilter = hunksfilter
1187 1185 return smartset.baseset(revs), differ
1188 1186
1189 1187
1190 1188 def _graphnodeformatter(ui, displayer):
1191 1189 spec = ui.config(b'command-templates', b'graphnode')
1192 1190 if not spec:
1193 1191 return templatekw.getgraphnode # fast path for "{graphnode}"
1194 1192
1195 1193 spec = templater.unquotestring(spec)
1196 1194 if isinstance(displayer, changesettemplater):
1197 1195 # reuse cache of slow templates
1198 1196 tres = displayer._tresources
1199 1197 else:
1200 1198 tres = formatter.templateresources(ui)
1201 1199 templ = formatter.maketemplater(
1202 1200 ui, spec, defaults=templatekw.keywords, resources=tres
1203 1201 )
1204 1202
1205 1203 def formatnode(repo, ctx, cache):
1206 1204 props = {b'ctx': ctx, b'repo': repo}
1207 1205 return templ.renderdefault(props)
1208 1206
1209 1207 return formatnode
1210 1208
1211 1209
1212 1210 def displaygraph(ui, repo, dag, displayer, edgefn, getcopies=None, props=None):
1213 1211 props = props or {}
1214 1212 formatnode = _graphnodeformatter(ui, displayer)
1215 1213 state = graphmod.asciistate()
1216 1214 styles = state.styles
1217 1215
1218 1216 # only set graph styling if HGPLAIN is not set.
1219 1217 if ui.plain(b'graph'):
1220 1218 # set all edge styles to |, the default pre-3.8 behaviour
1221 1219 styles.update(dict.fromkeys(styles, b'|'))
1222 1220 else:
1223 1221 edgetypes = {
1224 1222 b'parent': graphmod.PARENT,
1225 1223 b'grandparent': graphmod.GRANDPARENT,
1226 1224 b'missing': graphmod.MISSINGPARENT,
1227 1225 }
1228 1226 for name, key in edgetypes.items():
1229 1227 # experimental config: experimental.graphstyle.*
1230 1228 styles[key] = ui.config(
1231 1229 b'experimental', b'graphstyle.%s' % name, styles[key]
1232 1230 )
1233 1231 if not styles[key]:
1234 1232 styles[key] = None
1235 1233
1236 1234 # experimental config: experimental.graphshorten
1237 1235 state.graphshorten = ui.configbool(b'experimental', b'graphshorten')
1238 1236
1239 1237 formatnode_cache = {}
1240 1238 for rev, type, ctx, parents in dag:
1241 1239 char = formatnode(repo, ctx, formatnode_cache)
1242 1240 copies = getcopies(ctx) if getcopies else None
1243 1241 edges = edgefn(type, char, state, rev, parents)
1244 1242 firstedge = next(edges)
1245 1243 width = firstedge[2]
1246 1244 displayer.show(
1247 1245 ctx, copies=copies, graphwidth=width, **pycompat.strkwargs(props)
1248 1246 )
1249 1247 lines = displayer.hunk.pop(rev).split(b'\n')
1250 1248 if not lines[-1]:
1251 1249 del lines[-1]
1252 1250 displayer.flush(ctx)
1253 1251 for type, char, width, coldata in itertools.chain([firstedge], edges):
1254 1252 graphmod.ascii(ui, state, type, char, lines, coldata)
1255 1253 lines = []
1256 1254 displayer.close()
1257 1255
1258 1256
1259 1257 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
1260 1258 revdag = graphmod.dagwalker(repo, revs)
1261 1259 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
1262 1260
1263 1261
1264 1262 def displayrevs(ui, repo, revs, displayer, getcopies):
1265 1263 for rev in revs:
1266 1264 ctx = repo[rev]
1267 1265 copies = getcopies(ctx) if getcopies else None
1268 1266 displayer.show(ctx, copies=copies)
1269 1267 displayer.flush(ctx)
1270 1268 displayer.close()
1271 1269
1272 1270
1273 1271 def checkunsupportedgraphflags(pats, opts):
1274 1272 for op in [b"newest_first"]:
1275 1273 if op in opts and opts[op]:
1276 1274 raise error.InputError(
1277 1275 _(b"-G/--graph option is incompatible with --%s")
1278 1276 % op.replace(b"_", b"-")
1279 1277 )
1280 1278
1281 1279
1282 1280 def graphrevs(repo, nodes, opts):
1283 1281 limit = getlimit(opts)
1284 1282 nodes.reverse()
1285 1283 if limit is not None:
1286 1284 nodes = nodes[:limit]
1287 1285 return graphmod.nodes(repo, nodes)
@@ -1,37 +1,35
1 1 hg log --debug shouldn't show different data than {file_*} template keywords
2 2 https://bz.mercurial-scm.org/show_bug.cgi?id=6642
3 3
4 4 $ hg init issue6642
5 5 $ cd issue6642
6 6
7 7 $ echo a > a
8 8 $ hg ci -qAm a
9 9 $ echo b > b
10 10 $ hg ci -qAm b
11 11 $ hg up 0 -q
12 12 $ echo c > c
13 13 $ hg ci -qAm c
14 14 $ hg merge -q
15 15 $ hg ci -m merge
16 16
17 17 $ hg log -GT '{rev} {desc} file_adds: [{file_adds}], file_mods: [{file_mods}], file_dels: [{file_dels}], files: [{files}]\n'
18 18 @ 3 merge file_adds: [], file_mods: [], file_dels: [], files: []
19 19 |\
20 20 | o 2 c file_adds: [c], file_mods: [], file_dels: [], files: [c]
21 21 | |
22 22 o | 1 b file_adds: [b], file_mods: [], file_dels: [], files: [b]
23 23 |/
24 24 o 0 a file_adds: [a], file_mods: [], file_dels: [], files: [a]
25 25
26 26
27 27 $ hg log -r . --debug | grep files
28 files+: b (known-bad-output !)
29 [1] (missing-correct-output !)
28 [1]
30 29 $ hg log -r . --debug -T json | egrep '(added|removed|modified)'
31 "added": ["b"], (known-bad-output !)
32 "added": [], (missing-correct-output !)
30 "added": [],
33 31 "modified": [],
34 32 "removed": [],
35 33 $ hg log -r . --debug -T xml | grep path
36 34 <paths>
37 35 </paths>
@@ -1,1043 +1,1042
1 1 $ cat > $TESTTMP/hook.sh << 'EOF'
2 2 > echo "test-hook-close-phase: $HG_NODE: $HG_OLDPHASE -> $HG_PHASE"
3 3 > EOF
4 4
5 5 $ cat >> $HGRCPATH << EOF
6 6 > [extensions]
7 7 > phasereport=$TESTDIR/testlib/ext-phase-report.py
8 8 > [hooks]
9 9 > txnclose-phase.test = sh $TESTTMP/hook.sh
10 10 > EOF
11 11
12 12 $ hglog() { hg log --template "{rev} {phaseidx} {desc}\n" $*; }
13 13 $ mkcommit() {
14 14 > echo "$1" > "$1"
15 15 > hg add "$1"
16 16 > message="$1"
17 17 > shift
18 18 > hg ci -m "$message" $*
19 19 > }
20 20
21 21 $ hg init initialrepo
22 22 $ cd initialrepo
23 23
24 24 Cannot change null revision phase
25 25
26 26 $ hg phase --force --secret null
27 27 abort: cannot change null revision phase
28 28 [255]
29 29 $ hg phase null
30 30 -1: public
31 31
32 32 $ mkcommit A
33 33 test-debug-phase: new rev 0: x -> 1
34 34 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: -> draft
35 35
36 36 New commit are draft by default
37 37
38 38 $ hglog
39 39 0 1 A
40 40
41 41 Following commit are draft too
42 42
43 43 $ mkcommit B
44 44 test-debug-phase: new rev 1: x -> 1
45 45 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: -> draft
46 46
47 47 $ hglog
48 48 1 1 B
49 49 0 1 A
50 50
51 51 Working directory phase is secret when its parent is secret.
52 52
53 53 $ hg phase --force --secret .
54 54 test-debug-phase: move rev 0: 1 -> 2
55 55 test-debug-phase: move rev 1: 1 -> 2
56 56 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: draft -> secret
57 57 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: draft -> secret
58 58 $ hg log -r 'wdir()' -T '{phase}\n'
59 59 secret
60 60 $ hg log -r 'wdir() and public()' -T '{phase}\n'
61 61 $ hg log -r 'wdir() and draft()' -T '{phase}\n'
62 62 $ hg log -r 'wdir() and secret()' -T '{phase}\n'
63 63 secret
64 64
65 65 Working directory phase is draft when its parent is draft.
66 66
67 67 $ hg phase --draft .
68 68 test-debug-phase: move rev 1: 2 -> 1
69 69 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: secret -> draft
70 70 $ hg log -r 'wdir()' -T '{phase}\n'
71 71 draft
72 72 $ hg log -r 'wdir() and public()' -T '{phase}\n'
73 73 $ hg log -r 'wdir() and draft()' -T '{phase}\n'
74 74 draft
75 75 $ hg log -r 'wdir() and secret()' -T '{phase}\n'
76 76
77 77 Working directory phase is secret when a new commit will be created as secret,
78 78 even if the parent is draft.
79 79
80 80 $ hg log -r 'wdir() and secret()' -T '{phase}\n' \
81 81 > --config phases.new-commit='secret'
82 82 secret
83 83
84 84 Working directory phase is draft when its parent is public.
85 85
86 86 $ hg phase --public .
87 87 test-debug-phase: move rev 0: 1 -> 0
88 88 test-debug-phase: move rev 1: 1 -> 0
89 89 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: draft -> public
90 90 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: draft -> public
91 91 $ hg log -r 'wdir()' -T '{phase}\n'
92 92 draft
93 93 $ hg log -r 'wdir() and public()' -T '{phase}\n'
94 94 $ hg log -r 'wdir() and draft()' -T '{phase}\n'
95 95 draft
96 96 $ hg log -r 'wdir() and secret()' -T '{phase}\n'
97 97 $ hg log -r 'wdir() and secret()' -T '{phase}\n' \
98 98 > --config phases.new-commit='secret'
99 99 secret
100 100
101 101 Draft commit are properly created over public one:
102 102
103 103 $ hg phase
104 104 1: public
105 105 $ hglog
106 106 1 0 B
107 107 0 0 A
108 108
109 109 $ mkcommit C
110 110 test-debug-phase: new rev 2: x -> 1
111 111 test-hook-close-phase: f838bfaca5c7226600ebcfd84f3c3c13a28d3757: -> draft
112 112 $ mkcommit D
113 113 test-debug-phase: new rev 3: x -> 1
114 114 test-hook-close-phase: b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e: -> draft
115 115
116 116 $ hglog
117 117 3 1 D
118 118 2 1 C
119 119 1 0 B
120 120 0 0 A
121 121
122 122 Test creating changeset as secret
123 123
124 124 $ mkcommit E --config phases.new-commit='secret'
125 125 test-debug-phase: new rev 4: x -> 2
126 126 test-hook-close-phase: a603bfb5a83e312131cebcd05353c217d4d21dde: -> secret
127 127 $ hglog
128 128 4 2 E
129 129 3 1 D
130 130 2 1 C
131 131 1 0 B
132 132 0 0 A
133 133
134 134 Test the secret property is inherited
135 135
136 136 $ mkcommit H
137 137 test-debug-phase: new rev 5: x -> 2
138 138 test-hook-close-phase: a030c6be5127abc010fcbff1851536552e6951a8: -> secret
139 139 $ hglog
140 140 5 2 H
141 141 4 2 E
142 142 3 1 D
143 143 2 1 C
144 144 1 0 B
145 145 0 0 A
146 146
147 147 Even on merge
148 148
149 149 $ hg up -q 1
150 150 $ mkcommit "B'"
151 151 test-debug-phase: new rev 6: x -> 1
152 152 created new head
153 153 test-hook-close-phase: cf9fe039dfd67e829edf6522a45de057b5c86519: -> draft
154 154 $ hglog
155 155 6 1 B'
156 156 5 2 H
157 157 4 2 E
158 158 3 1 D
159 159 2 1 C
160 160 1 0 B
161 161 0 0 A
162 162 $ hg merge 4 # E
163 163 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
164 164 (branch merge, don't forget to commit)
165 165 $ hg phase
166 166 6: draft
167 167 4: secret
168 168 $ hg ci -m "merge B' and E"
169 169 test-debug-phase: new rev 7: x -> 2
170 170 test-hook-close-phase: 17a481b3bccb796c0521ae97903d81c52bfee4af: -> secret
171 171
172 172 $ hglog
173 173 7 2 merge B' and E
174 174 6 1 B'
175 175 5 2 H
176 176 4 2 E
177 177 3 1 D
178 178 2 1 C
179 179 1 0 B
180 180 0 0 A
181 181
182 182 Test secret changeset are not pushed
183 183
184 184 $ hg init ../push-dest
185 185 $ cat > ../push-dest/.hg/hgrc << EOF
186 186 > [phases]
187 187 > publish=False
188 188 > EOF
189 189 $ hg outgoing ../push-dest --template='{rev} {phase} {desc|firstline}\n'
190 190 comparing with ../push-dest
191 191 searching for changes
192 192 0 public A
193 193 1 public B
194 194 2 draft C
195 195 3 draft D
196 196 6 draft B'
197 197 $ hg outgoing -r 'branch(default)' ../push-dest --template='{rev} {phase} {desc|firstline}\n'
198 198 comparing with ../push-dest
199 199 searching for changes
200 200 0 public A
201 201 1 public B
202 202 2 draft C
203 203 3 draft D
204 204 6 draft B'
205 205
206 206 $ hg push ../push-dest -f # force because we push multiple heads
207 207 pushing to ../push-dest
208 208 searching for changes
209 209 adding changesets
210 210 adding manifests
211 211 adding file changes
212 212 added 5 changesets with 5 changes to 5 files (+1 heads)
213 213 test-debug-phase: new rev 0: x -> 0
214 214 test-debug-phase: new rev 1: x -> 0
215 215 test-debug-phase: new rev 2: x -> 1
216 216 test-debug-phase: new rev 3: x -> 1
217 217 test-debug-phase: new rev 4: x -> 1
218 218 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: -> public
219 219 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: -> public
220 220 test-hook-close-phase: f838bfaca5c7226600ebcfd84f3c3c13a28d3757: -> draft
221 221 test-hook-close-phase: b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e: -> draft
222 222 test-hook-close-phase: cf9fe039dfd67e829edf6522a45de057b5c86519: -> draft
223 223 $ hglog
224 224 7 2 merge B' and E
225 225 6 1 B'
226 226 5 2 H
227 227 4 2 E
228 228 3 1 D
229 229 2 1 C
230 230 1 0 B
231 231 0 0 A
232 232 $ cd ../push-dest
233 233 $ hglog
234 234 4 1 B'
235 235 3 1 D
236 236 2 1 C
237 237 1 0 B
238 238 0 0 A
239 239
240 240 (Issue3303)
241 241 Check that remote secret changeset are ignore when checking creation of remote heads
242 242
243 243 We add a secret head into the push destination. This secret head shadows a
244 244 visible shared between the initial repo and the push destination.
245 245
246 246 $ hg up -q 4 # B'
247 247 $ mkcommit Z --config phases.new-commit=secret
248 248 test-debug-phase: new rev 5: x -> 2
249 249 test-hook-close-phase: 2713879da13d6eea1ff22b442a5a87cb31a7ce6a: -> secret
250 250 $ hg phase .
251 251 5: secret
252 252
253 253 We now try to push a new public changeset that descend from the common public
254 254 head shadowed by the remote secret head.
255 255
256 256 $ cd ../initialrepo
257 257 $ hg up -q 6 #B'
258 258 $ mkcommit I
259 259 test-debug-phase: new rev 8: x -> 1
260 260 created new head
261 261 test-hook-close-phase: 6d6770faffce199f1fddd1cf87f6f026138cf061: -> draft
262 262 $ hg push ../push-dest
263 263 pushing to ../push-dest
264 264 searching for changes
265 265 adding changesets
266 266 adding manifests
267 267 adding file changes
268 268 added 1 changesets with 1 changes to 1 files (+1 heads)
269 269 test-debug-phase: new rev 6: x -> 1
270 270 test-hook-close-phase: 6d6770faffce199f1fddd1cf87f6f026138cf061: -> draft
271 271
272 272 :note: The "(+1 heads)" is wrong as we do not had any visible head
273 273
274 274 check that branch cache with "served" filter are properly computed and stored
275 275
276 276 $ ls ../push-dest/.hg/cache/branch2*
277 277 ../push-dest/.hg/cache/branch2-base
278 278 ../push-dest/.hg/cache/branch2-served
279 279 $ cat ../push-dest/.hg/cache/branch2-served
280 280 6d6770faffce199f1fddd1cf87f6f026138cf061 6 465891ffab3c47a3c23792f7dc84156e19a90722
281 281 b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e o default
282 282 6d6770faffce199f1fddd1cf87f6f026138cf061 o default
283 283 $ hg heads -R ../push-dest --template '{rev}:{node} {phase}\n' #update visible cache too
284 284 6:6d6770faffce199f1fddd1cf87f6f026138cf061 draft
285 285 5:2713879da13d6eea1ff22b442a5a87cb31a7ce6a secret
286 286 3:b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e draft
287 287 $ ls ../push-dest/.hg/cache/branch2*
288 288 ../push-dest/.hg/cache/branch2-base
289 289 ../push-dest/.hg/cache/branch2-served
290 290 ../push-dest/.hg/cache/branch2-visible
291 291 $ cat ../push-dest/.hg/cache/branch2-served
292 292 6d6770faffce199f1fddd1cf87f6f026138cf061 6 465891ffab3c47a3c23792f7dc84156e19a90722
293 293 b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e o default
294 294 6d6770faffce199f1fddd1cf87f6f026138cf061 o default
295 295 $ cat ../push-dest/.hg/cache/branch2-visible
296 296 6d6770faffce199f1fddd1cf87f6f026138cf061 6
297 297 b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e o default
298 298 2713879da13d6eea1ff22b442a5a87cb31a7ce6a o default
299 299 6d6770faffce199f1fddd1cf87f6f026138cf061 o default
300 300
301 301
302 302 Restore condition prior extra insertion.
303 303 $ hg -q --config extensions.mq= strip .
304 304 $ hg up -q 7
305 305 $ cd ..
306 306
307 307 Test secret changeset are not pull
308 308
309 309 $ hg init pull-dest
310 310 $ cd pull-dest
311 311 $ hg pull ../initialrepo
312 312 pulling from ../initialrepo
313 313 requesting all changes
314 314 adding changesets
315 315 adding manifests
316 316 adding file changes
317 317 added 5 changesets with 5 changes to 5 files (+1 heads)
318 318 new changesets 4a2df7238c3b:cf9fe039dfd6
319 319 test-debug-phase: new rev 0: x -> 0
320 320 test-debug-phase: new rev 1: x -> 0
321 321 test-debug-phase: new rev 2: x -> 0
322 322 test-debug-phase: new rev 3: x -> 0
323 323 test-debug-phase: new rev 4: x -> 0
324 324 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: -> public
325 325 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: -> public
326 326 test-hook-close-phase: f838bfaca5c7226600ebcfd84f3c3c13a28d3757: -> public
327 327 test-hook-close-phase: b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e: -> public
328 328 test-hook-close-phase: cf9fe039dfd67e829edf6522a45de057b5c86519: -> public
329 329 (run 'hg heads' to see heads, 'hg merge' to merge)
330 330 $ hglog
331 331 4 0 B'
332 332 3 0 D
333 333 2 0 C
334 334 1 0 B
335 335 0 0 A
336 336 $ cd ..
337 337
338 338 But secret can still be bundled explicitly
339 339
340 340 $ cd initialrepo
341 341 $ hg bundle --base '4^' -r 'children(4)' ../secret-bundle.hg
342 342 4 changesets found
343 343 $ cd ..
344 344
345 345 Test secret changeset are not cloned
346 346 (during local clone)
347 347
348 348 $ hg clone -qU initialrepo clone-dest
349 349 test-debug-phase: new rev 0: x -> 0
350 350 test-debug-phase: new rev 1: x -> 0
351 351 test-debug-phase: new rev 2: x -> 0
352 352 test-debug-phase: new rev 3: x -> 0
353 353 test-debug-phase: new rev 4: x -> 0
354 354 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: -> public
355 355 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: -> public
356 356 test-hook-close-phase: f838bfaca5c7226600ebcfd84f3c3c13a28d3757: -> public
357 357 test-hook-close-phase: b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e: -> public
358 358 test-hook-close-phase: cf9fe039dfd67e829edf6522a45de057b5c86519: -> public
359 359 $ hglog -R clone-dest
360 360 4 0 B'
361 361 3 0 D
362 362 2 0 C
363 363 1 0 B
364 364 0 0 A
365 365
366 366 Test summary
367 367
368 368 $ hg summary -R clone-dest --verbose
369 369 parent: -1:000000000000 (no revision checked out)
370 370 branch: default
371 371 commit: (clean)
372 372 update: 5 new changesets (update)
373 373 $ hg summary -R initialrepo
374 374 parent: 7:17a481b3bccb tip
375 375 merge B' and E
376 376 branch: default
377 377 commit: (clean) (secret)
378 378 update: 1 new changesets, 2 branch heads (merge)
379 379 phases: 3 draft, 3 secret
380 380 $ hg summary -R initialrepo --quiet
381 381 parent: 7:17a481b3bccb tip
382 382 update: 1 new changesets, 2 branch heads (merge)
383 383
384 384 Test revset
385 385
386 386 $ cd initialrepo
387 387 $ hglog -r 'public()'
388 388 0 0 A
389 389 1 0 B
390 390 $ hglog -r 'draft()'
391 391 2 1 C
392 392 3 1 D
393 393 6 1 B'
394 394 $ hglog -r 'secret()'
395 395 4 2 E
396 396 5 2 H
397 397 7 2 merge B' and E
398 398
399 399 test that phase are displayed in log at debug level
400 400
401 401 $ hg log --debug
402 402 changeset: 7:17a481b3bccb796c0521ae97903d81c52bfee4af
403 403 tag: tip
404 404 phase: secret
405 405 parent: 6:cf9fe039dfd67e829edf6522a45de057b5c86519
406 406 parent: 4:a603bfb5a83e312131cebcd05353c217d4d21dde
407 407 manifest: 7:5e724ffacba267b2ab726c91fc8b650710deaaa8
408 408 user: test
409 409 date: Thu Jan 01 00:00:00 1970 +0000
410 files+: C D E
411 410 extra: branch=default
412 411 description:
413 412 merge B' and E
414 413
415 414
416 415 changeset: 6:cf9fe039dfd67e829edf6522a45de057b5c86519
417 416 phase: draft
418 417 parent: 1:27547f69f25460a52fff66ad004e58da7ad3fb56
419 418 parent: -1:0000000000000000000000000000000000000000
420 419 manifest: 6:ab8bfef2392903058bf4ebb9e7746e8d7026b27a
421 420 user: test
422 421 date: Thu Jan 01 00:00:00 1970 +0000
423 422 files+: B'
424 423 extra: branch=default
425 424 description:
426 425 B'
427 426
428 427
429 428 changeset: 5:a030c6be5127abc010fcbff1851536552e6951a8
430 429 phase: secret
431 430 parent: 4:a603bfb5a83e312131cebcd05353c217d4d21dde
432 431 parent: -1:0000000000000000000000000000000000000000
433 432 manifest: 5:5c710aa854874fe3d5fa7192e77bdb314cc08b5a
434 433 user: test
435 434 date: Thu Jan 01 00:00:00 1970 +0000
436 435 files+: H
437 436 extra: branch=default
438 437 description:
439 438 H
440 439
441 440
442 441 changeset: 4:a603bfb5a83e312131cebcd05353c217d4d21dde
443 442 phase: secret
444 443 parent: 3:b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e
445 444 parent: -1:0000000000000000000000000000000000000000
446 445 manifest: 4:7173fd1c27119750b959e3a0f47ed78abe75d6dc
447 446 user: test
448 447 date: Thu Jan 01 00:00:00 1970 +0000
449 448 files+: E
450 449 extra: branch=default
451 450 description:
452 451 E
453 452
454 453
455 454 changeset: 3:b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e
456 455 phase: draft
457 456 parent: 2:f838bfaca5c7226600ebcfd84f3c3c13a28d3757
458 457 parent: -1:0000000000000000000000000000000000000000
459 458 manifest: 3:6e1f4c47ecb533ffd0c8e52cdc88afb6cd39e20c
460 459 user: test
461 460 date: Thu Jan 01 00:00:00 1970 +0000
462 461 files+: D
463 462 extra: branch=default
464 463 description:
465 464 D
466 465
467 466
468 467 changeset: 2:f838bfaca5c7226600ebcfd84f3c3c13a28d3757
469 468 phase: draft
470 469 parent: 1:27547f69f25460a52fff66ad004e58da7ad3fb56
471 470 parent: -1:0000000000000000000000000000000000000000
472 471 manifest: 2:66a5a01817fdf5239c273802b5b7618d051c89e4
473 472 user: test
474 473 date: Thu Jan 01 00:00:00 1970 +0000
475 474 files+: C
476 475 extra: branch=default
477 476 description:
478 477 C
479 478
480 479
481 480 changeset: 1:27547f69f25460a52fff66ad004e58da7ad3fb56
482 481 phase: public
483 482 parent: 0:4a2df7238c3b48766b5e22fafbb8a2f506ec8256
484 483 parent: -1:0000000000000000000000000000000000000000
485 484 manifest: 1:cb5cbbc1bfbf24cc34b9e8c16914e9caa2d2a7fd
486 485 user: test
487 486 date: Thu Jan 01 00:00:00 1970 +0000
488 487 files+: B
489 488 extra: branch=default
490 489 description:
491 490 B
492 491
493 492
494 493 changeset: 0:4a2df7238c3b48766b5e22fafbb8a2f506ec8256
495 494 phase: public
496 495 parent: -1:0000000000000000000000000000000000000000
497 496 parent: -1:0000000000000000000000000000000000000000
498 497 manifest: 0:007d8c9d88841325f5c6b06371b35b4e8a2b1a83
499 498 user: test
500 499 date: Thu Jan 01 00:00:00 1970 +0000
501 500 files+: A
502 501 extra: branch=default
503 502 description:
504 503 A
505 504
506 505
507 506
508 507
509 508 (Issue3707)
510 509 test invalid phase name
511 510
512 511 $ mkcommit I --config phases.new-commit='babar'
513 512 transaction abort!
514 513 rollback completed
515 514 config error: phases.new-commit: not a valid phase name ('babar')
516 515 [30]
517 516 Test phase command
518 517 ===================
519 518
520 519 initial picture
521 520
522 521 $ hg log -G --template "{rev} {phase} {desc}\n"
523 522 @ 7 secret merge B' and E
524 523 |\
525 524 | o 6 draft B'
526 525 | |
527 526 +---o 5 secret H
528 527 | |
529 528 o | 4 secret E
530 529 | |
531 530 o | 3 draft D
532 531 | |
533 532 o | 2 draft C
534 533 |/
535 534 o 1 public B
536 535 |
537 536 o 0 public A
538 537
539 538
540 539 display changesets phase
541 540
542 541 (mixing -r and plain rev specification)
543 542
544 543 $ hg phase 1::4 -r 7
545 544 1: public
546 545 2: draft
547 546 3: draft
548 547 4: secret
549 548 7: secret
550 549
551 550
552 551 move changeset forward
553 552
554 553 (with -r option)
555 554
556 555 $ hg phase --public -r 2
557 556 test-debug-phase: move rev 2: 1 -> 0
558 557 test-hook-close-phase: f838bfaca5c7226600ebcfd84f3c3c13a28d3757: draft -> public
559 558 $ hg log -G --template "{rev} {phase} {desc}\n"
560 559 @ 7 secret merge B' and E
561 560 |\
562 561 | o 6 draft B'
563 562 | |
564 563 +---o 5 secret H
565 564 | |
566 565 o | 4 secret E
567 566 | |
568 567 o | 3 draft D
569 568 | |
570 569 o | 2 public C
571 570 |/
572 571 o 1 public B
573 572 |
574 573 o 0 public A
575 574
576 575
577 576 move changeset backward
578 577
579 578 (without -r option)
580 579
581 580 $ hg phase --draft --force 2
582 581 test-debug-phase: move rev 2: 0 -> 1
583 582 test-hook-close-phase: f838bfaca5c7226600ebcfd84f3c3c13a28d3757: public -> draft
584 583 $ hg log -G --template "{rev} {phase} {desc}\n"
585 584 @ 7 secret merge B' and E
586 585 |\
587 586 | o 6 draft B'
588 587 | |
589 588 +---o 5 secret H
590 589 | |
591 590 o | 4 secret E
592 591 | |
593 592 o | 3 draft D
594 593 | |
595 594 o | 2 draft C
596 595 |/
597 596 o 1 public B
598 597 |
599 598 o 0 public A
600 599
601 600
602 601 move changeset forward and backward
603 602
604 603 $ hg phase --draft --force 1::4
605 604 test-debug-phase: move rev 1: 0 -> 1
606 605 test-debug-phase: move rev 4: 2 -> 1
607 606 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: public -> draft
608 607 test-hook-close-phase: a603bfb5a83e312131cebcd05353c217d4d21dde: secret -> draft
609 608 $ hg log -G --template "{rev} {phase} {desc}\n"
610 609 @ 7 secret merge B' and E
611 610 |\
612 611 | o 6 draft B'
613 612 | |
614 613 +---o 5 secret H
615 614 | |
616 615 o | 4 draft E
617 616 | |
618 617 o | 3 draft D
619 618 | |
620 619 o | 2 draft C
621 620 |/
622 621 o 1 draft B
623 622 |
624 623 o 0 public A
625 624
626 625 test partial failure
627 626
628 627 $ hg phase --public 7
629 628 test-debug-phase: move rev 1: 1 -> 0
630 629 test-debug-phase: move rev 2: 1 -> 0
631 630 test-debug-phase: move rev 3: 1 -> 0
632 631 test-debug-phase: move rev 4: 1 -> 0
633 632 test-debug-phase: move rev 6: 1 -> 0
634 633 test-debug-phase: move rev 7: 2 -> 0
635 634 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: draft -> public
636 635 test-hook-close-phase: f838bfaca5c7226600ebcfd84f3c3c13a28d3757: draft -> public
637 636 test-hook-close-phase: b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e: draft -> public
638 637 test-hook-close-phase: a603bfb5a83e312131cebcd05353c217d4d21dde: draft -> public
639 638 test-hook-close-phase: cf9fe039dfd67e829edf6522a45de057b5c86519: draft -> public
640 639 test-hook-close-phase: 17a481b3bccb796c0521ae97903d81c52bfee4af: secret -> public
641 640 $ hg phase --draft '5 or 7'
642 641 test-debug-phase: move rev 5: 2 -> 1
643 642 test-hook-close-phase: a030c6be5127abc010fcbff1851536552e6951a8: secret -> draft
644 643 cannot move 1 changesets to a higher phase, use --force
645 644 phase changed for 1 changesets
646 645 [1]
647 646 $ hg log -G --template "{rev} {phase} {desc}\n"
648 647 @ 7 public merge B' and E
649 648 |\
650 649 | o 6 public B'
651 650 | |
652 651 +---o 5 draft H
653 652 | |
654 653 o | 4 public E
655 654 | |
656 655 o | 3 public D
657 656 | |
658 657 o | 2 public C
659 658 |/
660 659 o 1 public B
661 660 |
662 661 o 0 public A
663 662
664 663
665 664 test complete failure
666 665
667 666 $ hg phase --draft 7
668 667 cannot move 1 changesets to a higher phase, use --force
669 668 no phases changed
670 669 [1]
671 670
672 671 $ cd ..
673 672
674 673 test hidden changeset are not cloned as public (issue3935)
675 674
676 675 $ cd initialrepo
677 676
678 677 (enabling evolution)
679 678 $ cat >> $HGRCPATH << EOF
680 679 > [experimental]
681 680 > evolution.createmarkers=True
682 681 > EOF
683 682
684 683 (making a changeset hidden; H in that case)
685 684 $ hg debugobsolete `hg id --debug -r 5`
686 685 1 new obsolescence markers
687 686 obsoleted 1 changesets
688 687
689 688 $ cd ..
690 689 $ hg clone initialrepo clonewithobs
691 690 requesting all changes
692 691 adding changesets
693 692 adding manifests
694 693 adding file changes
695 694 added 7 changesets with 6 changes to 6 files
696 695 new changesets 4a2df7238c3b:17a481b3bccb
697 696 test-debug-phase: new rev 0: x -> 0
698 697 test-debug-phase: new rev 1: x -> 0
699 698 test-debug-phase: new rev 2: x -> 0
700 699 test-debug-phase: new rev 3: x -> 0
701 700 test-debug-phase: new rev 4: x -> 0
702 701 test-debug-phase: new rev 5: x -> 0
703 702 test-debug-phase: new rev 6: x -> 0
704 703 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: -> public
705 704 test-hook-close-phase: 27547f69f25460a52fff66ad004e58da7ad3fb56: -> public
706 705 test-hook-close-phase: f838bfaca5c7226600ebcfd84f3c3c13a28d3757: -> public
707 706 test-hook-close-phase: b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e: -> public
708 707 test-hook-close-phase: a603bfb5a83e312131cebcd05353c217d4d21dde: -> public
709 708 test-hook-close-phase: cf9fe039dfd67e829edf6522a45de057b5c86519: -> public
710 709 test-hook-close-phase: 17a481b3bccb796c0521ae97903d81c52bfee4af: -> public
711 710 updating to branch default
712 711 6 files updated, 0 files merged, 0 files removed, 0 files unresolved
713 712 $ cd clonewithobs
714 713 $ hg log -G --template "{rev} {phase} {desc}\n"
715 714 @ 6 public merge B' and E
716 715 |\
717 716 | o 5 public B'
718 717 | |
719 718 o | 4 public E
720 719 | |
721 720 o | 3 public D
722 721 | |
723 722 o | 2 public C
724 723 |/
725 724 o 1 public B
726 725 |
727 726 o 0 public A
728 727
729 728
730 729 test verify repo containing hidden changesets, which should not abort just
731 730 because repo.cancopy() is False
732 731
733 732 $ cd ../initialrepo
734 733 $ hg verify
735 734 checking changesets
736 735 checking manifests
737 736 crosschecking files in changesets and manifests
738 737 checking files
739 738 checked 8 changesets with 7 changes to 7 files
740 739
741 740 $ cd ..
742 741
743 742 check whether HG_PENDING makes pending changes only in related
744 743 repositories visible to an external hook.
745 744
746 745 (emulate a transaction running concurrently by copied
747 746 .hg/phaseroots.pending in subsequent test)
748 747
749 748 $ cat > $TESTTMP/savepending.sh <<EOF
750 749 > cp .hg/store/phaseroots.pending .hg/store/phaseroots.pending.saved
751 750 > exit 1 # to avoid changing phase for subsequent tests
752 751 > EOF
753 752 $ cd push-dest
754 753 $ hg phase 6
755 754 6: draft
756 755 $ hg --config hooks.pretxnclose="sh $TESTTMP/savepending.sh" phase -f -s 6
757 756 transaction abort!
758 757 rollback completed
759 758 abort: pretxnclose hook exited with status 1
760 759 [40]
761 760 $ cp .hg/store/phaseroots.pending.saved .hg/store/phaseroots.pending
762 761
763 762 (check (in)visibility of phaseroot while transaction running in repo)
764 763
765 764 $ cat > $TESTTMP/checkpending.sh <<EOF
766 765 > echo '@initialrepo'
767 766 > hg -R "$TESTTMP/initialrepo" phase 7
768 767 > echo '@push-dest'
769 768 > hg -R "$TESTTMP/push-dest" phase 6
770 769 > exit 1 # to avoid changing phase for subsequent tests
771 770 > EOF
772 771 $ cd ../initialrepo
773 772 $ hg phase 7
774 773 7: public
775 774 $ hg --config hooks.pretxnclose="sh $TESTTMP/checkpending.sh" phase -f -s 7
776 775 @initialrepo
777 776 7: secret
778 777 @push-dest
779 778 6: draft
780 779 transaction abort!
781 780 rollback completed
782 781 abort: pretxnclose hook exited with status 1
783 782 [40]
784 783
785 784 Check that pretxnclose-phase hook can control phase movement
786 785
787 786 $ hg phase --force b3325c91a4d9 --secret
788 787 test-debug-phase: move rev 3: 0 -> 2
789 788 test-debug-phase: move rev 4: 0 -> 2
790 789 test-debug-phase: move rev 5: 1 -> 2
791 790 test-debug-phase: move rev 7: 0 -> 2
792 791 test-hook-close-phase: b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e: public -> secret
793 792 test-hook-close-phase: a603bfb5a83e312131cebcd05353c217d4d21dde: public -> secret
794 793 test-hook-close-phase: a030c6be5127abc010fcbff1851536552e6951a8: draft -> secret
795 794 test-hook-close-phase: 17a481b3bccb796c0521ae97903d81c52bfee4af: public -> secret
796 795 $ hg log -G -T phases
797 796 @ changeset: 7:17a481b3bccb
798 797 |\ tag: tip
799 798 | | phase: secret
800 799 | | parent: 6:cf9fe039dfd6
801 800 | | parent: 4:a603bfb5a83e
802 801 | | user: test
803 802 | | date: Thu Jan 01 00:00:00 1970 +0000
804 803 | | summary: merge B' and E
805 804 | |
806 805 | o changeset: 6:cf9fe039dfd6
807 806 | | phase: public
808 807 | | parent: 1:27547f69f254
809 808 | | user: test
810 809 | | date: Thu Jan 01 00:00:00 1970 +0000
811 810 | | summary: B'
812 811 | |
813 812 o | changeset: 4:a603bfb5a83e
814 813 | | phase: secret
815 814 | | user: test
816 815 | | date: Thu Jan 01 00:00:00 1970 +0000
817 816 | | summary: E
818 817 | |
819 818 o | changeset: 3:b3325c91a4d9
820 819 | | phase: secret
821 820 | | user: test
822 821 | | date: Thu Jan 01 00:00:00 1970 +0000
823 822 | | summary: D
824 823 | |
825 824 o | changeset: 2:f838bfaca5c7
826 825 |/ phase: public
827 826 | user: test
828 827 | date: Thu Jan 01 00:00:00 1970 +0000
829 828 | summary: C
830 829 |
831 830 o changeset: 1:27547f69f254
832 831 | phase: public
833 832 | user: test
834 833 | date: Thu Jan 01 00:00:00 1970 +0000
835 834 | summary: B
836 835 |
837 836 o changeset: 0:4a2df7238c3b
838 837 phase: public
839 838 user: test
840 839 date: Thu Jan 01 00:00:00 1970 +0000
841 840 summary: A
842 841
843 842
844 843 Install a hook that prevent b3325c91a4d9 to become public
845 844
846 845 $ cat >> .hg/hgrc << EOF
847 846 > [hooks]
848 847 > pretxnclose-phase.nopublish_D = sh -c "(echo \$HG_NODE| grep -v b3325c91a4d9>/dev/null) || [ 'public' != \$HG_PHASE ]"
849 848 > EOF
850 849
851 850 Try various actions. only the draft move should succeed
852 851
853 852 $ hg phase --public b3325c91a4d9
854 853 transaction abort!
855 854 rollback completed
856 855 abort: pretxnclose-phase.nopublish_D hook exited with status 1
857 856 [40]
858 857 $ hg phase --public a603bfb5a83e
859 858 transaction abort!
860 859 rollback completed
861 860 abort: pretxnclose-phase.nopublish_D hook exited with status 1
862 861 [40]
863 862 $ hg phase --draft 17a481b3bccb
864 863 test-debug-phase: move rev 3: 2 -> 1
865 864 test-debug-phase: move rev 4: 2 -> 1
866 865 test-debug-phase: move rev 7: 2 -> 1
867 866 test-hook-close-phase: b3325c91a4d916bcc4cdc83ea3fe4ece46a42f6e: secret -> draft
868 867 test-hook-close-phase: a603bfb5a83e312131cebcd05353c217d4d21dde: secret -> draft
869 868 test-hook-close-phase: 17a481b3bccb796c0521ae97903d81c52bfee4af: secret -> draft
870 869 $ hg phase --public 17a481b3bccb
871 870 transaction abort!
872 871 rollback completed
873 872 abort: pretxnclose-phase.nopublish_D hook exited with status 1
874 873 [40]
875 874
876 875 $ cd ..
877 876
878 877 Test for the "internal" phase
879 878 =============================
880 879
881 880 Check we deny its usage on older repository
882 881
883 882 $ hg init no-internal-phase --config format.internal-phase=no
884 883 $ cd no-internal-phase
885 884 $ hg debugrequires | grep internal-phase
886 885 [1]
887 886 $ echo X > X
888 887 $ hg add X
889 888 $ hg status
890 889 A X
891 890 $ hg --config "phases.new-commit=internal" commit -m "my test internal commit" 2>&1 | grep ProgrammingError
892 891 ** ProgrammingError: this repository does not support the internal phase
893 892 raise error.ProgrammingError(msg) (no-pyoxidizer !)
894 893 *ProgrammingError: this repository does not support the internal phase (glob)
895 894 $ hg --config "phases.new-commit=archived" commit -m "my test archived commit" 2>&1 | grep ProgrammingError
896 895 ** ProgrammingError: this repository does not support the archived phase
897 896 raise error.ProgrammingError(msg) (no-pyoxidizer !)
898 897 *ProgrammingError: this repository does not support the archived phase (glob)
899 898
900 899 $ cd ..
901 900
902 901 Check it works fine with repository that supports it.
903 902
904 903 $ hg init internal-phase --config format.internal-phase=yes
905 904 $ cd internal-phase
906 905 $ hg debugrequires | grep internal-phase
907 906 internal-phase
908 907 $ mkcommit A
909 908 test-debug-phase: new rev 0: x -> 1
910 909 test-hook-close-phase: 4a2df7238c3b48766b5e22fafbb8a2f506ec8256: -> draft
911 910
912 911 Commit an internal changesets
913 912
914 913 $ echo B > B
915 914 $ hg add B
916 915 $ hg status
917 916 A B
918 917 $ hg --config "phases.new-commit=internal" commit -m "my test internal commit"
919 918 test-debug-phase: new rev 1: x -> 96
920 919 test-hook-close-phase: c01c42dffc7f81223397e99652a0703f83e1c5ea: -> internal
921 920
922 921 The changeset is a working parent descendant.
923 922 Per the usual visibility rules, it is made visible.
924 923
925 924 $ hg log -G -l 3
926 925 @ changeset: 1:c01c42dffc7f
927 926 | tag: tip
928 927 | user: test
929 928 | date: Thu Jan 01 00:00:00 1970 +0000
930 929 | summary: my test internal commit
931 930 |
932 931 o changeset: 0:4a2df7238c3b
933 932 user: test
934 933 date: Thu Jan 01 00:00:00 1970 +0000
935 934 summary: A
936 935
937 936
938 937 Commit is hidden as expected
939 938
940 939 $ hg up 0
941 940 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
942 941 $ hg log -G
943 942 @ changeset: 0:4a2df7238c3b
944 943 tag: tip
945 944 user: test
946 945 date: Thu Jan 01 00:00:00 1970 +0000
947 946 summary: A
948 947
949 948
950 949 Test for archived phase
951 950 -----------------------
952 951
953 952 Commit an archived changesets
954 953
955 954 $ echo B > B
956 955 $ hg add B
957 956 $ hg status
958 957 A B
959 958 $ hg --config "phases.new-commit=archived" commit -m "my test archived commit"
960 959 test-debug-phase: new rev 2: x -> 32
961 960 test-hook-close-phase: 8df5997c3361518f733d1ae67cd3adb9b0eaf125: -> archived
962 961
963 962 The changeset is a working parent descendant.
964 963 Per the usual visibility rules, it is made visible.
965 964
966 965 $ hg log -G -l 3
967 966 @ changeset: 2:8df5997c3361
968 967 | tag: tip
969 968 | parent: 0:4a2df7238c3b
970 969 | user: test
971 970 | date: Thu Jan 01 00:00:00 1970 +0000
972 971 | summary: my test archived commit
973 972 |
974 973 o changeset: 0:4a2df7238c3b
975 974 user: test
976 975 date: Thu Jan 01 00:00:00 1970 +0000
977 976 summary: A
978 977
979 978
980 979 Commit is hidden as expected
981 980
982 981 $ hg up 0
983 982 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
984 983 $ hg log -G
985 984 @ changeset: 0:4a2df7238c3b
986 985 tag: tip
987 986 user: test
988 987 date: Thu Jan 01 00:00:00 1970 +0000
989 988 summary: A
990 989
991 990 $ cd ..
992 991
993 992 Recommitting an exact match of a public commit shouldn't change it to
994 993 draft:
995 994
996 995 $ cd initialrepo
997 996 $ hg phase -r 2
998 997 2: public
999 998 $ hg up -C 1
1000 999 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
1001 1000 $ mkcommit C
1002 1001 warning: commit already existed in the repository!
1003 1002 $ hg phase -r 2
1004 1003 2: public
1005 1004
1006 1005 Same, but for secret:
1007 1006
1008 1007 $ hg up 7
1009 1008 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
1010 1009 $ mkcommit F -s
1011 1010 test-debug-phase: new rev 8: x -> 2
1012 1011 test-hook-close-phase: de414268ec5ce2330c590b942fbb5ff0b0ca1a0a: -> secret
1013 1012 $ hg up 7
1014 1013 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
1015 1014 $ hg phase
1016 1015 7: draft
1017 1016 $ mkcommit F
1018 1017 test-debug-phase: new rev 8: x -> 2
1019 1018 warning: commit already existed in the repository!
1020 1019 test-hook-close-phase: de414268ec5ce2330c590b942fbb5ff0b0ca1a0a: -> secret
1021 1020 $ hg phase -r tip
1022 1021 8: secret
1023 1022
1024 1023 But what about obsoleted changesets?
1025 1024
1026 1025 $ hg up 4
1027 1026 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
1028 1027 $ mkcommit H
1029 1028 test-debug-phase: new rev 5: x -> 2
1030 1029 warning: commit already existed in the repository!
1031 1030 test-hook-close-phase: a030c6be5127abc010fcbff1851536552e6951a8: -> secret
1032 1031 $ hg phase -r 5
1033 1032 5: secret
1034 1033 $ hg par
1035 1034 changeset: 5:a030c6be5127
1036 1035 user: test
1037 1036 date: Thu Jan 01 00:00:00 1970 +0000
1038 1037 obsolete: pruned
1039 1038 summary: H
1040 1039
1041 1040 $ hg up tip
1042 1041 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
1043 1042 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now