Mercurial > hg
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 |