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