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

login
register
mail settings
Submitter Denis Laxalde
Date Oct. 4, 2017, 3:04 p.m.
Message ID <0f2d8b304223a8d00163.1507129442@sh77.tls.logilab.fr>
Download mbox | patch
Permalink /patch/24500/
State Changes Requested
Headers show

Comments

Denis Laxalde - Oct. 4, 2017, 3:04 p.m.
# HG changeset patch
# User Denis Laxalde <denis.laxalde@logilab.fr>
# Date 1507127733 -7200
#      Wed Oct 04 16:35:33 2017 +0200
# Node ID 0f2d8b304223a8d00163f917fdc18082a88bceae
# Parent  9274bdbba0f913672305ed3d91c7e05f38d1eab7
# Available At http://hg.logilab.org/users/dlaxalde/hg
#              hg pull http://hg.logilab.org/users/dlaxalde/hg -r 0f2d8b304223
# EXP-Topic followlines-cli
log: add -L/--line-range option to follow file history by line range

We add a -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 (it's also more convenient to type).

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 or
regular file pattern in a meaningful way. It also respects --follow semantics.
Only the --graph option is currently not supported.

Some debatable UI choices (I did not think too much about this for now).

*   "," as a separator between the FILE and line range information; the idea
    is to avoid confusion with file pattern syntax which uses ":".

*   "-" in the line range information may not be the best choice; in
    particular, we might want to add support for an offset +/- syntax.


The implementation spreads between commands.log() and cmdutil module.
In commands.log(), the main loop now works with tuples of (rev, matchfn,
lineranges), where 'rev' and 'matchfn' are similar to before except that
'matchfn' is now built out the loop. Without any -L option, "lineranges" is
None. With a -L option, 'matchfn' is a new matcher function built from files
specified in -L option(s) and any other regular FILE pattern.

The logic to build revisions from -L/--line-range options lives in
cmdutil.getloglineranges() and the resulting information is combined with
information from --rev option and file patterns in commands.log(). Options
--rev and file patterns act as restrictions to the revisions obtained by
following line-range history of files specified in -L/--line-range patterns.

.. feature::

   Add a -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.
Yuya Nishihara - Oct. 5, 2017, 3:16 p.m.
On Wed, 04 Oct 2017 17:04:02 +0200, Denis Laxalde wrote:
> # HG changeset patch
> # User Denis Laxalde <denis.laxalde@logilab.fr>
> # Date 1507127733 -7200
> #      Wed Oct 04 16:35:33 2017 +0200
> # Node ID 0f2d8b304223a8d00163f917fdc18082a88bceae
> # Parent  9274bdbba0f913672305ed3d91c7e05f38d1eab7
> # Available At http://hg.logilab.org/users/dlaxalde/hg
> #              hg pull http://hg.logilab.org/users/dlaxalde/hg -r 0f2d8b304223
> # EXP-Topic followlines-cli
> log: add -L/--line-range option to follow file history by line range
> 
> We add a -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 (it's also more convenient to type).
> 
> 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 or
> regular file pattern in a meaningful way. It also respects --follow semantics.
> Only the --graph option is currently not supported.
> 
> Some debatable UI choices (I did not think too much about this for now).
> 
> *   "," as a separator between the FILE and line range information; the idea
>     is to avoid confusion with file pattern syntax which uses ":".
> 
> *   "-" in the line range information may not be the best choice; in
>     particular, we might want to add support for an offset +/- syntax.

Nit: we use ':' in revset.

> The implementation spreads between commands.log() and cmdutil module.
> In commands.log(), the main loop now works with tuples of (rev, matchfn,
> lineranges), where 'rev' and 'matchfn' are similar to before except that
> 'matchfn' is now built out the loop. Without any -L option, "lineranges" is
> None. With a -L option, 'matchfn' is a new matcher function built from files
> specified in -L option(s) and any other regular FILE pattern.
> 
> The logic to build revisions from -L/--line-range options lives in
> cmdutil.getloglineranges() and the resulting information is combined with
> information from --rev option and file patterns in commands.log(). Options
> --rev and file patterns act as restrictions to the revisions obtained by
> following line-range history of files specified in -L/--line-range patterns.

[...]

> +def getloglineranges(repo, opts):
> +    """Return a generator of tuples (rev, fpath, [line range]) obtained by
> +    processing "line-range" log options and walking block ancestors of each
> +    specified file/line-range.
> +    """
> +    wctx = repo[None]
> +    follow = bool(opts.get('follow'))
> +    # Two-levels map of "rev -> file path -> [line range]".
> +    linerangesbyrev = {}
> +    for fname, (fromline, toline) in _parselinerangelogopt(repo, opts):
> +        fctx = wctx.filectx(fname)
> +        for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
> +            fpath = fctx.path()
> +            if not follow and fpath != fname:
> +                continue
> +            linerangesbyrev.setdefault(
> +                fctx.rev(), {}).setdefault(
> +                    fpath, []).append(linerange)
> +    return sorted(linerangesbyrev.iteritems(), reverse=follow)

--follow isn't the option to flip the order. I think --follow should be
implied (or required) given how line-range works.

> diff --git a/mercurial/commands.py b/mercurial/commands.py
> --- a/mercurial/commands.py
> +++ b/mercurial/commands.py
> @@ -3230,6 +3230,8 @@ 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'),
> +     _('FILE,RANGE')),

No idea if we like the short option -L.

> +    # Build an iterator of (rev, matcher, lineranges) tuples to be consumed
> +    # later in the main loop.
> +    if followlines:
> +        # Filter revisions from --line-range patterns from those not in --rev
> +        # option and combine any existing filematcher with the one built from
> +        # "line range" patterns.
> +        if filematcher is None:
> +            def buildmatcher(files):
> +                return scmutil.match(repo[None], files)
> +        else:
> +            def buildmatcher(files):
> +                return scmutil.match(repo[None],
> +                                     files + filematcher(None).files(),
> +                                     default='path')
> +        revsmatchandlineranges = (
> +            (rev, buildmatcher(list(lrs)), lrs)
> +            for rev, lrs in linerangesbyrev if rev in revs
> +        )

I don't think it's intuitive that '-L PAT1' and 'PAT2' are AND-ed. (Well, I
didn't carefully read the code, but probably PAT2 would be filtered by revs
even though PATs are OR-ed.)

So I think -L PAT and plain PATs should be mutually exclusive. Alternatively,
-L could be considered pseudo-pair of file pattern.

  $ hg log FILE1 -L RANGE1 FILE2 -L RANGE2

I prefer this over -L FILE,RANGE syntax.

> -        displayer.show(ctx, copies=copies, matchfn=revmatchfn)
> +        displayer.show(ctx, copies=copies, matchfn=matchfn,
> +                       lineranges=lineranges)

Appears that passing matchfn adds extra '\n' in showpatch().
Denis Laxalde - Oct. 6, 2017, 2:38 p.m.
Yuya Nishihara a écrit :
> On Wed, 04 Oct 2017 17:04:02 +0200, Denis Laxalde wrote:
>> # HG changeset patch
>> # User Denis Laxalde <denis.laxalde@logilab.fr>
>> # Date 1507127733 -7200
>> #      Wed Oct 04 16:35:33 2017 +0200
>> # Node ID 0f2d8b304223a8d00163f917fdc18082a88bceae
>> # Parent  9274bdbba0f913672305ed3d91c7e05f38d1eab7
>> # Available At http://hg.logilab.org/users/dlaxalde/hg
>> #              hg pull http://hg.logilab.org/users/dlaxalde/hg -r 0f2d8b304223
>> # EXP-Topic followlines-cli
>> log: add -L/--line-range option to follow file history by line range
>>
>> We add a -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 (it's also more convenient to type).
>>
>> 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 or
>> regular file pattern in a meaningful way. It also respects --follow semantics.
>> Only the --graph option is currently not supported.
>>
>> Some debatable UI choices (I did not think too much about this for now).
>>
>> *   "," as a separator between the FILE and line range information; the idea
>>      is to avoid confusion with file pattern syntax which uses ":".
>>
>> *   "-" in the line range information may not be the best choice; in
>>      particular, we might want to add support for an offset +/- syntax.
> 
> Nit: we use ':' in revset.
> 
>> The implementation spreads between commands.log() and cmdutil module.
>> In commands.log(), the main loop now works with tuples of (rev, matchfn,
>> lineranges), where 'rev' and 'matchfn' are similar to before except that
>> 'matchfn' is now built out the loop. Without any -L option, "lineranges" is
>> None. With a -L option, 'matchfn' is a new matcher function built from files
>> specified in -L option(s) and any other regular FILE pattern.
>>
>> The logic to build revisions from -L/--line-range options lives in
>> cmdutil.getloglineranges() and the resulting information is combined with
>> information from --rev option and file patterns in commands.log(). Options
>> --rev and file patterns act as restrictions to the revisions obtained by
>> following line-range history of files specified in -L/--line-range patterns.
> 
> [...]
> 
>> +def getloglineranges(repo, opts):
>> +    """Return a generator of tuples (rev, fpath, [line range]) obtained by
>> +    processing "line-range" log options and walking block ancestors of each
>> +    specified file/line-range.
>> +    """
>> +    wctx = repo[None]
>> +    follow = bool(opts.get('follow'))
>> +    # Two-levels map of "rev -> file path -> [line range]".
>> +    linerangesbyrev = {}
>> +    for fname, (fromline, toline) in _parselinerangelogopt(repo, opts):
>> +        fctx = wctx.filectx(fname)
>> +        for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
>> +            fpath = fctx.path()
>> +            if not follow and fpath != fname:
>> +                continue
>> +            linerangesbyrev.setdefault(
>> +                fctx.rev(), {}).setdefault(
>> +                    fpath, []).append(linerange)
>> +    return sorted(linerangesbyrev.iteritems(), reverse=follow)
> 
> --follow isn't the option to flip the order. I think --follow should be
> implied (or required) given how line-range works.

Okay, will do.

> 
>> diff --git a/mercurial/commands.py b/mercurial/commands.py
>> --- a/mercurial/commands.py
>> +++ b/mercurial/commands.py
>> @@ -3230,6 +3230,8 @@ 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'),
>> +     _('FILE,RANGE')),
> 
> No idea if we like the short option -L.
> 
>> +    # Build an iterator of (rev, matcher, lineranges) tuples to be consumed
>> +    # later in the main loop.
>> +    if followlines:
>> +        # Filter revisions from --line-range patterns from those not in --rev
>> +        # option and combine any existing filematcher with the one built from
>> +        # "line range" patterns.
>> +        if filematcher is None:
>> +            def buildmatcher(files):
>> +                return scmutil.match(repo[None], files)
>> +        else:
>> +            def buildmatcher(files):
>> +                return scmutil.match(repo[None],
>> +                                     files + filematcher(None).files(),
>> +                                     default='path')
>> +        revsmatchandlineranges = (
>> +            (rev, buildmatcher(list(lrs)), lrs)
>> +            for rev, lrs in linerangesbyrev if rev in revs
>> +        )
> 
> I don't think it's intuitive that '-L PAT1' and 'PAT2' are AND-ed. (Well, I
> didn't carefully read the code, but probably PAT2 would be filtered by revs
> even though PATs are OR-ed.)
> 
> So I think -L PAT and plain PATs should be mutually exclusive. Alternatively,
> -L could be considered pseudo-pair of file pattern.
> 
>    $ hg log FILE1 -L RANGE1 FILE2 -L RANGE2
> 
> I prefer this over -L FILE,RANGE syntax.

I'm fine for making -L PAT and plain PATs mutually exclusive, it makes
sense.

I'm not sure I understand your proposal (how more specifically, how to
implement it). How should I make FILE1 and RANGE1 matching? Is there a
way to enforce a "-L RANGE" to be followed by a "FILE"? Or should I just
assuming that "opts['line_range']" and "pats" will have the same order
and length and zip on the two lists?

