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