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