##// END OF EJS Templates
log: map None rev to wdirrev when filtering revisions with --line-range...
Denis Laxalde -
r44173:d1b9d2c6 stable
parent child Browse files
Show More
@@ -1,1070 +1,1072 b''
1 1 # logcmdutil.py - utility for log-like commands
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 itertools
11 11 import os
12 12 import posixpath
13 13
14 14 from .i18n import _
15 15 from .node import (
16 16 nullid,
17 17 wdirid,
18 18 wdirrev,
19 19 )
20 20
21 21 from . import (
22 22 dagop,
23 23 error,
24 24 formatter,
25 25 graphmod,
26 26 match as matchmod,
27 27 mdiff,
28 28 patch,
29 29 pathutil,
30 30 pycompat,
31 31 revset,
32 32 revsetlang,
33 33 scmutil,
34 34 smartset,
35 35 templatekw,
36 36 templater,
37 37 util,
38 38 )
39 39 from .utils import (
40 40 dateutil,
41 41 stringutil,
42 42 )
43 43
44 44
45 45 def getlimit(opts):
46 46 """get the log limit according to option -l/--limit"""
47 47 limit = opts.get(b'limit')
48 48 if limit:
49 49 try:
50 50 limit = int(limit)
51 51 except ValueError:
52 52 raise error.Abort(_(b'limit must be a positive integer'))
53 53 if limit <= 0:
54 54 raise error.Abort(_(b'limit must be positive'))
55 55 else:
56 56 limit = None
57 57 return limit
58 58
59 59
60 60 def diffordiffstat(
61 61 ui,
62 62 repo,
63 63 diffopts,
64 64 node1,
65 65 node2,
66 66 match,
67 67 changes=None,
68 68 stat=False,
69 69 fp=None,
70 70 graphwidth=0,
71 71 prefix=b'',
72 72 root=b'',
73 73 listsubrepos=False,
74 74 hunksfilterfn=None,
75 75 ):
76 76 '''show diff or diffstat.'''
77 77 ctx1 = repo[node1]
78 78 ctx2 = repo[node2]
79 79 if root:
80 80 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
81 81 else:
82 82 relroot = b''
83 83 copysourcematch = None
84 84
85 85 def compose(f, g):
86 86 return lambda x: f(g(x))
87 87
88 88 def pathfn(f):
89 89 return posixpath.join(prefix, f)
90 90
91 91 if relroot != b'':
92 92 # XXX relative roots currently don't work if the root is within a
93 93 # subrepo
94 94 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
95 95 uirelroot = uipathfn(pathfn(relroot))
96 96 relroot += b'/'
97 97 for matchroot in match.files():
98 98 if not matchroot.startswith(relroot):
99 99 ui.warn(
100 100 _(b'warning: %s not inside relative root %s\n')
101 101 % (uipathfn(pathfn(matchroot)), uirelroot)
102 102 )
103 103
104 104 relrootmatch = scmutil.match(ctx2, pats=[relroot], default=b'path')
105 105 match = matchmod.intersectmatchers(match, relrootmatch)
106 106 copysourcematch = relrootmatch
107 107
108 108 checkroot = repo.ui.configbool(
109 109 b'devel', b'all-warnings'
110 110 ) or repo.ui.configbool(b'devel', b'check-relroot')
111 111
112 112 def relrootpathfn(f):
113 113 if checkroot and not f.startswith(relroot):
114 114 raise AssertionError(
115 115 b"file %s doesn't start with relroot %s" % (f, relroot)
116 116 )
117 117 return f[len(relroot) :]
118 118
119 119 pathfn = compose(relrootpathfn, pathfn)
120 120
121 121 if stat:
122 122 diffopts = diffopts.copy(context=0, noprefix=False)
123 123 width = 80
124 124 if not ui.plain():
125 125 width = ui.termwidth() - graphwidth
126 126 # If an explicit --root was given, don't respect ui.relative-paths
127 127 if not relroot:
128 128 pathfn = compose(scmutil.getuipathfn(repo), pathfn)
129 129
130 130 chunks = ctx2.diff(
131 131 ctx1,
132 132 match,
133 133 changes,
134 134 opts=diffopts,
135 135 pathfn=pathfn,
136 136 copysourcematch=copysourcematch,
137 137 hunksfilterfn=hunksfilterfn,
138 138 )
139 139
140 140 if fp is not None or ui.canwritewithoutlabels():
141 141 out = fp or ui
142 142 if stat:
143 143 chunks = [patch.diffstat(util.iterlines(chunks), width=width)]
144 144 for chunk in util.filechunkiter(util.chunkbuffer(chunks)):
145 145 out.write(chunk)
146 146 else:
147 147 if stat:
148 148 chunks = patch.diffstatui(util.iterlines(chunks), width=width)
149 149 else:
150 150 chunks = patch.difflabel(
151 151 lambda chunks, **kwargs: chunks, chunks, opts=diffopts
152 152 )
153 153 if ui.canbatchlabeledwrites():
154 154
155 155 def gen():
156 156 for chunk, label in chunks:
157 157 yield ui.label(chunk, label=label)
158 158
159 159 for chunk in util.filechunkiter(util.chunkbuffer(gen())):
160 160 ui.write(chunk)
161 161 else:
162 162 for chunk, label in chunks:
163 163 ui.write(chunk, label=label)
164 164
165 165 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
166 166 tempnode2 = node2
167 167 try:
168 168 if node2 is not None:
169 169 tempnode2 = ctx2.substate[subpath][1]
170 170 except KeyError:
171 171 # A subrepo that existed in node1 was deleted between node1 and
172 172 # node2 (inclusive). Thus, ctx2's substate won't contain that
173 173 # subpath. The best we can do is to ignore it.
174 174 tempnode2 = None
175 175 submatch = matchmod.subdirmatcher(subpath, match)
176 176 subprefix = repo.wvfs.reljoin(prefix, subpath)
177 177 if listsubrepos or match.exact(subpath) or any(submatch.files()):
178 178 sub.diff(
179 179 ui,
180 180 diffopts,
181 181 tempnode2,
182 182 submatch,
183 183 changes=changes,
184 184 stat=stat,
185 185 fp=fp,
186 186 prefix=subprefix,
187 187 )
188 188
189 189
190 190 class changesetdiffer(object):
191 191 """Generate diff of changeset with pre-configured filtering functions"""
192 192
193 193 def _makefilematcher(self, ctx):
194 194 return scmutil.matchall(ctx.repo())
195 195
196 196 def _makehunksfilter(self, ctx):
197 197 return None
198 198
199 199 def showdiff(self, ui, ctx, diffopts, graphwidth=0, stat=False):
200 200 repo = ctx.repo()
201 201 node = ctx.node()
202 202 prev = ctx.p1().node()
203 203 diffordiffstat(
204 204 ui,
205 205 repo,
206 206 diffopts,
207 207 prev,
208 208 node,
209 209 match=self._makefilematcher(ctx),
210 210 stat=stat,
211 211 graphwidth=graphwidth,
212 212 hunksfilterfn=self._makehunksfilter(ctx),
213 213 )
214 214
215 215
216 216 def changesetlabels(ctx):
217 217 labels = [b'log.changeset', b'changeset.%s' % ctx.phasestr()]
218 218 if ctx.obsolete():
219 219 labels.append(b'changeset.obsolete')
220 220 if ctx.isunstable():
221 221 labels.append(b'changeset.unstable')
222 222 for instability in ctx.instabilities():
223 223 labels.append(b'instability.%s' % instability)
224 224 return b' '.join(labels)
225 225
226 226
227 227 class changesetprinter(object):
228 228 '''show changeset information when templating not requested.'''
229 229
230 230 def __init__(self, ui, repo, differ=None, diffopts=None, buffered=False):
231 231 self.ui = ui
232 232 self.repo = repo
233 233 self.buffered = buffered
234 234 self._differ = differ or changesetdiffer()
235 235 self._diffopts = patch.diffallopts(ui, diffopts)
236 236 self._includestat = diffopts and diffopts.get(b'stat')
237 237 self._includediff = diffopts and diffopts.get(b'patch')
238 238 self.header = {}
239 239 self.hunk = {}
240 240 self.lastheader = None
241 241 self.footer = None
242 242 self._columns = templatekw.getlogcolumns()
243 243
244 244 def flush(self, ctx):
245 245 rev = ctx.rev()
246 246 if rev in self.header:
247 247 h = self.header[rev]
248 248 if h != self.lastheader:
249 249 self.lastheader = h
250 250 self.ui.write(h)
251 251 del self.header[rev]
252 252 if rev in self.hunk:
253 253 self.ui.write(self.hunk[rev])
254 254 del self.hunk[rev]
255 255
256 256 def close(self):
257 257 if self.footer:
258 258 self.ui.write(self.footer)
259 259
260 260 def show(self, ctx, copies=None, **props):
261 261 props = pycompat.byteskwargs(props)
262 262 if self.buffered:
263 263 self.ui.pushbuffer(labeled=True)
264 264 self._show(ctx, copies, props)
265 265 self.hunk[ctx.rev()] = self.ui.popbuffer()
266 266 else:
267 267 self._show(ctx, copies, props)
268 268
269 269 def _show(self, ctx, copies, props):
270 270 '''show a single changeset or file revision'''
271 271 changenode = ctx.node()
272 272 graphwidth = props.get(b'graphwidth', 0)
273 273
274 274 if self.ui.quiet:
275 275 self.ui.write(
276 276 b"%s\n" % scmutil.formatchangeid(ctx), label=b'log.node'
277 277 )
278 278 return
279 279
280 280 columns = self._columns
281 281 self.ui.write(
282 282 columns[b'changeset'] % scmutil.formatchangeid(ctx),
283 283 label=changesetlabels(ctx),
284 284 )
285 285
286 286 # branches are shown first before any other names due to backwards
287 287 # compatibility
288 288 branch = ctx.branch()
289 289 # don't show the default branch name
290 290 if branch != b'default':
291 291 self.ui.write(columns[b'branch'] % branch, label=b'log.branch')
292 292
293 293 for nsname, ns in pycompat.iteritems(self.repo.names):
294 294 # branches has special logic already handled above, so here we just
295 295 # skip it
296 296 if nsname == b'branches':
297 297 continue
298 298 # we will use the templatename as the color name since those two
299 299 # should be the same
300 300 for name in ns.names(self.repo, changenode):
301 301 self.ui.write(ns.logfmt % name, label=b'log.%s' % ns.colorname)
302 302 if self.ui.debugflag:
303 303 self.ui.write(
304 304 columns[b'phase'] % ctx.phasestr(), label=b'log.phase'
305 305 )
306 306 for pctx in scmutil.meaningfulparents(self.repo, ctx):
307 307 label = b'log.parent changeset.%s' % pctx.phasestr()
308 308 self.ui.write(
309 309 columns[b'parent'] % scmutil.formatchangeid(pctx), label=label
310 310 )
311 311
312 312 if self.ui.debugflag:
313 313 mnode = ctx.manifestnode()
314 314 if mnode is None:
315 315 mnode = wdirid
316 316 mrev = wdirrev
317 317 else:
318 318 mrev = self.repo.manifestlog.rev(mnode)
319 319 self.ui.write(
320 320 columns[b'manifest']
321 321 % scmutil.formatrevnode(self.ui, mrev, mnode),
322 322 label=b'ui.debug log.manifest',
323 323 )
324 324 self.ui.write(columns[b'user'] % ctx.user(), label=b'log.user')
325 325 self.ui.write(
326 326 columns[b'date'] % dateutil.datestr(ctx.date()), label=b'log.date'
327 327 )
328 328
329 329 if ctx.isunstable():
330 330 instabilities = ctx.instabilities()
331 331 self.ui.write(
332 332 columns[b'instability'] % b', '.join(instabilities),
333 333 label=b'log.instability',
334 334 )
335 335
336 336 elif ctx.obsolete():
337 337 self._showobsfate(ctx)
338 338
339 339 self._exthook(ctx)
340 340
341 341 if self.ui.debugflag:
342 342 files = ctx.p1().status(ctx)[:3]
343 343 for key, value in zip([b'files', b'files+', b'files-'], files):
344 344 if value:
345 345 self.ui.write(
346 346 columns[key] % b" ".join(value),
347 347 label=b'ui.debug log.files',
348 348 )
349 349 elif ctx.files() and self.ui.verbose:
350 350 self.ui.write(
351 351 columns[b'files'] % b" ".join(ctx.files()),
352 352 label=b'ui.note log.files',
353 353 )
354 354 if copies and self.ui.verbose:
355 355 copies = [b'%s (%s)' % c for c in copies]
356 356 self.ui.write(
357 357 columns[b'copies'] % b' '.join(copies),
358 358 label=b'ui.note log.copies',
359 359 )
360 360
361 361 extra = ctx.extra()
362 362 if extra and self.ui.debugflag:
363 363 for key, value in sorted(extra.items()):
364 364 self.ui.write(
365 365 columns[b'extra'] % (key, stringutil.escapestr(value)),
366 366 label=b'ui.debug log.extra',
367 367 )
368 368
369 369 description = ctx.description().strip()
370 370 if description:
371 371 if self.ui.verbose:
372 372 self.ui.write(
373 373 _(b"description:\n"), label=b'ui.note log.description'
374 374 )
375 375 self.ui.write(description, label=b'ui.note log.description')
376 376 self.ui.write(b"\n\n")
377 377 else:
378 378 self.ui.write(
379 379 columns[b'summary'] % description.splitlines()[0],
380 380 label=b'log.summary',
381 381 )
382 382 self.ui.write(b"\n")
383 383
384 384 self._showpatch(ctx, graphwidth)
385 385
386 386 def _showobsfate(self, ctx):
387 387 # TODO: do not depend on templater
388 388 tres = formatter.templateresources(self.repo.ui, self.repo)
389 389 t = formatter.maketemplater(
390 390 self.repo.ui,
391 391 b'{join(obsfate, "\n")}',
392 392 defaults=templatekw.keywords,
393 393 resources=tres,
394 394 )
395 395 obsfate = t.renderdefault({b'ctx': ctx}).splitlines()
396 396
397 397 if obsfate:
398 398 for obsfateline in obsfate:
399 399 self.ui.write(
400 400 self._columns[b'obsolete'] % obsfateline,
401 401 label=b'log.obsfate',
402 402 )
403 403
404 404 def _exthook(self, ctx):
405 405 '''empty method used by extension as a hook point
406 406 '''
407 407
408 408 def _showpatch(self, ctx, graphwidth=0):
409 409 if self._includestat:
410 410 self._differ.showdiff(
411 411 self.ui, ctx, self._diffopts, graphwidth, stat=True
412 412 )
413 413 if self._includestat and self._includediff:
414 414 self.ui.write(b"\n")
415 415 if self._includediff:
416 416 self._differ.showdiff(
417 417 self.ui, ctx, self._diffopts, graphwidth, stat=False
418 418 )
419 419 if self._includestat or self._includediff:
420 420 self.ui.write(b"\n")
421 421
422 422
423 423 class changesetformatter(changesetprinter):
424 424 """Format changeset information by generic formatter"""
425 425
426 426 def __init__(
427 427 self, ui, repo, fm, differ=None, diffopts=None, buffered=False
428 428 ):
429 429 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
430 430 self._diffopts = patch.difffeatureopts(ui, diffopts, git=True)
431 431 self._fm = fm
432 432
433 433 def close(self):
434 434 self._fm.end()
435 435
436 436 def _show(self, ctx, copies, props):
437 437 '''show a single changeset or file revision'''
438 438 fm = self._fm
439 439 fm.startitem()
440 440 fm.context(ctx=ctx)
441 441 fm.data(rev=scmutil.intrev(ctx), node=fm.hexfunc(scmutil.binnode(ctx)))
442 442
443 443 datahint = fm.datahint()
444 444 if self.ui.quiet and not datahint:
445 445 return
446 446
447 447 fm.data(
448 448 branch=ctx.branch(),
449 449 phase=ctx.phasestr(),
450 450 user=ctx.user(),
451 451 date=fm.formatdate(ctx.date()),
452 452 desc=ctx.description(),
453 453 bookmarks=fm.formatlist(ctx.bookmarks(), name=b'bookmark'),
454 454 tags=fm.formatlist(ctx.tags(), name=b'tag'),
455 455 parents=fm.formatlist(
456 456 [fm.hexfunc(c.node()) for c in ctx.parents()], name=b'node'
457 457 ),
458 458 )
459 459
460 460 if self.ui.debugflag or b'manifest' in datahint:
461 461 fm.data(manifest=fm.hexfunc(ctx.manifestnode() or wdirid))
462 462 if self.ui.debugflag or b'extra' in datahint:
463 463 fm.data(extra=fm.formatdict(ctx.extra()))
464 464
465 465 if (
466 466 self.ui.debugflag
467 467 or b'modified' in datahint
468 468 or b'added' in datahint
469 469 or b'removed' in datahint
470 470 ):
471 471 files = ctx.p1().status(ctx)
472 472 fm.data(
473 473 modified=fm.formatlist(files[0], name=b'file'),
474 474 added=fm.formatlist(files[1], name=b'file'),
475 475 removed=fm.formatlist(files[2], name=b'file'),
476 476 )
477 477
478 478 verbose = not self.ui.debugflag and self.ui.verbose
479 479 if verbose or b'files' in datahint:
480 480 fm.data(files=fm.formatlist(ctx.files(), name=b'file'))
481 481 if verbose and copies or b'copies' in datahint:
482 482 fm.data(
483 483 copies=fm.formatdict(copies or {}, key=b'name', value=b'source')
484 484 )
485 485
486 486 if self._includestat or b'diffstat' in datahint:
487 487 self.ui.pushbuffer()
488 488 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=True)
489 489 fm.data(diffstat=self.ui.popbuffer())
490 490 if self._includediff or b'diff' in datahint:
491 491 self.ui.pushbuffer()
492 492 self._differ.showdiff(self.ui, ctx, self._diffopts, stat=False)
493 493 fm.data(diff=self.ui.popbuffer())
494 494
495 495
496 496 class changesettemplater(changesetprinter):
497 497 '''format changeset information.
498 498
499 499 Note: there are a variety of convenience functions to build a
500 500 changesettemplater for common cases. See functions such as:
501 501 maketemplater, changesetdisplayer, buildcommittemplate, or other
502 502 functions that use changesest_templater.
503 503 '''
504 504
505 505 # Arguments before "buffered" used to be positional. Consider not
506 506 # adding/removing arguments before "buffered" to not break callers.
507 507 def __init__(
508 508 self, ui, repo, tmplspec, differ=None, diffopts=None, buffered=False
509 509 ):
510 510 changesetprinter.__init__(self, ui, repo, differ, diffopts, buffered)
511 511 # tres is shared with _graphnodeformatter()
512 512 self._tresources = tres = formatter.templateresources(ui, repo)
513 513 self.t = formatter.loadtemplater(
514 514 ui,
515 515 tmplspec,
516 516 defaults=templatekw.keywords,
517 517 resources=tres,
518 518 cache=templatekw.defaulttempl,
519 519 )
520 520 self._counter = itertools.count()
521 521
522 522 self._tref = tmplspec.ref
523 523 self._parts = {
524 524 b'header': b'',
525 525 b'footer': b'',
526 526 tmplspec.ref: tmplspec.ref,
527 527 b'docheader': b'',
528 528 b'docfooter': b'',
529 529 b'separator': b'',
530 530 }
531 531 if tmplspec.mapfile:
532 532 # find correct templates for current mode, for backward
533 533 # compatibility with 'log -v/-q/--debug' using a mapfile
534 534 tmplmodes = [
535 535 (True, b''),
536 536 (self.ui.verbose, b'_verbose'),
537 537 (self.ui.quiet, b'_quiet'),
538 538 (self.ui.debugflag, b'_debug'),
539 539 ]
540 540 for mode, postfix in tmplmodes:
541 541 for t in self._parts:
542 542 cur = t + postfix
543 543 if mode and cur in self.t:
544 544 self._parts[t] = cur
545 545 else:
546 546 partnames = [p for p in self._parts.keys() if p != tmplspec.ref]
547 547 m = formatter.templatepartsmap(tmplspec, self.t, partnames)
548 548 self._parts.update(m)
549 549
550 550 if self._parts[b'docheader']:
551 551 self.ui.write(self.t.render(self._parts[b'docheader'], {}))
552 552
553 553 def close(self):
554 554 if self._parts[b'docfooter']:
555 555 if not self.footer:
556 556 self.footer = b""
557 557 self.footer += self.t.render(self._parts[b'docfooter'], {})
558 558 return super(changesettemplater, self).close()
559 559
560 560 def _show(self, ctx, copies, props):
561 561 '''show a single changeset or file revision'''
562 562 props = props.copy()
563 563 props[b'ctx'] = ctx
564 564 props[b'index'] = index = next(self._counter)
565 565 props[b'revcache'] = {b'copies': copies}
566 566 graphwidth = props.get(b'graphwidth', 0)
567 567
568 568 # write separator, which wouldn't work well with the header part below
569 569 # since there's inherently a conflict between header (across items) and
570 570 # separator (per item)
571 571 if self._parts[b'separator'] and index > 0:
572 572 self.ui.write(self.t.render(self._parts[b'separator'], {}))
573 573
574 574 # write header
575 575 if self._parts[b'header']:
576 576 h = self.t.render(self._parts[b'header'], props)
577 577 if self.buffered:
578 578 self.header[ctx.rev()] = h
579 579 else:
580 580 if self.lastheader != h:
581 581 self.lastheader = h
582 582 self.ui.write(h)
583 583
584 584 # write changeset metadata, then patch if requested
585 585 key = self._parts[self._tref]
586 586 self.ui.write(self.t.render(key, props))
587 587 self._showpatch(ctx, graphwidth)
588 588
589 589 if self._parts[b'footer']:
590 590 if not self.footer:
591 591 self.footer = self.t.render(self._parts[b'footer'], props)
592 592
593 593
594 594 def templatespec(tmpl, mapfile):
595 595 if pycompat.ispy3:
596 596 assert not isinstance(tmpl, str), b'tmpl must not be a str'
597 597 if mapfile:
598 598 return formatter.templatespec(b'changeset', tmpl, mapfile)
599 599 else:
600 600 return formatter.templatespec(b'', tmpl, None)
601 601
602 602
603 603 def _lookuptemplate(ui, tmpl, style):
604 604 """Find the template matching the given template spec or style
605 605
606 606 See formatter.lookuptemplate() for details.
607 607 """
608 608
609 609 # ui settings
610 610 if not tmpl and not style: # template are stronger than style
611 611 tmpl = ui.config(b'ui', b'logtemplate')
612 612 if tmpl:
613 613 return templatespec(templater.unquotestring(tmpl), None)
614 614 else:
615 615 style = util.expandpath(ui.config(b'ui', b'style'))
616 616
617 617 if not tmpl and style:
618 618 mapfile = style
619 619 if not os.path.split(mapfile)[0]:
620 620 mapname = templater.templatepath(
621 621 b'map-cmdline.' + mapfile
622 622 ) or templater.templatepath(mapfile)
623 623 if mapname:
624 624 mapfile = mapname
625 625 return templatespec(None, mapfile)
626 626
627 627 return formatter.lookuptemplate(ui, b'changeset', tmpl)
628 628
629 629
630 630 def maketemplater(ui, repo, tmpl, buffered=False):
631 631 """Create a changesettemplater from a literal template 'tmpl'
632 632 byte-string."""
633 633 spec = templatespec(tmpl, None)
634 634 return changesettemplater(ui, repo, spec, buffered=buffered)
635 635
636 636
637 637 def changesetdisplayer(ui, repo, opts, differ=None, buffered=False):
638 638 """show one changeset using template or regular display.
639 639
640 640 Display format will be the first non-empty hit of:
641 641 1. option 'template'
642 642 2. option 'style'
643 643 3. [ui] setting 'logtemplate'
644 644 4. [ui] setting 'style'
645 645 If all of these values are either the unset or the empty string,
646 646 regular display via changesetprinter() is done.
647 647 """
648 648 postargs = (differ, opts, buffered)
649 649 spec = _lookuptemplate(ui, opts.get(b'template'), opts.get(b'style'))
650 650
651 651 # machine-readable formats have slightly different keyword set than
652 652 # plain templates, which are handled by changesetformatter.
653 653 # note that {b'pickle', b'debug'} can also be added to the list if needed.
654 654 if spec.ref in {b'cbor', b'json'}:
655 655 fm = ui.formatter(b'log', opts)
656 656 return changesetformatter(ui, repo, fm, *postargs)
657 657
658 658 if not spec.ref and not spec.tmpl and not spec.mapfile:
659 659 return changesetprinter(ui, repo, *postargs)
660 660
661 661 return changesettemplater(ui, repo, spec, *postargs)
662 662
663 663
664 664 def _makematcher(repo, revs, pats, opts):
665 665 """Build matcher and expanded patterns from log options
666 666
667 667 If --follow, revs are the revisions to follow from.
668 668
669 669 Returns (match, pats, slowpath) where
670 670 - match: a matcher built from the given pats and -I/-X opts
671 671 - pats: patterns used (globs are expanded on Windows)
672 672 - slowpath: True if patterns aren't as simple as scanning filelogs
673 673 """
674 674 # pats/include/exclude are passed to match.match() directly in
675 675 # _matchfiles() revset but walkchangerevs() builds its matcher with
676 676 # scmutil.match(). The difference is input pats are globbed on
677 677 # platforms without shell expansion (windows).
678 678 wctx = repo[None]
679 679 match, pats = scmutil.matchandpats(wctx, pats, opts)
680 680 slowpath = match.anypats() or (not match.always() and opts.get(b'removed'))
681 681 if not slowpath:
682 682 follow = opts.get(b'follow') or opts.get(b'follow_first')
683 683 startctxs = []
684 684 if follow and opts.get(b'rev'):
685 685 startctxs = [repo[r] for r in revs]
686 686 for f in match.files():
687 687 if follow and startctxs:
688 688 # No idea if the path was a directory at that revision, so
689 689 # take the slow path.
690 690 if any(f not in c for c in startctxs):
691 691 slowpath = True
692 692 continue
693 693 elif follow and f not in wctx:
694 694 # If the file exists, it may be a directory, so let it
695 695 # take the slow path.
696 696 if os.path.exists(repo.wjoin(f)):
697 697 slowpath = True
698 698 continue
699 699 else:
700 700 raise error.Abort(
701 701 _(
702 702 b'cannot follow file not in parent '
703 703 b'revision: "%s"'
704 704 )
705 705 % f
706 706 )
707 707 filelog = repo.file(f)
708 708 if not filelog:
709 709 # A zero count may be a directory or deleted file, so
710 710 # try to find matching entries on the slow path.
711 711 if follow:
712 712 raise error.Abort(
713 713 _(b'cannot follow nonexistent file: "%s"') % f
714 714 )
715 715 slowpath = True
716 716
717 717 # We decided to fall back to the slowpath because at least one
718 718 # of the paths was not a file. Check to see if at least one of them
719 719 # existed in history - in that case, we'll continue down the
720 720 # slowpath; otherwise, we can turn off the slowpath
721 721 if slowpath:
722 722 for path in match.files():
723 723 if path == b'.' or path in repo.store:
724 724 break
725 725 else:
726 726 slowpath = False
727 727
728 728 return match, pats, slowpath
729 729
730 730
731 731 def _fileancestors(repo, revs, match, followfirst):
732 732 fctxs = []
733 733 for r in revs:
734 734 ctx = repo[r]
735 735 fctxs.extend(ctx[f].introfilectx() for f in ctx.walk(match))
736 736
737 737 # When displaying a revision with --patch --follow FILE, we have
738 738 # to know which file of the revision must be diffed. With
739 739 # --follow, we want the names of the ancestors of FILE in the
740 740 # revision, stored in "fcache". "fcache" is populated as a side effect
741 741 # of the graph traversal.
742 742 fcache = {}
743 743
744 744 def filematcher(ctx):
745 745 return scmutil.matchfiles(repo, fcache.get(ctx.rev(), []))
746 746
747 747 def revgen():
748 748 for rev, cs in dagop.filectxancestors(fctxs, followfirst=followfirst):
749 749 fcache[rev] = [c.path() for c in cs]
750 750 yield rev
751 751
752 752 return smartset.generatorset(revgen(), iterasc=False), filematcher
753 753
754 754
755 755 def _makenofollowfilematcher(repo, pats, opts):
756 756 '''hook for extensions to override the filematcher for non-follow cases'''
757 757 return None
758 758
759 759
760 760 _opt2logrevset = {
761 761 b'no_merges': (b'not merge()', None),
762 762 b'only_merges': (b'merge()', None),
763 763 b'_matchfiles': (None, b'_matchfiles(%ps)'),
764 764 b'date': (b'date(%s)', None),
765 765 b'branch': (b'branch(%s)', b'%lr'),
766 766 b'_patslog': (b'filelog(%s)', b'%lr'),
767 767 b'keyword': (b'keyword(%s)', b'%lr'),
768 768 b'prune': (b'ancestors(%s)', b'not %lr'),
769 769 b'user': (b'user(%s)', b'%lr'),
770 770 }
771 771
772 772
773 773 def _makerevset(repo, match, pats, slowpath, opts):
774 774 """Return a revset string built from log options and file patterns"""
775 775 opts = dict(opts)
776 776 # follow or not follow?
777 777 follow = opts.get(b'follow') or opts.get(b'follow_first')
778 778
779 779 # branch and only_branch are really aliases and must be handled at
780 780 # the same time
781 781 opts[b'branch'] = opts.get(b'branch', []) + opts.get(b'only_branch', [])
782 782 opts[b'branch'] = [repo.lookupbranch(b) for b in opts[b'branch']]
783 783
784 784 if slowpath:
785 785 # See walkchangerevs() slow path.
786 786 #
787 787 # pats/include/exclude cannot be represented as separate
788 788 # revset expressions as their filtering logic applies at file
789 789 # level. For instance "-I a -X b" matches a revision touching
790 790 # "a" and "b" while "file(a) and not file(b)" does
791 791 # not. Besides, filesets are evaluated against the working
792 792 # directory.
793 793 matchargs = [b'r:', b'd:relpath']
794 794 for p in pats:
795 795 matchargs.append(b'p:' + p)
796 796 for p in opts.get(b'include', []):
797 797 matchargs.append(b'i:' + p)
798 798 for p in opts.get(b'exclude', []):
799 799 matchargs.append(b'x:' + p)
800 800 opts[b'_matchfiles'] = matchargs
801 801 elif not follow:
802 802 opts[b'_patslog'] = list(pats)
803 803
804 804 expr = []
805 805 for op, val in sorted(pycompat.iteritems(opts)):
806 806 if not val:
807 807 continue
808 808 if op not in _opt2logrevset:
809 809 continue
810 810 revop, listop = _opt2logrevset[op]
811 811 if revop and b'%' not in revop:
812 812 expr.append(revop)
813 813 elif not listop:
814 814 expr.append(revsetlang.formatspec(revop, val))
815 815 else:
816 816 if revop:
817 817 val = [revsetlang.formatspec(revop, v) for v in val]
818 818 expr.append(revsetlang.formatspec(listop, val))
819 819
820 820 if expr:
821 821 expr = b'(' + b' and '.join(expr) + b')'
822 822 else:
823 823 expr = None
824 824 return expr
825 825
826 826
827 827 def _initialrevs(repo, opts):
828 828 """Return the initial set of revisions to be filtered or followed"""
829 829 follow = opts.get(b'follow') or opts.get(b'follow_first')
830 830 if opts.get(b'rev'):
831 831 revs = scmutil.revrange(repo, opts[b'rev'])
832 832 elif follow and repo.dirstate.p1() == nullid:
833 833 revs = smartset.baseset()
834 834 elif follow:
835 835 revs = repo.revs(b'.')
836 836 else:
837 837 revs = smartset.spanset(repo)
838 838 revs.reverse()
839 839 return revs
840 840
841 841
842 842 def getrevs(repo, pats, opts):
843 843 """Return (revs, differ) where revs is a smartset
844 844
845 845 differ is a changesetdiffer with pre-configured file matcher.
846 846 """
847 847 follow = opts.get(b'follow') or opts.get(b'follow_first')
848 848 followfirst = opts.get(b'follow_first')
849 849 limit = getlimit(opts)
850 850 revs = _initialrevs(repo, opts)
851 851 if not revs:
852 852 return smartset.baseset(), None
853 853 match, pats, slowpath = _makematcher(repo, revs, pats, opts)
854 854 filematcher = None
855 855 if follow:
856 856 if slowpath or match.always():
857 857 revs = dagop.revancestors(repo, revs, followfirst=followfirst)
858 858 else:
859 859 revs, filematcher = _fileancestors(repo, revs, match, followfirst)
860 860 revs.reverse()
861 861 if filematcher is None:
862 862 filematcher = _makenofollowfilematcher(repo, pats, opts)
863 863 if filematcher is None:
864 864
865 865 def filematcher(ctx):
866 866 return match
867 867
868 868 expr = _makerevset(repo, match, pats, slowpath, opts)
869 869 if opts.get(b'graph'):
870 870 # User-specified revs might be unsorted, but don't sort before
871 871 # _makerevset because it might depend on the order of revs
872 872 if repo.ui.configbool(b'experimental', b'log.topo'):
873 873 if not revs.istopo():
874 874 revs = dagop.toposort(revs, repo.changelog.parentrevs)
875 875 # TODO: try to iterate the set lazily
876 876 revs = revset.baseset(list(revs), istopo=True)
877 877 elif not (revs.isdescending() or revs.istopo()):
878 878 revs.sort(reverse=True)
879 879 if expr:
880 880 matcher = revset.match(None, expr)
881 881 revs = matcher(repo, revs)
882 882 if limit is not None:
883 883 revs = revs.slice(0, limit)
884 884
885 885 differ = changesetdiffer()
886 886 differ._makefilematcher = filematcher
887 887 return revs, differ
888 888
889 889
890 890 def _parselinerangeopt(repo, opts):
891 891 """Parse --line-range log option and return a list of tuples (filename,
892 892 (fromline, toline)).
893 893 """
894 894 linerangebyfname = []
895 895 for pat in opts.get(b'line_range', []):
896 896 try:
897 897 pat, linerange = pat.rsplit(b',', 1)
898 898 except ValueError:
899 899 raise error.Abort(_(b'malformatted line-range pattern %s') % pat)
900 900 try:
901 901 fromline, toline = map(int, linerange.split(b':'))
902 902 except ValueError:
903 903 raise error.Abort(_(b"invalid line range for %s") % pat)
904 904 msg = _(b"line range pattern '%s' must match exactly one file") % pat
905 905 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
906 906 linerangebyfname.append(
907 907 (fname, util.processlinerange(fromline, toline))
908 908 )
909 909 return linerangebyfname
910 910
911 911
912 912 def getlinerangerevs(repo, userrevs, opts):
913 913 """Return (revs, differ).
914 914
915 915 "revs" are revisions obtained by processing "line-range" log options and
916 916 walking block ancestors of each specified file/line-range.
917 917
918 918 "differ" is a changesetdiffer with pre-configured file matcher and hunks
919 919 filter.
920 920 """
921 921 wctx = repo[None]
922 922
923 923 # Two-levels map of "rev -> file ctx -> [line range]".
924 924 linerangesbyrev = {}
925 925 for fname, (fromline, toline) in _parselinerangeopt(repo, opts):
926 926 if fname not in wctx:
927 927 raise error.Abort(
928 928 _(b'cannot follow file not in parent revision: "%s"') % fname
929 929 )
930 930 fctx = wctx.filectx(fname)
931 931 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
932 932 rev = fctx.introrev()
933 if rev is None:
934 rev = wdirrev
933 935 if rev not in userrevs:
934 936 continue
935 937 linerangesbyrev.setdefault(rev, {}).setdefault(
936 938 fctx.path(), []
937 939 ).append(linerange)
938 940
939 941 def nofilterhunksfn(fctx, hunks):
940 942 return hunks
941 943
942 944 def hunksfilter(ctx):
943 fctxlineranges = linerangesbyrev.get(ctx.rev())
945 fctxlineranges = linerangesbyrev.get(scmutil.intrev(ctx))
944 946 if fctxlineranges is None:
945 947 return nofilterhunksfn
946 948
947 949 def filterfn(fctx, hunks):
948 950 lineranges = fctxlineranges.get(fctx.path())
949 951 if lineranges is not None:
950 952 for hr, lines in hunks:
951 953 if hr is None: # binary
952 954 yield hr, lines
953 955 continue
954 956 if any(mdiff.hunkinrange(hr[2:], lr) for lr in lineranges):
955 957 yield hr, lines
956 958 else:
957 959 for hunk in hunks:
958 960 yield hunk
959 961
960 962 return filterfn
961 963
962 964 def filematcher(ctx):
963 files = list(linerangesbyrev.get(ctx.rev(), []))
965 files = list(linerangesbyrev.get(scmutil.intrev(ctx), []))
964 966 return scmutil.matchfiles(repo, files)
965 967
966 968 revs = sorted(linerangesbyrev, reverse=True)
967 969
968 970 differ = changesetdiffer()
969 971 differ._makefilematcher = filematcher
970 972 differ._makehunksfilter = hunksfilter
971 973 return revs, differ
972 974
973 975
974 976 def _graphnodeformatter(ui, displayer):
975 977 spec = ui.config(b'ui', b'graphnodetemplate')
976 978 if not spec:
977 979 return templatekw.getgraphnode # fast path for "{graphnode}"
978 980
979 981 spec = templater.unquotestring(spec)
980 982 if isinstance(displayer, changesettemplater):
981 983 # reuse cache of slow templates
982 984 tres = displayer._tresources
983 985 else:
984 986 tres = formatter.templateresources(ui)
985 987 templ = formatter.maketemplater(
986 988 ui, spec, defaults=templatekw.keywords, resources=tres
987 989 )
988 990
989 991 def formatnode(repo, ctx):
990 992 props = {b'ctx': ctx, b'repo': repo}
991 993 return templ.renderdefault(props)
992 994
993 995 return formatnode
994 996
995 997
996 998 def displaygraph(ui, repo, dag, displayer, edgefn, getcopies=None, props=None):
997 999 props = props or {}
998 1000 formatnode = _graphnodeformatter(ui, displayer)
999 1001 state = graphmod.asciistate()
1000 1002 styles = state[b'styles']
1001 1003
1002 1004 # only set graph styling if HGPLAIN is not set.
1003 1005 if ui.plain(b'graph'):
1004 1006 # set all edge styles to |, the default pre-3.8 behaviour
1005 1007 styles.update(dict.fromkeys(styles, b'|'))
1006 1008 else:
1007 1009 edgetypes = {
1008 1010 b'parent': graphmod.PARENT,
1009 1011 b'grandparent': graphmod.GRANDPARENT,
1010 1012 b'missing': graphmod.MISSINGPARENT,
1011 1013 }
1012 1014 for name, key in edgetypes.items():
1013 1015 # experimental config: experimental.graphstyle.*
1014 1016 styles[key] = ui.config(
1015 1017 b'experimental', b'graphstyle.%s' % name, styles[key]
1016 1018 )
1017 1019 if not styles[key]:
1018 1020 styles[key] = None
1019 1021
1020 1022 # experimental config: experimental.graphshorten
1021 1023 state[b'graphshorten'] = ui.configbool(b'experimental', b'graphshorten')
1022 1024
1023 1025 for rev, type, ctx, parents in dag:
1024 1026 char = formatnode(repo, ctx)
1025 1027 copies = getcopies(ctx) if getcopies else None
1026 1028 edges = edgefn(type, char, state, rev, parents)
1027 1029 firstedge = next(edges)
1028 1030 width = firstedge[2]
1029 1031 displayer.show(
1030 1032 ctx, copies=copies, graphwidth=width, **pycompat.strkwargs(props)
1031 1033 )
1032 1034 lines = displayer.hunk.pop(rev).split(b'\n')
1033 1035 if not lines[-1]:
1034 1036 del lines[-1]
1035 1037 displayer.flush(ctx)
1036 1038 for type, char, width, coldata in itertools.chain([firstedge], edges):
1037 1039 graphmod.ascii(ui, state, type, char, lines, coldata)
1038 1040 lines = []
1039 1041 displayer.close()
1040 1042
1041 1043
1042 1044 def displaygraphrevs(ui, repo, revs, displayer, getrenamed):
1043 1045 revdag = graphmod.dagwalker(repo, revs)
1044 1046 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed)
1045 1047
1046 1048
1047 1049 def displayrevs(ui, repo, revs, displayer, getcopies):
1048 1050 for rev in revs:
1049 1051 ctx = repo[rev]
1050 1052 copies = getcopies(ctx) if getcopies else None
1051 1053 displayer.show(ctx, copies=copies)
1052 1054 displayer.flush(ctx)
1053 1055 displayer.close()
1054 1056
1055 1057
1056 1058 def checkunsupportedgraphflags(pats, opts):
1057 1059 for op in [b"newest_first"]:
1058 1060 if op in opts and opts[op]:
1059 1061 raise error.Abort(
1060 1062 _(b"-G/--graph option is incompatible with --%s")
1061 1063 % op.replace(b"_", b"-")
1062 1064 )
1063 1065
1064 1066
1065 1067 def graphrevs(repo, nodes, opts):
1066 1068 limit = getlimit(opts)
1067 1069 nodes.reverse()
1068 1070 if limit is not None:
1069 1071 nodes = nodes[:limit]
1070 1072 return graphmod.nodes(repo, nodes)
@@ -1,954 +1,1047 b''
1 1 $ cat >> $HGRCPATH << EOF
2 2 > [diff]
3 3 > git = true
4 4 > EOF
5 5
6 6 $ hg init
7 7 $ cat > foo << EOF
8 8 > 0
9 9 > 1
10 10 > 2
11 11 > 3
12 12 > 4
13 13 > EOF
14 14 $ hg ci -Am init
15 15 adding foo
16 16 $ cat > foo << EOF
17 17 > 0
18 18 > 0
19 19 > 0
20 20 > 0
21 21 > 1
22 22 > 2
23 23 > 3
24 24 > 4
25 25 > EOF
26 26 $ hg ci -m 'more 0'
27 27 $ sed 's/2/2+/' foo > foo.new
28 28 $ mv foo.new foo
29 29 $ cat > bar << EOF
30 30 > a
31 31 > b
32 32 > c
33 33 > d
34 34 > e
35 35 > EOF
36 36 $ hg add bar
37 37 $ hg ci -Am "2 -> 2+; added bar"
38 38 $ cat >> foo << EOF
39 39 > 5
40 40 > 6
41 41 > 7
42 42 > 8
43 43 > 9
44 44 > 10
45 45 > 11
46 46 > EOF
47 47 $ hg ci -m "to 11"
48 48
49 49 Add some changes with two diff hunks
50 50
51 51 $ sed 's/^1$/ 1/' foo > foo.new
52 52 $ mv foo.new foo
53 53 $ sed 's/^11$/11+/' foo > foo.new
54 54 $ mv foo.new foo
55 55 $ hg ci -m '11 -> 11+; leading space before "1"'
56 56 (make sure there are two hunks in "foo")
57 57 $ hg diff -c .
58 58 diff --git a/foo b/foo
59 59 --- a/foo
60 60 +++ b/foo
61 61 @@ -2,7 +2,7 @@
62 62 0
63 63 0
64 64 0
65 65 -1
66 66 + 1
67 67 2+
68 68 3
69 69 4
70 70 @@ -12,4 +12,4 @@
71 71 8
72 72 9
73 73 10
74 74 -11
75 75 +11+
76 76 $ sed 's/3/3+/' foo > foo.new
77 77 $ mv foo.new foo
78 78 $ sed 's/^11+$/11-/' foo > foo.new
79 79 $ mv foo.new foo
80 80 $ sed 's/a/a+/' bar > bar.new
81 81 $ mv bar.new bar
82 82 $ hg ci -m 'foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+'
83 83 (make sure there are two hunks in "foo")
84 84 $ hg diff -c . foo
85 85 diff --git a/foo b/foo
86 86 --- a/foo
87 87 +++ b/foo
88 88 @@ -4,7 +4,7 @@
89 89 0
90 90 1
91 91 2+
92 92 -3
93 93 +3+
94 94 4
95 95 5
96 96 6
97 97 @@ -12,4 +12,4 @@
98 98 8
99 99 9
100 100 10
101 101 -11+
102 102 +11-
103 103
104 104 $ hg log -f -L foo,5:7 -p
105 105 changeset: 5:cfdf972b3971
106 106 tag: tip
107 107 user: test
108 108 date: Thu Jan 01 00:00:00 1970 +0000
109 109 summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
110 110
111 111 diff --git a/foo b/foo
112 112 --- a/foo
113 113 +++ b/foo
114 114 @@ -4,7 +4,7 @@
115 115 0
116 116 1
117 117 2+
118 118 -3
119 119 +3+
120 120 4
121 121 5
122 122 6
123 123
124 124 changeset: 4:eaec41c1a0c9
125 125 user: test
126 126 date: Thu Jan 01 00:00:00 1970 +0000
127 127 summary: 11 -> 11+; leading space before "1"
128 128
129 129 diff --git a/foo b/foo
130 130 --- a/foo
131 131 +++ b/foo
132 132 @@ -2,7 +2,7 @@
133 133 0
134 134 0
135 135 0
136 136 -1
137 137 + 1
138 138 2+
139 139 3
140 140 4
141 141
142 142 changeset: 2:63a884426fd0
143 143 user: test
144 144 date: Thu Jan 01 00:00:00 1970 +0000
145 145 summary: 2 -> 2+; added bar
146 146
147 147 diff --git a/foo b/foo
148 148 --- a/foo
149 149 +++ b/foo
150 150 @@ -3,6 +3,6 @@
151 151 0
152 152 0
153 153 1
154 154 -2
155 155 +2+
156 156 3
157 157 4
158 158
159 159 changeset: 0:5ae1f82b9a00
160 160 user: test
161 161 date: Thu Jan 01 00:00:00 1970 +0000
162 162 summary: init
163 163
164 164 diff --git a/foo b/foo
165 165 new file mode 100644
166 166 --- /dev/null
167 167 +++ b/foo
168 168 @@ -0,0 +1,5 @@
169 169 +0
170 170 +1
171 171 +2
172 172 +3
173 173 +4
174 174
175 175 $ hg log -f --graph -L foo,5:7 -p
176 176 @ changeset: 5:cfdf972b3971
177 177 | tag: tip
178 178 | user: test
179 179 | date: Thu Jan 01 00:00:00 1970 +0000
180 180 | summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
181 181 |
182 182 | diff --git a/foo b/foo
183 183 | --- a/foo
184 184 | +++ b/foo
185 185 | @@ -4,7 +4,7 @@
186 186 | 0
187 187 | 1
188 188 | 2+
189 189 | -3
190 190 | +3+
191 191 | 4
192 192 | 5
193 193 | 6
194 194 |
195 195 o changeset: 4:eaec41c1a0c9
196 196 : user: test
197 197 : date: Thu Jan 01 00:00:00 1970 +0000
198 198 : summary: 11 -> 11+; leading space before "1"
199 199 :
200 200 : diff --git a/foo b/foo
201 201 : --- a/foo
202 202 : +++ b/foo
203 203 : @@ -2,7 +2,7 @@
204 204 : 0
205 205 : 0
206 206 : 0
207 207 : -1
208 208 : + 1
209 209 : 2+
210 210 : 3
211 211 : 4
212 212 :
213 213 o changeset: 2:63a884426fd0
214 214 : user: test
215 215 : date: Thu Jan 01 00:00:00 1970 +0000
216 216 : summary: 2 -> 2+; added bar
217 217 :
218 218 : diff --git a/foo b/foo
219 219 : --- a/foo
220 220 : +++ b/foo
221 221 : @@ -3,6 +3,6 @@
222 222 : 0
223 223 : 0
224 224 : 1
225 225 : -2
226 226 : +2+
227 227 : 3
228 228 : 4
229 229 :
230 230 o changeset: 0:5ae1f82b9a00
231 231 user: test
232 232 date: Thu Jan 01 00:00:00 1970 +0000
233 233 summary: init
234 234
235 235 diff --git a/foo b/foo
236 236 new file mode 100644
237 237 --- /dev/null
238 238 +++ b/foo
239 239 @@ -0,0 +1,5 @@
240 240 +0
241 241 +1
242 242 +2
243 243 +3
244 244 +4
245 245
246 246
247 247 With --template.
248 248
249 249 $ hg log -f -L foo,5:7 -T '{rev}:{node|short} {desc|firstline}\n'
250 250 5:cfdf972b3971 foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
251 251 4:eaec41c1a0c9 11 -> 11+; leading space before "1"
252 252 2:63a884426fd0 2 -> 2+; added bar
253 253 0:5ae1f82b9a00 init
254 254 $ hg log -f -L foo,5:7 -T json
255 255 [
256 256 {
257 257 "bookmarks": [],
258 258 "branch": "default",
259 259 "date": [0, 0],
260 260 "desc": "foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+",
261 261 "node": "cfdf972b3971a2a59638bf9583c0debbffee5404",
262 262 "parents": ["eaec41c1a0c9ad0a5e999611d0149d171beffb8c"],
263 263 "phase": "draft",
264 264 "rev": 5,
265 265 "tags": ["tip"],
266 266 "user": "test"
267 267 },
268 268 {
269 269 "bookmarks": [],
270 270 "branch": "default",
271 271 "date": [0, 0],
272 272 "desc": "11 -> 11+; leading space before \"1\"",
273 273 "node": "eaec41c1a0c9ad0a5e999611d0149d171beffb8c",
274 274 "parents": ["730a61fbaecf426c17c2c66bc42d195b5d5b0ba8"],
275 275 "phase": "draft",
276 276 "rev": 4,
277 277 "tags": [],
278 278 "user": "test"
279 279 },
280 280 {
281 281 "bookmarks": [],
282 282 "branch": "default",
283 283 "date": [0, 0],
284 284 "desc": "2 -> 2+; added bar",
285 285 "node": "63a884426fd0b277fcd55895bbb2f230434576eb",
286 286 "parents": ["29a1e7c6b80024f63f310a2d71de979e9d2996d7"],
287 287 "phase": "draft",
288 288 "rev": 2,
289 289 "tags": [],
290 290 "user": "test"
291 291 },
292 292 {
293 293 "bookmarks": [],
294 294 "branch": "default",
295 295 "date": [0, 0],
296 296 "desc": "init",
297 297 "node": "5ae1f82b9a000ff1e0967d0dac1c58b9d796e1b4",
298 298 "parents": ["0000000000000000000000000000000000000000"],
299 299 "phase": "draft",
300 300 "rev": 0,
301 301 "tags": [],
302 302 "user": "test"
303 303 }
304 304 ]
305 305
306 306 With some white-space diff option, respective revisions are skipped.
307 307
308 308 $ hg log -f -L foo,5:7 -p --config diff.ignorews=true
309 309 changeset: 5:cfdf972b3971
310 310 tag: tip
311 311 user: test
312 312 date: Thu Jan 01 00:00:00 1970 +0000
313 313 summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
314 314
315 315 diff --git a/foo b/foo
316 316 --- a/foo
317 317 +++ b/foo
318 318 @@ -4,7 +4,7 @@
319 319 0
320 320 1
321 321 2+
322 322 -3
323 323 +3+
324 324 4
325 325 5
326 326 6
327 327
328 328 changeset: 2:63a884426fd0
329 329 user: test
330 330 date: Thu Jan 01 00:00:00 1970 +0000
331 331 summary: 2 -> 2+; added bar
332 332
333 333 diff --git a/foo b/foo
334 334 --- a/foo
335 335 +++ b/foo
336 336 @@ -3,6 +3,6 @@
337 337 0
338 338 0
339 339 1
340 340 -2
341 341 +2+
342 342 3
343 343 4
344 344
345 345 changeset: 0:5ae1f82b9a00
346 346 user: test
347 347 date: Thu Jan 01 00:00:00 1970 +0000
348 348 summary: init
349 349
350 350 diff --git a/foo b/foo
351 351 new file mode 100644
352 352 --- /dev/null
353 353 +++ b/foo
354 354 @@ -0,0 +1,5 @@
355 355 +0
356 356 +1
357 357 +2
358 358 +3
359 359 +4
360 360
361 361
362 362 Regular file patterns are not allowed.
363 363
364 364 $ hg log -f -L foo,5:7 -p bar
365 365 abort: FILE arguments are not compatible with --line-range option
366 366 [255]
367 367
368 368 Option --rev acts as a restriction.
369 369
370 370 $ hg log -f -L foo,5:7 -p -r 'desc(2)'
371 371 changeset: 2:63a884426fd0
372 372 user: test
373 373 date: Thu Jan 01 00:00:00 1970 +0000
374 374 summary: 2 -> 2+; added bar
375 375
376 376 diff --git a/foo b/foo
377 377 --- a/foo
378 378 +++ b/foo
379 379 @@ -3,6 +3,6 @@
380 380 0
381 381 0
382 382 1
383 383 -2
384 384 +2+
385 385 3
386 386 4
387 387
388 388 changeset: 0:5ae1f82b9a00
389 389 user: test
390 390 date: Thu Jan 01 00:00:00 1970 +0000
391 391 summary: init
392 392
393 393 diff --git a/foo b/foo
394 394 new file mode 100644
395 395 --- /dev/null
396 396 +++ b/foo
397 397 @@ -0,0 +1,5 @@
398 398 +0
399 399 +1
400 400 +2
401 401 +3
402 402 +4
403 403
404 404
405 405 With several -L patterns, changes touching any files in their respective line
406 406 range are show.
407 407
408 408 $ hg log -f -L foo,5:7 -L bar,1:2 -p
409 409 changeset: 5:cfdf972b3971
410 410 tag: tip
411 411 user: test
412 412 date: Thu Jan 01 00:00:00 1970 +0000
413 413 summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
414 414
415 415 diff --git a/bar b/bar
416 416 --- a/bar
417 417 +++ b/bar
418 418 @@ -1,4 +1,4 @@
419 419 -a
420 420 +a+
421 421 b
422 422 c
423 423 d
424 424 diff --git a/foo b/foo
425 425 --- a/foo
426 426 +++ b/foo
427 427 @@ -4,7 +4,7 @@
428 428 0
429 429 1
430 430 2+
431 431 -3
432 432 +3+
433 433 4
434 434 5
435 435 6
436 436
437 437 changeset: 4:eaec41c1a0c9
438 438 user: test
439 439 date: Thu Jan 01 00:00:00 1970 +0000
440 440 summary: 11 -> 11+; leading space before "1"
441 441
442 442 diff --git a/foo b/foo
443 443 --- a/foo
444 444 +++ b/foo
445 445 @@ -2,7 +2,7 @@
446 446 0
447 447 0
448 448 0
449 449 -1
450 450 + 1
451 451 2+
452 452 3
453 453 4
454 454
455 455 changeset: 2:63a884426fd0
456 456 user: test
457 457 date: Thu Jan 01 00:00:00 1970 +0000
458 458 summary: 2 -> 2+; added bar
459 459
460 460 diff --git a/bar b/bar
461 461 new file mode 100644
462 462 --- /dev/null
463 463 +++ b/bar
464 464 @@ -0,0 +1,5 @@
465 465 +a
466 466 +b
467 467 +c
468 468 +d
469 469 +e
470 470 diff --git a/foo b/foo
471 471 --- a/foo
472 472 +++ b/foo
473 473 @@ -3,6 +3,6 @@
474 474 0
475 475 0
476 476 1
477 477 -2
478 478 +2+
479 479 3
480 480 4
481 481
482 482 changeset: 0:5ae1f82b9a00
483 483 user: test
484 484 date: Thu Jan 01 00:00:00 1970 +0000
485 485 summary: init
486 486
487 487 diff --git a/foo b/foo
488 488 new file mode 100644
489 489 --- /dev/null
490 490 +++ b/foo
491 491 @@ -0,0 +1,5 @@
492 492 +0
493 493 +1
494 494 +2
495 495 +3
496 496 +4
497 497
498 498
499 499 Multiple -L options with the same file yields changes touching any of
500 500 specified line ranges.
501 501
502 502 $ hg log -f -L foo,5:7 -L foo,14:15 -p
503 503 changeset: 5:cfdf972b3971
504 504 tag: tip
505 505 user: test
506 506 date: Thu Jan 01 00:00:00 1970 +0000
507 507 summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
508 508
509 509 diff --git a/foo b/foo
510 510 --- a/foo
511 511 +++ b/foo
512 512 @@ -4,7 +4,7 @@
513 513 0
514 514 1
515 515 2+
516 516 -3
517 517 +3+
518 518 4
519 519 5
520 520 6
521 521 @@ -12,4 +12,4 @@
522 522 8
523 523 9
524 524 10
525 525 -11+
526 526 +11-
527 527
528 528 changeset: 4:eaec41c1a0c9
529 529 user: test
530 530 date: Thu Jan 01 00:00:00 1970 +0000
531 531 summary: 11 -> 11+; leading space before "1"
532 532
533 533 diff --git a/foo b/foo
534 534 --- a/foo
535 535 +++ b/foo
536 536 @@ -2,7 +2,7 @@
537 537 0
538 538 0
539 539 0
540 540 -1
541 541 + 1
542 542 2+
543 543 3
544 544 4
545 545 @@ -12,4 +12,4 @@
546 546 8
547 547 9
548 548 10
549 549 -11
550 550 +11+
551 551
552 552 changeset: 3:730a61fbaecf
553 553 user: test
554 554 date: Thu Jan 01 00:00:00 1970 +0000
555 555 summary: to 11
556 556
557 557 diff --git a/foo b/foo
558 558 --- a/foo
559 559 +++ b/foo
560 560 @@ -6,3 +6,10 @@
561 561 2+
562 562 3
563 563 4
564 564 +5
565 565 +6
566 566 +7
567 567 +8
568 568 +9
569 569 +10
570 570 +11
571 571
572 572 changeset: 2:63a884426fd0
573 573 user: test
574 574 date: Thu Jan 01 00:00:00 1970 +0000
575 575 summary: 2 -> 2+; added bar
576 576
577 577 diff --git a/foo b/foo
578 578 --- a/foo
579 579 +++ b/foo
580 580 @@ -3,6 +3,6 @@
581 581 0
582 582 0
583 583 1
584 584 -2
585 585 +2+
586 586 3
587 587 4
588 588
589 589 changeset: 0:5ae1f82b9a00
590 590 user: test
591 591 date: Thu Jan 01 00:00:00 1970 +0000
592 592 summary: init
593 593
594 594 diff --git a/foo b/foo
595 595 new file mode 100644
596 596 --- /dev/null
597 597 +++ b/foo
598 598 @@ -0,0 +1,5 @@
599 599 +0
600 600 +1
601 601 +2
602 602 +3
603 603 +4
604 604
605 605
606 606 A file with a comma in its name.
607 607
608 608 $ cat > ba,z << EOF
609 609 > q
610 610 > w
611 611 > e
612 612 > r
613 613 > t
614 614 > y
615 615 > EOF
616 616 $ hg ci -Am 'querty'
617 617 adding ba,z
618 618 $ cat >> ba,z << EOF
619 619 > u
620 620 > i
621 621 > o
622 622 > p
623 623 > EOF
624 624 $ hg ci -m 'more keys'
625 625 $ cat > ba,z << EOF
626 626 > a
627 627 > z
628 628 > e
629 629 > r
630 630 > t
631 631 > y
632 632 > u
633 633 > i
634 634 > o
635 635 > p
636 636 > EOF
637 637 $ hg ci -m 'azerty'
638 638 $ hg log -f -L ba,z,1:2 -p
639 639 changeset: 8:52373265138b
640 640 tag: tip
641 641 user: test
642 642 date: Thu Jan 01 00:00:00 1970 +0000
643 643 summary: azerty
644 644
645 645 diff --git a/ba,z b/ba,z
646 646 --- a/ba,z
647 647 +++ b/ba,z
648 648 @@ -1,5 +1,5 @@
649 649 -q
650 650 -w
651 651 +a
652 652 +z
653 653 e
654 654 r
655 655 t
656 656
657 657 changeset: 6:96ba8850f316
658 658 user: test
659 659 date: Thu Jan 01 00:00:00 1970 +0000
660 660 summary: querty
661 661
662 662 diff --git a/ba,z b/ba,z
663 663 new file mode 100644
664 664 --- /dev/null
665 665 +++ b/ba,z
666 666 @@ -0,0 +1,6 @@
667 667 +q
668 668 +w
669 669 +e
670 670 +r
671 671 +t
672 672 +y
673 673
674 674
675 675 Exact prefix kinds work in -L options.
676 676
677 677 $ mkdir dir
678 678 $ cd dir
679 679 $ hg log -f -L path:foo,5:7 -p
680 680 changeset: 5:cfdf972b3971
681 681 user: test
682 682 date: Thu Jan 01 00:00:00 1970 +0000
683 683 summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
684 684
685 685 diff --git a/foo b/foo
686 686 --- a/foo
687 687 +++ b/foo
688 688 @@ -4,7 +4,7 @@
689 689 0
690 690 1
691 691 2+
692 692 -3
693 693 +3+
694 694 4
695 695 5
696 696 6
697 697
698 698 changeset: 4:eaec41c1a0c9
699 699 user: test
700 700 date: Thu Jan 01 00:00:00 1970 +0000
701 701 summary: 11 -> 11+; leading space before "1"
702 702
703 703 diff --git a/foo b/foo
704 704 --- a/foo
705 705 +++ b/foo
706 706 @@ -2,7 +2,7 @@
707 707 0
708 708 0
709 709 0
710 710 -1
711 711 + 1
712 712 2+
713 713 3
714 714 4
715 715
716 716 changeset: 2:63a884426fd0
717 717 user: test
718 718 date: Thu Jan 01 00:00:00 1970 +0000
719 719 summary: 2 -> 2+; added bar
720 720
721 721 diff --git a/foo b/foo
722 722 --- a/foo
723 723 +++ b/foo
724 724 @@ -3,6 +3,6 @@
725 725 0
726 726 0
727 727 1
728 728 -2
729 729 +2+
730 730 3
731 731 4
732 732
733 733 changeset: 0:5ae1f82b9a00
734 734 user: test
735 735 date: Thu Jan 01 00:00:00 1970 +0000
736 736 summary: init
737 737
738 738 diff --git a/foo b/foo
739 739 new file mode 100644
740 740 --- /dev/null
741 741 +++ b/foo
742 742 @@ -0,0 +1,5 @@
743 743 +0
744 744 +1
745 745 +2
746 746 +3
747 747 +4
748 748
749 749
750 750 Renames are followed.
751 751
752 752 $ hg mv ../foo baz
753 753 $ sed 's/1/1+/' baz > baz.new
754 754 $ mv baz.new baz
755 755 $ hg ci -m 'foo -> dir/baz; 1-1+'
756 756 $ hg diff -c .
757 757 diff --git a/foo b/dir/baz
758 758 rename from foo
759 759 rename to dir/baz
760 760 --- a/foo
761 761 +++ b/dir/baz
762 762 @@ -2,7 +2,7 @@
763 763 0
764 764 0
765 765 0
766 766 - 1
767 767 + 1+
768 768 2+
769 769 3+
770 770 4
771 771 @@ -11,5 +11,5 @@
772 772 7
773 773 8
774 774 9
775 775 -10
776 776 -11-
777 777 +1+0
778 778 +1+1-
779 779 $ hg log -f -L relpath:baz,5:7 -p
780 780 changeset: 9:6af29c3a778f
781 781 tag: tip
782 782 user: test
783 783 date: Thu Jan 01 00:00:00 1970 +0000
784 784 summary: foo -> dir/baz; 1-1+
785 785
786 786 diff --git a/foo b/dir/baz
787 787 copy from foo
788 788 copy to dir/baz
789 789 --- a/foo
790 790 +++ b/dir/baz
791 791 @@ -2,7 +2,7 @@
792 792 0
793 793 0
794 794 0
795 795 - 1
796 796 + 1+
797 797 2+
798 798 3+
799 799 4
800 800
801 801 changeset: 5:cfdf972b3971
802 802 user: test
803 803 date: Thu Jan 01 00:00:00 1970 +0000
804 804 summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
805 805
806 806 diff --git a/foo b/foo
807 807 --- a/foo
808 808 +++ b/foo
809 809 @@ -4,7 +4,7 @@
810 810 0
811 811 1
812 812 2+
813 813 -3
814 814 +3+
815 815 4
816 816 5
817 817 6
818 818
819 819 changeset: 4:eaec41c1a0c9
820 820 user: test
821 821 date: Thu Jan 01 00:00:00 1970 +0000
822 822 summary: 11 -> 11+; leading space before "1"
823 823
824 824 diff --git a/foo b/foo
825 825 --- a/foo
826 826 +++ b/foo
827 827 @@ -2,7 +2,7 @@
828 828 0
829 829 0
830 830 0
831 831 -1
832 832 + 1
833 833 2+
834 834 3
835 835 4
836 836
837 837 changeset: 2:63a884426fd0
838 838 user: test
839 839 date: Thu Jan 01 00:00:00 1970 +0000
840 840 summary: 2 -> 2+; added bar
841 841
842 842 diff --git a/foo b/foo
843 843 --- a/foo
844 844 +++ b/foo
845 845 @@ -3,6 +3,6 @@
846 846 0
847 847 0
848 848 1
849 849 -2
850 850 +2+
851 851 3
852 852 4
853 853
854 854 changeset: 0:5ae1f82b9a00
855 855 user: test
856 856 date: Thu Jan 01 00:00:00 1970 +0000
857 857 summary: init
858 858
859 859 diff --git a/foo b/foo
860 860 new file mode 100644
861 861 --- /dev/null
862 862 +++ b/foo
863 863 @@ -0,0 +1,5 @@
864 864 +0
865 865 +1
866 866 +2
867 867 +3
868 868 +4
869 869
870 870
871 871 Uncommitted changes with a rename
872 872
873 873 $ hg mv baz bazn
874 874 $ hg log -f -L bazn,5:7
875 875 changeset: 9:6af29c3a778f
876 876 tag: tip
877 877 user: test
878 878 date: Thu Jan 01 00:00:00 1970 +0000
879 879 summary: foo -> dir/baz; 1-1+
880 880
881 881 changeset: 5:cfdf972b3971
882 882 user: test
883 883 date: Thu Jan 01 00:00:00 1970 +0000
884 884 summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
885 885
886 886 changeset: 4:eaec41c1a0c9
887 887 user: test
888 888 date: Thu Jan 01 00:00:00 1970 +0000
889 889 summary: 11 -> 11+; leading space before "1"
890 890
891 891 changeset: 2:63a884426fd0
892 892 user: test
893 893 date: Thu Jan 01 00:00:00 1970 +0000
894 894 summary: 2 -> 2+; added bar
895 895
896 896 changeset: 0:5ae1f82b9a00
897 897 user: test
898 898 date: Thu Jan 01 00:00:00 1970 +0000
899 899 summary: init
900 900
901
902 Uncommitted changes in requested line range
903
904 $ sed 's/2/ /' bazn > bazn.new
905 $ mv bazn.new bazn
906 $ hg diff
907 diff --git a/dir/baz b/dir/bazn
908 rename from dir/baz
909 rename to dir/bazn
910 --- a/dir/baz
911 +++ b/dir/bazn
912 @@ -3,7 +3,7 @@
913 0
914 0
915 1+
916 -2+
917 + +
918 3+
919 4
920 5
921 $ hg log -f -L bazn,5:7
922 changeset: 9:6af29c3a778f
923 tag: tip
924 user: test
925 date: Thu Jan 01 00:00:00 1970 +0000
926 summary: foo -> dir/baz; 1-1+
927
928 changeset: 5:cfdf972b3971
929 user: test
930 date: Thu Jan 01 00:00:00 1970 +0000
931 summary: foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
932
933 changeset: 4:eaec41c1a0c9
934 user: test
935 date: Thu Jan 01 00:00:00 1970 +0000
936 summary: 11 -> 11+; leading space before "1"
937
938 changeset: 2:63a884426fd0
939 user: test
940 date: Thu Jan 01 00:00:00 1970 +0000
941 summary: 2 -> 2+; added bar
942
943 changeset: 0:5ae1f82b9a00
944 user: test
945 date: Thu Jan 01 00:00:00 1970 +0000
946 summary: init
947
948
949 Uncommitted changes in line-range + wdir()
950
951 $ hg log -r 'wdir()' -f -L bazn,5:7 --limit 2 -p
952 changeset: 2147483647:ffffffffffff
953 parent: 9:6af29c3a778f
954 user: test
955 date: Thu Jan 01 00:00:00 1970 +0000
956
957 diff --git a/dir/baz b/dir/bazn
958 copy from dir/baz
959 copy to dir/bazn
960 --- a/dir/baz
961 +++ b/dir/bazn
962 @@ -3,7 +3,7 @@
963 0
964 0
965 1+
966 -2+
967 + +
968 3+
969 4
970 5
971
972 changeset: 9:6af29c3a778f
973 tag: tip
974 user: test
975 date: Thu Jan 01 00:00:00 1970 +0000
976 summary: foo -> dir/baz; 1-1+
977
978 diff --git a/foo b/dir/baz
979 copy from foo
980 copy to dir/baz
981 --- a/foo
982 +++ b/dir/baz
983 @@ -2,7 +2,7 @@
984 0
985 0
986 0
987 - 1
988 + 1+
989 2+
990 3+
991 4
992
993
901 994 $ hg revert -a -C -q
902 995
903 996 Binary files work but without diff hunks filtering.
904 997 (Checking w/ and w/o diff.git option.)
905 998
906 999 >>> open('binary', 'wb').write(b'this\nis\na\nbinary\0') and None
907 1000 $ hg add binary
908 1001 $ hg ci -m 'add a binary file' --quiet
909 1002 $ hg log -f -L binary,1:2 -p
910 1003 changeset: 10:c96381c229df
911 1004 tag: tip
912 1005 user: test
913 1006 date: Thu Jan 01 00:00:00 1970 +0000
914 1007 summary: add a binary file
915 1008
916 1009 diff --git a/dir/binary b/dir/binary
917 1010 new file mode 100644
918 1011 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c2e1fbed209fe919b3f189a6a31950e9adf61e45
919 1012 GIT binary patch
920 1013 literal 17
921 1014 Wc$_QA$SmdpqC~Ew%)G>+N(KNlNClYy
922 1015
923 1016
924 1017 $ hg log -f -L binary,1:2 -p --config diff.git=false
925 1018 changeset: 10:c96381c229df
926 1019 tag: tip
927 1020 user: test
928 1021 date: Thu Jan 01 00:00:00 1970 +0000
929 1022 summary: add a binary file
930 1023
931 1024 diff -r 6af29c3a778f -r c96381c229df dir/binary
932 1025 Binary file dir/binary has changed
933 1026
934 1027
935 1028 Option --follow is required.
936 1029
937 1030 $ hg log -L foo,5:7
938 1031 abort: --line-range requires --follow
939 1032 [255]
940 1033
941 1034 Non-exact pattern kinds are not allowed.
942 1035
943 1036 $ cd ..
944 1037 $ hg log -f -L glob:*a*,1:2
945 1038 hg: parse error: line range pattern 'glob:*a*' must match exactly one file
946 1039 [255]
947 1040
948 1041 We get an error for removed files.
949 1042
950 1043 $ hg rm dir/baz
951 1044 $ hg ci -m 'remove baz' --quiet
952 1045 $ hg log -f -L dir/baz,5:7 -p
953 1046 abort: cannot follow file not in parent revision: "dir/baz"
954 1047 [255]
General Comments 0
You need to be logged in to leave comments. Login now