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

login
register
mail settings
Submitter Denis Laxalde
Date Oct. 6, 2017, 7:01 p.m.
Message ID <9567f30cc93821001a99.1507316488@marimba>
Download mbox | patch
Permalink /patch/24623/
State Superseded
Headers show

Comments

Denis Laxalde - Oct. 6, 2017, 7:01 p.m.
# HG changeset patch
# User Denis Laxalde <denis.laxalde@logilab.fr>
# Date 1507290475 -7200
#      Fri Oct 06 13:47:55 2017 +0200
# Node ID 9567f30cc93821001a995149ba30ff5d99493d6f
# Parent  60ec507b90ea07a43f1e5d572f0b990128909eb8
# EXP-Topic followlines-cli/v2
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 to
futher restrict revisions. Revisions are shown in descending order and
renames are followed (sort of implying --follow).
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 may now use a "hunksfilter" factory (similar
to "filematcher") that, for a given "rev", produces a filtering function for

Patch

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. In commands.log(), "revs" resulting from
cmdutil.getloglinerangerevs() are filtered out of revisions that do not
match the revset specified by --rev option.

.. 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.

diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -26,12 +26,14 @@  from . import (
     changelog,
     copies,
     crecord as crecordmod,
+    dagop,
     dirstateguard,
     encoding,
     error,
     formatter,
     graphmod,
     match as matchmod,
+    mdiff,
     obsolete,
     patch,
     pathutil,
@@ -2592,6 +2594,86 @@  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 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]
+
+    # 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):
+            if fctx.rev() not in userrevs:
+                continue
+            linerangesbyrev.setdefault(
+                fctx.rev(), {}).setdefault(
+                    fctx, []).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)
+                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
+
+            return filterfn
+
+        def filematcher(rev):
+            files = [fctx.path() for fctx in linerangesbyrev.get(rev, ())]
+            return scmutil.matchfiles(repo, files)
+
+    revs = sorted(linerangesbyrev, reverse=True)
+
+    return revs, filematcher, hunksfilter
+
 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 with patch::
