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