[PATCH 4 of 4 v4] log: add -L/--line-range option to follow file history by line range

Yuya Nishihara yuya at tcha.org
Wed Oct 18 11:00:40 EDT 2017


On Tue, 17 Oct 2017 21:37:00 +0200, Denis Laxalde wrote:
> # HG changeset patch
> # User Denis Laxalde <denis.laxalde at logilab.fr>
> # Date 1508267731 -7200
> #      Tue Oct 17 21:15:31 2017 +0200
> # Node ID 601ab53301506bc35ab75adc8de9f611af9b3d80
> # Parent  909a69f31ef323ded6fef8dd56fb44dc97f4cd89
> # EXP-Topic followlines-cli
> log: add -L/--line-range option to follow file history by line range

I found a couple of UI bugs, but queued to start bikeshedding. Thanks.

> +def _parselinerangelogopt(repo, opts):
> +    """Parse --line-range log option and return a list of tuples (filename,
> +    (fromline, toline)).
> +    """
> +    linerangebyfname = []
> +    for pat in opts.get('line_range', []):
> +        try:
> +            pat, linerange = pat.rsplit(',', 1)
> +        except ValueError:
> +            raise error.Abort(_('malformatted line-range pattern %s') % pat)
> +        try:
> +            fromline, toline = map(int, linerange.split('-'))

Nit: I prefer : than - for consistency.

> +        except ValueError:
> +            raise error.Abort(_("invalid line range for %s") % pat)
> +        msg = _("line range pattern '%s' must match exactly one file") % pat
> +        fname = scmutil.parsefollowlinespattern(repo, None, pat, msg)
> +        linerangebyfname.append(
> +            (fname, util.processlinerange(fromline, toline)))
> +    return linerangebyfname
> +
> +def getloglinerangerevs(repo, userrevs, opts):
> +    """Return (revs, filematcher, hunksfilter).
> +
> +    "revs" are revisions obtained by processing "line-range" log options and
> +    walking block ancestors of each specified file/line-range.
> +
> +    "filematcher(rev) -> match" is a factory function returning a match object
> +    for a given revision for file patterns specified in --line-range option.
> +    If neither --stat nor --patch options are passed, "filematcher" is None.
> +
> +    "hunksfilter(rev) -> filterfn(fctx, hunks)" is a factory function
> +    returning a hunks filtering function.
> +    If neither --stat nor --patch options are passed, "filterhunks" is None.
> +    """
> +    wctx = repo[None]

Perhaps, it should track history from the specified revision.

  $ hg log -frREV -L FILE,RANGE
  (will be identical to -r 'followlines(FILE, RANGE, startrev=REV)')

> +    # Two-levels map of "rev -> file ctx -> [line range]".
> +    linerangesbyrev = {}
> +    for fname, (fromline, toline) in _parselinerangelogopt(repo, opts):
> +        fctx = wctx.filectx(fname)
> +        for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
> +            rev = fctx.introrev()
> +            if rev not in userrevs:
> +                continue
> +            linerangesbyrev.setdefault(
> +                rev, {}).setdefault(
> +                    fctx.path(), []).append(linerange)
> +
> +    filematcher = None
> +    hunksfilter = None
> +    if opts.get('patch') or opts.get('stat'):
> +
> +        def nofilterhunksfn(fctx, hunks):
> +            return hunks
> +
> +        def hunksfilter(rev):
> +            fctxlineranges = linerangesbyrev.get(rev)
> +            if fctxlineranges is None:
> +                return nofilterhunksfn
> +
> +            def filterfn(fctx, hunks):
> +                lineranges = fctxlineranges.get(fctx.path())
> +                if lineranges is not None:
> +                    for hr, lines in hunks:
> +                        if any(mdiff.hunkinrange(hr[2:], lr)
> +                               for lr in lineranges):
> +                            yield hr, lines
> +                else:
> +                    for hunk in hunks:
> +                        yield hunk

Got TypeError with a binary file. Can you investigate it?

> diff --git a/mercurial/commands.py b/mercurial/commands.py
> --- a/mercurial/commands.py
> +++ b/mercurial/commands.py
> @@ -3234,6 +3234,9 @@ def locate(ui, repo, *pats, **opts):
>      ('k', 'keyword', [],
>       _('do case-insensitive search for a given text'), _('TEXT')),
>      ('r', 'rev', [], _('show the specified revision or revset'), _('REV')),
> +    ('L', 'line-range', [],
> +     _('follow line range of specified file (EXPERIMENTAL)'),
> +     _('FILE,RANGE')),
>      ('', 'removed', None, _('include revisions where files were removed')),
>      ('m', 'only-merges', None, _('show only merges (DEPRECATED)')),
>      ('u', 'user', [], _('revisions committed by user'), _('USER')),
> @@ -3275,6 +3278,12 @@ def log(ui, repo, *pats, **opts):
>      Paths in the DAG are represented with '|', '/' and so forth. ':' in place
>      of a '|' indicates one or more revisions in a path are omitted.
>  
> +    Use -L/--line-range FILE,M-N options to follow the history of lines from M
> +    to N in FILE. With -p/--patch only diff hunks affecting specified line
> +    range will be shown. This option requires --follow; it can be specified
> +    multiple times. Currently, this option is not compatible with --graph.
> +    This option is experimental.
> +
>      .. note::
>  
>         :hg:`log --patch` may generate unexpected diff output for merge
> @@ -3288,6 +3297,12 @@ def log(ui, repo, *pats, **opts):
>         made on branches and will not show removals or mode changes. To
>         see all such changes, use the --removed switch.
>  
> +    .. note::
> +
> +        The history resulting from -L/--line-range options depends on diff
> +        options; for instance if white-spaces are ignored, respective changes
> +        with only white-spaces in specified line range will not be listed.

Moved these paragraphs under verbose container since -L is still experimental.

>      """
>      opts = pycompat.byteskwargs(opts)
> +    linerange = opts.get('line_range')
> +
> +    if linerange and not opts.get('follow'):
> +        raise error.Abort(_('--line-range requires --follow'))
> +
>      if opts.get('follow') and opts.get('rev'):
>          opts['rev'] = [revsetlang.formatspec('reverse(::%lr)', opts.get('rev'))]
>          del opts['follow']
>  
>      if opts.get('graph'):
> +        if linerange:
> +            raise error.Abort(_('graph not supported with line range patterns'))
>          return cmdutil.graphlog(ui, repo, pats, opts)
>  
>      revs, expr, filematcher = cmdutil.getlogrevs(repo, pats, opts)
> +    hunksfilter = None
> +
> +    if linerange:
> +        revs, lrfilematcher, hunksfilter = cmdutil.getloglinerangerevs(
> +            repo, revs, opts)
> +
> +        if filematcher is not None and lrfilematcher is not None:
> +            basefilematcher = filematcher
> +
> +            def filematcher(rev):
> +                files = (basefilematcher(rev).files()
> +                         + lrfilematcher(rev).files())
> +                return scmutil.matchfiles(repo, files)
> +
> +        elif filematcher is None:
> +            filematcher = lrfilematcher

So, --line-range appears to conflict with the bare file patterns.

  $ hg log -f -L hg,1-2 hgweb.cgi
  (should show followlines(hg, 1:2) + follow(hgweb.cgi))

I think bare file patterns should be rejected until it works as expected.


More information about the Mercurial-devel mailing list