+
+          hg log -L file.c,13-23 -L main.c,2-6 -p
+
     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,28 @@  def log(ui, repo, *pats, **opts):
 
     """
     opts = pycompat.byteskwargs(opts)
+    linerange = opts.get('line_range')
+    if linerange and pats:
+        raise error.Abort(
+            _('FILE arguments are not compatible with --line-range option')
+        )
+
     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, filematcher, hunksfilter = cmdutil.getloglinerangerevs(
+            repo, revs, opts)
+
     limit = cmdutil.loglimit(opts)
     count = 0
 
@@ -3381,7 +3417,12 @@  def log(ui, repo, *pats, **opts):
             revmatchfn = filematcher(ctx.rev())
         else:
             revmatchfn = None
-        displayer.show(ctx, copies=copies, matchfn=revmatchfn)
+        if hunksfilter:
+            revhunksfilter = hunksfilter(rev)
+        else:
+            revhunksfilter = None
+        displayer.show(ctx, copies=copies, matchfn=revmatchfn,
+                       hunksfilterfn=revhunksfilter)
         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,794 @@ 
+  $ 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
+  $ cat > bar << EOF
+  > a
+  > b
+  > c
+  > d
+  > e
+  > EOF
+  $ hg add bar
+  $ hg ci -Am "2 -> 2+; added bar"
+  $ 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
+  $ sed 's/a/a+/' bar > bar.new
+  $ mv bar.new bar
+  $ hg ci -m 'foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+'
+(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:   5:cfdf972b3971
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -4,7 +4,7 @@
+   0
+    1
+   2+
+  -3
+  +3+
+   4
+   5
+   6
+  
+  changeset:   4:eaec41c1a0c9
+  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:63a884426fd0
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     2 -> 2+; added bar
+  
+  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 --template.
+
+  $ hg log -L foo,5-7 -T '{rev}:{node|short} {desc|firstline}\n'
+  5:cfdf972b3971 foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
+  4:eaec41c1a0c9 11 -> 11+; leading space before "1"
+  2:63a884426fd0 2 -> 2+; added bar
+  0:5ae1f82b9a00 init
+  $ hg log -L foo,5-7 -T json
+  [
+   {
+    "rev": 5,
+    "node": "cfdf972b3971a2a59638bf9583c0debbffee5404",
+    "branch": "default",
+    "phase": "draft",
+    "user": "test",
+    "date": [0, 0],
+    "desc": "foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+",
+    "bookmarks": [],
+    "tags": ["tip"],
+    "parents": ["eaec41c1a0c9ad0a5e999611d0149d171beffb8c"]
+   },
+   {
+    "rev": 4,
+    "node": "eaec41c1a0c9ad0a5e999611d0149d171beffb8c",
+    "branch": "default",
+    "phase": "draft",
+    "user": "test",
+    "date": [0, 0],
+    "desc": "11 -> 11+; leading space before \"1\"",
+    "bookmarks": [],
+    "tags": [],
+    "parents": ["730a61fbaecf426c17c2c66bc42d195b5d5b0ba8"]
+   },
+   {
+    "rev": 2,
+    "node": "63a884426fd0b277fcd55895bbb2f230434576eb",
+    "branch": "default",
+    "phase": "draft",
+    "user": "test",
+    "date": [0, 0],
+    "desc": "2 -> 2+; added bar",
+    "bookmarks": [],
+    "tags": [],
+    "parents": ["29a1e7c6b80024f63f310a2d71de979e9d2996d7"]
+   },
+   {
+    "rev": 0,
+    "node": "5ae1f82b9a000ff1e0967d0dac1c58b9d796e1b4",
+    "branch": "default",
+    "phase": "draft",
+    "user": "test",
+    "date": [0, 0],
+    "desc": "init",
+    "bookmarks": [],
+    "tags": [],
+    "parents": ["0000000000000000000000000000000000000000"]
+   }
+  ]
+
+With some white-space diff option, respective revisions are skipped.
+
+  $ hg log -L foo,5-7 -p --config diff.ignorews=true
+  changeset:   5:cfdf972b3971
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -4,7 +4,7 @@
+   0
+    1
+   2+
+  -3
+  +3+
+   4
+   5
+   6
+  
+  changeset:   2:63a884426fd0
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     2 -> 2+; added bar
+  
+  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
+  
+
+Regular file patterns are not allowed with -L.
+
+  $ hg log -L foo,5-7 -p bar
+  abort: FILE arguments are not compatible with --line-range option
+  [255]
+
+Option --rev acts as a restriction.
+
+  $ hg log -L foo,5-7 -p -r 'desc(2)'
+  changeset:   2:63a884426fd0
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     2 -> 2+; added bar
+  
+  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, changes touching any files in their respective line
+range are show.
+
+  $ hg log -L foo,5-7 -L bar,1-2 -p
+  changeset:   5:cfdf972b3971
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
+  
+  diff --git a/bar b/bar
+  --- a/bar
+  +++ b/bar
+  @@ -1,4 +1,4 @@
+  -a
+  +a+
+   b
+   c
+   d
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -4,7 +4,7 @@
+   0
+    1
+   2+
+  -3
+  +3+
+   4
+   5
+   6
+  
+  changeset:   4:eaec41c1a0c9
+  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:63a884426fd0
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     2 -> 2+; 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
+  @@ -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
+  changeset:   5:cfdf972b3971
+  tag:         tip
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
+  
+  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:eaec41c1a0c9
+  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:730a61fbaecf
+  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:63a884426fd0
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     2 -> 2+; added bar
+  
+  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:   8:52373265138b
+  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
+  
+  changeset:   6:96ba8850f316
+  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
+  
+
+Exact prefix kinds work in -L options.
+
+  $ mkdir dir
+  $ cd dir
+  $ hg log -L path:foo,5-7 -p
+  changeset:   5:cfdf972b3971
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -4,7 +4,7 @@
+   0
+    1
+   2+
+  -3
+  +3+
+   4
+   5
+   6
+  
+  changeset:   4:eaec41c1a0c9
+  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:63a884426fd0
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     2 -> 2+; added bar
+  
+  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.
+
+  $ 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:   9:6af29c3a778f
+  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:cfdf972b3971
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     foo: 3 -> 3+ and 11+ -> 11-; bar: a -> a+
+  
+  diff --git a/foo b/foo
+  --- a/foo
+  +++ b/foo
+  @@ -4,7 +4,7 @@
+   0
+    1
+   2+
+  -3
+  +3+
+   4
+   5
+   6
+  
+  changeset:   4:eaec41c1a0c9
+  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:63a884426fd0
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     2 -> 2+; added bar
+  
+  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]