> 
>> -        displayer.show(ctx, copies=copies, matchfn=revmatchfn)
>> +        displayer.show(ctx, copies=copies, matchfn=matchfn,
>> +                       lineranges=lineranges)
> 
> Appears that passing matchfn adds extra '\n' in showpatch().
>
Yuya Nishihara - Oct. 6, 2017, 3:52 p.m.
On Fri, 6 Oct 2017 16:38:23 +0200, Denis Laxalde wrote:
> Yuya Nishihara a écrit :
> > I don't think it's intuitive that '-L PAT1' and 'PAT2' are AND-ed. (Well, I
> > didn't carefully read the code, but probably PAT2 would be filtered by revs
> > even though PATs are OR-ed.)
> > 
> > So I think -L PAT and plain PATs should be mutually exclusive. Alternatively,
> > -L could be considered pseudo-pair of file pattern.
> > 
> >    $ hg log FILE1 -L RANGE1 FILE2 -L RANGE2
> > 
> > I prefer this over -L FILE,RANGE syntax.
> 
> I'm fine for making -L PAT and plain PATs mutually exclusive, it makes
> sense.
> 
> I'm not sure I understand your proposal (how more specifically, how to
> implement it). How should I make FILE1 and RANGE1 matching? Is there a
> way to enforce a "-L RANGE" to be followed by a "FILE"? Or should I just
> assuming that "opts['line_range']" and "pats" will have the same order
> and length and zip on the two lists?

