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