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