There's no way to strictly parse options along with positional arguments, so
that would be similar to what "prune --biject" does.

I think we all have UI concern about the last patch. This should get more
attention.
Denis Laxalde - Oct. 6, 2017, 7:01 p.m.
Yuya Nishihara a écrit :
> On Fri, 6 Oct 2017 16:38:23 +0200, Denis Laxalde wrote:
>> Yuya Nishihara a écrit :
>>> I don't think it's intuitive that '-L PAT1' and 'PAT2' are AND-ed. (Well, I
>>> didn't carefully read the code, but probably PAT2 would be filtered by revs
>>> even though PATs are OR-ed.)
>>>
>>> So I think -L PAT and plain PATs should be mutually exclusive. Alternatively,
>>> -L could be considered pseudo-pair of file pattern.
>>>
>>>     $ hg log FILE1 -L RANGE1 FILE2 -L RANGE2
>>>
>>> I prefer this over -L FILE,RANGE syntax.
>>
>> I'm fine for making -L PAT and plain PATs mutually exclusive, it makes
>> sense.
>>
>> I'm not sure I understand your proposal (how more specifically, how to
>> implement it). How should I make FILE1 and RANGE1 matching? Is there a
>> way to enforce a "-L RANGE" to be followed by a "FILE"? Or should I just
>> assuming that "opts['line_range']" and "pats" will have the same order
>> and length and zip on the two lists?
> 
> There's no way to strictly parse options along with positional arguments, so
> that would be similar to what "prune --biject" does.
> 
> I think we all have UI concern about the last patch. This should get more
> attention.
> 

