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