comparison mercurial/cmdutil.py @ 34857:84c6b9384d6a

log: add -L/--line-range option to follow file history by line range We add an experimental -L/--line-range option to 'hg log' taking file patterns along with a line range using the (new) FILE,FROMLINE-TOLINE syntax where FILE may be a pattern (matching exactly one file). The resulting history is similar to what the "followlines" revset except that, if --patch is specified, only diff hunks within specified line range are shown. Basically, this brings the CLI on par with what currently only exists in hgweb through line selection in "file" and "annotate" views resulting in a file log with filtered patch to only display followed line range. The option may be specified multiple times and can be combined with --rev and regular file patterns to further restrict revisions. Usage of this option requires --follow; revisions are shown in descending order and renames are followed. Only the --graph option is currently not supported. The UI is the result of a consensus from review feedback at: https://www.mercurial-scm.org/pipermail/mercurial-devel/2017-October/106749.html The implementation spreads between commands.log() and cmdutil module. In commands.log(), the main loop may now use a "hunksfilter" factory (similar to "filematcher") that, for a given "rev", produces a filtering function for diff hunks for a given file context object. The logic to build revisions from -L/--line-range options lives in cmdutil.getloglinerangerevs() which produces "revs", "filematcher" and "hunksfilter" information. Revisions obtained by following files' line range are filtered if they do not match the revset specified by --rev option. If regular FILE arguments are passed along with -L options, both filematchers are combined into a new matcher. .. feature:: Add an experimental -L/--line-range FILE,FROMLINE-TOLINE option to 'hg log' command to follow the history of files by line range. In combination with -p/--patch option, only diff hunks within specified line range will be displayed. Feedback, especially on UX aspects, is welcome.
author Denis Laxalde <denis.laxalde@logilab.fr>
date Tue, 17 Oct 2017 21:15:31 +0200
parents 890afefa7296
children 068e0e531584
comparison
equal deleted inserted replaced
34856:890afefa7296 34857:84c6b9384d6a
24 from . import ( 24 from . import (
25 bookmarks, 25 bookmarks,
26 changelog, 26 changelog,
27 copies, 27 copies,
28 crecord as crecordmod, 28 crecord as crecordmod,
29 dagop,
29 dirstateguard, 30 dirstateguard,
30 encoding, 31 encoding,
31 error, 32 error,
32 formatter, 33 formatter,
33 graphmod, 34 graphmod,
34 match as matchmod, 35 match as matchmod,
36 mdiff,
35 obsolete, 37 obsolete,
36 patch, 38 patch,
37 pathutil, 39 pathutil,
38 pycompat, 40 pycompat,
39 registrar, 41 registrar,
2583 limitedrevs.append(r) 2585 limitedrevs.append(r)
2584 revs = smartset.baseset(limitedrevs) 2586 revs = smartset.baseset(limitedrevs)
2585 2587
2586 return revs, expr, filematcher 2588 return revs, expr, filematcher
2587 2589
2590 def _parselinerangelogopt(repo, opts):
2591 """Parse --line-range log option and return a list of tuples (filename,
2592 (fromline, toline)).
2593 """
2594 linerangebyfname = []
2595 for pat in opts.get('line_range', []):
2596 try:
2597 pat, linerange = pat.rsplit(',', 1)
2598 except ValueError:
2599 raise error.Abort(_('malformatted line-range pattern %s') % pat)
2600 try:
2601 fromline, toline = map(int, linerange.split('-'))
2602 except ValueError:
2603 raise error.Abort(_("invalid line range for %s") % pat)
2604 msg = _("line range pattern '%s' must match exactly one file") % pat
2605 fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
2606 linerangebyfname.append(
2607 (fname, util.processlinerange(fromline, toline)))
2608 return linerangebyfname
2609
2610 def getloglinerangerevs(repo, userrevs, opts):
2611 """Return (revs, filematcher, hunksfilter).
2612
2613 "revs" are revisions obtained by processing "line-range" log options and
2614 walking block ancestors of each specified file/line-range.
2615
2616 "filematcher(rev) -> match" is a factory function returning a match object
2617 for a given revision for file patterns specified in --line-range option.
2618 If neither --stat nor --patch options are passed, "filematcher" is None.
2619
2620 "hunksfilter(rev) -> filterfn(fctx, hunks)" is a factory function
2621 returning a hunks filtering function.
2622 If neither --stat nor --patch options are passed, "filterhunks" is None.
2623 """
2624 wctx = repo[None]
2625
2626 # Two-levels map of "rev -> file ctx -> [line range]".
2627 linerangesbyrev = {}
2628 for fname, (fromline, toline) in _parselinerangelogopt(repo, opts):
2629 fctx = wctx.filectx(fname)
2630 for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
2631 rev = fctx.introrev()
2632 if rev not in userrevs:
2633 continue
2634 linerangesbyrev.setdefault(
2635 rev, {}).setdefault(
2636 fctx.path(), []).append(linerange)
2637
2638 filematcher = None
2639 hunksfilter = None
2640 if opts.get('patch') or opts.get('stat'):
2641
2642 def nofilterhunksfn(fctx, hunks):
2643 return hunks
2644
2645 def hunksfilter(rev):
2646 fctxlineranges = linerangesbyrev.get(rev)
2647 if fctxlineranges is None:
2648 return nofilterhunksfn
2649
2650 def filterfn(fctx, hunks):
2651 lineranges = fctxlineranges.get(fctx.path())
2652 if lineranges is not None:
2653 for hr, lines in hunks:
2654 if any(mdiff.hunkinrange(hr[2:], lr)
2655 for lr in lineranges):
2656 yield hr, lines
2657 else:
2658 for hunk in hunks:
2659 yield hunk
2660
2661 return filterfn
2662
2663 def filematcher(rev):
2664 files = list(linerangesbyrev.get(rev, []))
2665 return scmutil.matchfiles(repo, files)
2666
2667 revs = sorted(linerangesbyrev, reverse=True)
2668
2669 return revs, filematcher, hunksfilter
2670
2588 def _graphnodeformatter(ui, displayer): 2671 def _graphnodeformatter(ui, displayer):
2589 spec = ui.config('ui', 'graphnodetemplate') 2672 spec = ui.config('ui', 'graphnodetemplate')
2590 if not spec: 2673 if not spec:
2591 return templatekw.showgraphnode # fast path for "{graphnode}" 2674 return templatekw.showgraphnode # fast path for "{graphnode}"
2592 2675