Ok. I'll submit a v2 addressing other points of your review while
sticking with -L FILE,RANGE syntax (but making it exclusive with plain
file patterns) so that we can focus on this UI question.

Patch

diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -26,6 +26,7 @@  from . import (
     changelog,
     copies,
     crecord as crecordmod,
+    dagop,
     dirstateguard,
     encoding,
     error,
@@ -2593,6 +2594,46 @@  def getlogrevs(repo, pats, opts):
 
     return revs, expr, filematcher
 
+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('-'))
+        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 getloglineranges(repo, opts):
+    """Return a generator of tuples (rev, fpath, [line range]) obtained by
+    processing "line-range" log options and walking block ancestors of each
+    specified file/line-range.
+    """
+    wctx = repo[None]
+    follow = bool(opts.get('follow'))
+    # Two-levels map of "rev -> file path -> [line range]".
+    linerangesbyrev = {}
+    for fname, (fromline, toline) in _parselinerangelogopt(repo, opts):
+        fctx = wctx.filectx(fname)
+        for fctx, linerange in dagop.blockancestors(fctx, fromline, toline):
+            fpath = fctx.path()
+            if not follow and fpath != fname:
+                continue
+            linerangesbyrev.setdefault(
+                fctx.rev(), {}).setdefault(
+                    fpath, []).append(linerange)
+    return sorted(linerangesbyrev.iteritems(), reverse=follow)
+
 def _graphnodeformatter(ui, displayer):
     spec = ui.config('ui', 'graphnodetemplate')
     if not spec:
diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -3230,6 +3230,8 @@  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'),
+     _('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')),
@@ -3271,6 +3273,11 @@  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 can be specified multiple times.
+    This option is currently not compatible with --graph.
+
     .. note::
 
        :hg:`log --patch` may generate unexpected diff output for merge
@@ -3284,6 +3291,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.
+
     .. container:: verbose
 
       Some examples:
@@ -3332,6 +3345,15 @@  def log(ui, repo, *pats, **opts):
 
           hg log -r "last(tagged())::" --template "{desc|firstline}\\n"
 
+      - changesets touching lines 13 to 23 for file.c::
+
+          hg log -L file.c,13-23
+
+      - changesets touching lines 13 to 23 for file.c and lines 2 to 6 of
+        main.c, shown in descending direction with patch::
+
+          hg log -L file.c,13-23 -L main.c,2-6 -p -f
+
     See :hg:`help dates` for a list of formats valid for -d/--date.
 
     See :hg:`help revisions` for more about specifying and ordering
