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