@@ -3346,14 +3368,45 @@  def log(ui, repo, *pats, **opts):
 
     """
     opts = pycompat.byteskwargs(opts)
+    followlines = opts.get('line_range')
+    if followlines:
+        linerangesbyrev = cmdutil.getloglineranges(repo, opts)
+
     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 followlines:
+            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)
+
+    # Build an iterator of (rev, matcher, lineranges) tuples to be consumed
+    # later in the main loop.
+    if followlines:
+        # Filter revisions from --line-range patterns from those not in --rev
+        # option and combine any existing filematcher with the one built from
+        # "line range" patterns.
+        if filematcher is None:
+            def buildmatcher(files):
+                return scmutil.match(repo[None], files)
+        else:
+            def buildmatcher(files):
+                return scmutil.match(repo[None],
+                                     files + filematcher(None).files(),
+                                     default='path')
+        revsmatchandlineranges = (
+            (rev, buildmatcher(list(lrs)), lrs)
+            for rev, lrs in linerangesbyrev if rev in revs
+        )
+    else:
+        revsmatchandlineranges = (
+            (rev, filematcher(rev) if filematcher else None, None)
+            for rev in revs
+        )
+
     limit = cmdutil.loglimit(opts)
     count = 0
 
@@ -3366,7 +3419,7 @@  def log(ui, repo, *pats, **opts):
 
     ui.pager('log')
     displayer = cmdutil.show_changeset(ui, repo, opts, buffered=True)
-    for rev in revs:
+    for rev, matchfn, lineranges in revsmatchandlineranges:
         if count == limit:
             break
         ctx = repo[rev]
@@ -3377,11 +3430,8 @@  def log(ui, repo, *pats, **opts):
                 rename = getrenamed(fn, rev)
                 if rename:
                     copies.append((fn, rename[0]))
-        if filematcher:
-            revmatchfn = filematcher(ctx.rev())
-        else:
-            revmatchfn = None
-        displayer.show(ctx, copies=copies, matchfn=revmatchfn)
+        displayer.show(ctx, copies=copies, matchfn=matchfn,
+                       lineranges=lineranges)
         if displayer.flush(ctx):
             count += 1
 
diff --git a/tests/test-completion.t b/tests/test-completion.t
--- a/tests/test-completion.t
+++ b/tests/test-completion.t
@@ -225,7 +225,7 @@  Show all commands + options
   export: output, switch-parent, rev, text, git, binary, nodates
   forget: include, exclude
   init: ssh, remotecmd, insecure
-  log: follow, follow-first, date, copies, keyword, rev, removed, only-merges, user, only-branch, branch, prune, patch, git, limit, no-merges, stat, graph, style, template, include, exclude
+  log: follow, follow-first, date, copies, keyword, rev, line-range, removed, only-merges, user, only-branch, branch, prune, patch, git, limit, no-merges, stat, graph, style, template, include, exclude
   merge: force, rev, preview, tool
   pull: update, force, rev, bookmark, branch, ssh, remotecmd, insecure
   push: force, rev, bookmark, branch, new-branch, pushvars, ssh, remotecmd, insecure
diff --git a/tests/test-log-linerange.t b/tests/test-log-linerange.t
new file mode 100644
--- /dev/null
+++ b/tests/test-log-linerange.t
@@ -0,0 +1,848 @@ 
+  $ cat >> $HGRCPATH << EOF
+  > [diff]
+  > git = true
+  > EOF
+
+  $ hg init
+  $ cat > foo << EOF
+  > 0
+  > 1
+  > 2
+  > 3
+  > 4
+  > EOF
+  $ hg ci -Am init
+  adding foo
+  $ cat > foo << EOF
+  > 0
+  > 0
+  > 0
+  > 0
+  > 1
+  > 2
+  > 3
+  > 4
+  > EOF
+  $ hg ci -m 'more 0'
+  $ sed 's/2/2+/' foo > foo.new
+  $ mv foo.new foo
+  $ hg ci -m "2 -> 2+"
+  $ cat >> foo << EOF
+  > 5
+  > 6
+  > 7
+  > 8
+  > 9
+  > 10
+  > 11
+  > EOF
+  $ hg ci -m "to 11"
+
+Add some changes with two diff hunks
+
+  $ sed 's/^1$/ 1/' foo > foo.new
+  $ mv foo.new foo
+  $ sed 's/^11$/11+/' foo > foo.new
+  $ mv foo.new foo
+  $ hg ci -m '11 -> 11+; leading space before "1"'
+(make sure there are two hunks in "foo")
+  $ hg diff -c .
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -2,7 +2,7 @@
+   0
+   0
+   0
+  -1
+  + 1
+   2+
+   3
+   4
+  @@ -12,4 +12,4 @@
+   8
+   9
+   10
+  -11
+  +11+
+  $ sed 's/3/3+/' foo > foo.new
+  $ mv foo.new foo
+  $ sed 's/^11+$/11-/' foo > foo.new
+  $ mv foo.new foo
+  $ cat > bar << EOF
+  > a
+  > b
+  > c
+  > d
+  > e
+  > EOF
+  $ hg ci -Am '3 -> 3+; 11+ -> 11- and added bar'
+  adding bar
+(make sure there are two hunks in "foo")
+  $ hg diff -c . foo
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -4,7 +4,7 @@
+   0
+    1
+   2+
+  -3
+  +3+
+   4
+   5
+   6
+  @@ -12,4 +12,4 @@
+   8
+   9
+   10
+  -11+
+  +11-
+
+  $ hg log -L foo,5-7 -p
+  changeset:   0:5ae1f82b9a00
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     init
+  
+  diff --git a/foo b/foo
+  new file mode 100644
+  --- /dev/null
+  +++ b/foo
+  @@ -0,0 +1,5 @@
+  +0
+  +1
+  +2
+  +3
+  +4
+  
+  changeset:   2:3aa051770d8a
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     2 -> 2+
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -3,6 +3,6 @@
+   0
+   0
+   1
+  -2
+  +2+
+   3
+   4
+  
+  changeset:   4:d7650978f3c7
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     11 -> 11+; leading space before "1"
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -2,7 +2,7 @@
+   0
+   0
+   0
+  -1
+  + 1
+   2+
+   3
+   4
+  
+  changeset:   5:b628de18978f
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     3 -> 3+; 11+ -> 11- and added bar
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -4,7 +4,7 @@
+   0
+    1
+   2+
+  -3
+  +3+
+   4
+   5
+   6
+  
+
+With --follow.
+
+  $ hg log -L foo,5-7 -p -f
+  changeset:   5:b628de18978f
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     3 -> 3+; 11+ -> 11- and added bar
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -4,7 +4,7 @@
+   0
+    1
+   2+
+  -3
+  +3+
+   4
+   5
+   6
+  
+  changeset:   4:d7650978f3c7
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     11 -> 11+; leading space before "1"
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -2,7 +2,7 @@
+   0
+   0
+   0
+  -1
+  + 1
+   2+
+   3
+   4
+  
+  changeset:   2:3aa051770d8a
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     2 -> 2+
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -3,6 +3,6 @@
+   0
+   0
+   1
+  -2
+  +2+
+   3
+   4
+  
+  changeset:   0:5ae1f82b9a00
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     init
+  
+  diff --git a/foo b/foo
+  new file mode 100644
+  --- /dev/null
+  +++ b/foo
+  @@ -0,0 +1,5 @@
+  +0
+  +1
+  +2
+  +3
+  +4
+  
+
+With some white-space diff option, respective revisions are skipped.
+
+  $ hg log -L foo,5-7 -p -f --config diff.ignorews=true
+  changeset:   5:b628de18978f
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     3 -> 3+; 11+ -> 11- and added bar
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -4,7 +4,7 @@
+   0
+    1
+   2+
+  -3
+  +3+
+   4
+   5
+   6
+  
+  changeset:   2:3aa051770d8a
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     2 -> 2+
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -3,6 +3,6 @@
+   0
+   0
+   1
+  -2
+  +2+
+   3
+   4
+  
+  changeset:   0:5ae1f82b9a00
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     init
+  
+  diff --git a/foo b/foo
+  new file mode 100644
+  --- /dev/null
+  +++ b/foo
+  @@ -0,0 +1,5 @@
+  +0
+  +1
+  +2
+  +3
+  +4
+  
+
+With a regular file pattern, revision not touching this pattern are not shown.
+
+  $ sed 's/d/d+/' bar > bar.new
+  $ mv bar.new bar
+  $ hg ci -Am 'd -> d+'
+  $ hg log -L foo,5-7 -p bar
+  changeset:   5:b628de18978f
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     3 -> 3+; 11+ -> 11- and added bar
+  
+  diff --git a/bar b/bar
+  new file mode 100644
+  --- /dev/null
+  +++ b/bar
+  @@ -0,0 +1,5 @@
+  +a
+  +b
+  +c
+  +d
+  +e
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -4,7 +4,7 @@
+   0
+    1
+   2+
+  -3
+  +3+
+   4
+   5
+   6
+  
+
+Option --rev acts as a restriction as well.
+
+  $ hg log -L foo,5-7 -p -r 'desc(2)'
+  changeset:   2:3aa051770d8a
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     2 -> 2+
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -3,6 +3,6 @@
+   0
+   0
+   1
+  -2
+  +2+
+   3
+   4
+  
+
+With several -L patterns, only changes touching both files in their respective
+line range are show.
+
+  $ hg log -L foo,5-7 -L bar,1-2 -p -f
+  changeset:   5:b628de18978f
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     3 -> 3+; 11+ -> 11- and added bar
+  
+  diff --git a/bar b/bar
+  new file mode 100644
+  --- /dev/null
+  +++ b/bar
+  @@ -0,0 +1,5 @@
+  +a
+  +b
+  +c
+  +d
+  +e
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -4,7 +4,7 @@
+   0
+    1
+   2+
+  -3
+  +3+
+   4
+   5
+   6
+  
+  changeset:   4:d7650978f3c7
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     11 -> 11+; leading space before "1"
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -2,7 +2,7 @@
+   0
+   0
+   0
+  -1
+  + 1
+   2+
+   3
+   4
+  
+  changeset:   2:3aa051770d8a
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     2 -> 2+
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -3,6 +3,6 @@
+   0
+   0
+   1
+  -2
+  +2+
+   3
+   4
+  
+  changeset:   0:5ae1f82b9a00
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     init
+  
+  diff --git a/foo b/foo
+  new file mode 100644
+  --- /dev/null
+  +++ b/foo
+  @@ -0,0 +1,5 @@
+  +0
+  +1
+  +2
+  +3
+  +4
+  
+
+Multiple -L options with the same file yields changes touching any of
+specified line ranges.
+
+  $ hg log -L foo,5-7 -L foo,14-15 -p -f
+  changeset:   5:b628de18978f
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     3 -> 3+; 11+ -> 11- and added bar
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -4,7 +4,7 @@
+   0
+    1
+   2+
+  -3
+  +3+
+   4
+   5
+   6
+  @@ -12,4 +12,4 @@
+   8
+   9
+   10
+  -11+
+  +11-
+  
+  changeset:   4:d7650978f3c7
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     11 -> 11+; leading space before "1"
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -2,7 +2,7 @@
+   0
+   0
+   0
+  -1
+  + 1
+   2+
+   3
+   4
+  @@ -12,4 +12,4 @@
+   8
+   9
+   10
+  -11
+  +11+
+  
+  changeset:   3:bf412c98f1f1
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     to 11
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -6,3 +6,10 @@
+   2+
+   3
+   4
+  +5
+  +6
+  +7
+  +8
+  +9
+  +10
+  +11
+  
+  changeset:   2:3aa051770d8a
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     2 -> 2+
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -3,6 +3,6 @@
+   0
+   0
+   1
+  -2
+  +2+
+   3
+   4
+  
+  changeset:   0:5ae1f82b9a00
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     init
+  
+  diff --git a/foo b/foo
+  new file mode 100644
+  --- /dev/null
+  +++ b/foo
+  @@ -0,0 +1,5 @@
+  +0
+  +1
+  +2
+  +3
+  +4
+  
+
+A file with a comma in its name.
+
+  $ cat > ba,z << EOF
+  > q
+  > w
+  > e
+  > r
+  > t
+  > y
+  > EOF
+  $ hg ci -Am 'querty'
+  adding ba,z
+  $ cat >> ba,z << EOF
+  > u
+  > i
+  > o
+  > p
+  > EOF
+  $ hg ci -m 'more keys'
+  $ cat > ba,z << EOF
+  > a
+  > z
+  > e
+  > r
+  > t
+  > y
+  > u
+  > i
+  > o
+  > p
+  > EOF
+  $ hg ci -m 'azerty'
+  $ hg log -L ba,z,1-2 -p
+  changeset:   7:61cf51d887ba
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     querty
+  
+  diff --git a/ba,z b/ba,z
+  new file mode 100644
+  --- /dev/null
+  +++ b/ba,z
+  @@ -0,0 +1,6 @@
+  +q
+  +w
+  +e
+  +r
+  +t
+  +y
+  
+  changeset:   9:2d4ce1e2d65f
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     azerty
+  
+  diff --git a/ba,z b/ba,z
+  --- a/ba,z
+  +++ b/ba,z
+  @@ -1,5 +1,5 @@
+  -q
+  -w
+  +a
+  +z
+   e
+   r
+   t
+  
+
+Exact prefix kinds work in -L options.
+
+  $ mkdir dir
+  $ cd dir
+  $ hg log -L path:foo,5-7 -p -f
+  changeset:   5:b628de18978f
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     3 -> 3+; 11+ -> 11- and added bar
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -4,7 +4,7 @@
+   0
+    1
+   2+
+  -3
+  +3+
+   4
+   5
+   6
+  
+  changeset:   4:d7650978f3c7
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     11 -> 11+; leading space before "1"
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -2,7 +2,7 @@
+   0
+   0
+   0
+  -1
+  + 1
+   2+
+   3
+   4
+  
+  changeset:   2:3aa051770d8a
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     2 -> 2+
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -3,6 +3,6 @@
+   0
+   0
+   1
+  -2
+  +2+
+   3
+   4
+  
+  changeset:   0:5ae1f82b9a00
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     init
+  
+  diff --git a/foo b/foo
+  new file mode 100644
+  --- /dev/null
+  +++ b/foo
+  @@ -0,0 +1,5 @@
+  +0
+  +1
+  +2
+  +3
+  +4
+  
+
+Renames are followed according to --follow.
+
+  $ hg mv ../foo baz
+  $ sed 's/1/1+/' baz > baz.new
+  $ mv baz.new baz
+  $ hg ci -m 'foo -> dir/baz; 1-1+'
+  $ hg diff -c .
+  diff --git a/foo b/dir/baz
+  rename from foo
+  rename to dir/baz
+  --- a/foo
+  +++ b/dir/baz
+  @@ -2,7 +2,7 @@
+   0
+   0
+   0
+  - 1
+  + 1+
+   2+
+   3+
+   4
+  @@ -11,5 +11,5 @@
+   7
+   8
+   9
+  -10
+  -11-
+  +1+0
+  +1+1-
+  $ hg log -L relpath:baz,5-7 -p
+  changeset:   10:cb106a952b96
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     foo -> dir/baz; 1-1+
+  
+  diff --git a/foo b/dir/baz
+  copy from foo
+  copy to dir/baz
+  --- a/foo
+  +++ b/dir/baz
+  @@ -2,7 +2,7 @@
+   0
+   0
+   0
+  - 1
+  + 1+
+   2+
+   3+
+   4
+  
+  $ hg log -L relpath:baz,5-7 -p --follow
+  changeset:   10:cb106a952b96
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     foo -> dir/baz; 1-1+
+  
+  diff --git a/foo b/dir/baz
+  copy from foo
+  copy to dir/baz
+  --- a/foo
+  +++ b/dir/baz
+  @@ -2,7 +2,7 @@
+   0
+   0
+   0
+  - 1
+  + 1+
+   2+
+   3+
+   4
+  
+  changeset:   5:b628de18978f
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     3 -> 3+; 11+ -> 11- and added bar
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -4,7 +4,7 @@
+   0
+    1
+   2+
+  -3
+  +3+
+   4
+   5
+   6
+  
+  changeset:   4:d7650978f3c7
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     11 -> 11+; leading space before "1"
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -2,7 +2,7 @@
+   0
+   0
+   0
+  -1
+  + 1
+   2+
+   3
+   4
+  
+  changeset:   2:3aa051770d8a
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     2 -> 2+
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -3,6 +3,6 @@
+   0
+   0
+   1
+  -2
+  +2+
+   3
+   4
+  
+  changeset:   0:5ae1f82b9a00
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     init
+  
+  diff --git a/foo b/foo
+  new file mode 100644
+  --- /dev/null
+  +++ b/foo
+  @@ -0,0 +1,5 @@
+  +0
+  +1
+  +2
+  +3
+  +4
+  
+
+Non-exact pattern kinds are not allowed.
+
+  $ cd ..
+  $ hg log -L glob:*a*,1-2
+  hg: parse error: line range pattern 'glob:*a*' must match exactly one file
+  [255]
+
+Graph log does work yet.
+
+  $ hg log -L dir/baz,5-7 --graph
+  abort: graph not supported with line range patterns
+  [255]