Patchwork [3,of,3,V4] revset: add a followlines(file, fromline, toline[, rev]) revset

login
register
mail settings
Submitter Denis Laxalde
Date Jan. 4, 2017, 4:03 p.m.
Message ID <e36f0540260ec2bf9e3f.1483545829@sh77.tls.logilab.fr>
Download mbox | patch
Permalink /patch/18090/
State Accepted
Headers show

Comments

Denis Laxalde - Jan. 4, 2017, 4:03 p.m.
# HG changeset patch
# User Denis Laxalde <denis.laxalde@logilab.fr>
# Date 1483544869 -3600
#      Wed Jan 04 16:47:49 2017 +0100
# Node ID e36f0540260ec2bf9e3f324bc7839d5409a3314b
# Parent  ae66a9ae337e267ecb47f8e2610c308fd81743b7
# EXP-Topic linerange-log/revset
revset: add a followlines(file, fromline, toline[, rev]) revset

This revset returns the history of a range of lines (fromline, toline) of a
file starting from `rev` or the current working directory.

Added tests in test-annotate.t which already contains a reasonably complex
repository.
Yuya Nishihara - Jan. 7, 2017, 10:26 a.m.
On Wed, 04 Jan 2017 17:03:49 +0100, Denis Laxalde wrote:
> # HG changeset patch
> # User Denis Laxalde <denis.laxalde@logilab.fr>
> # Date 1483544869 -3600
> #      Wed Jan 04 16:47:49 2017 +0100
> # Node ID e36f0540260ec2bf9e3f324bc7839d5409a3314b
> # Parent  ae66a9ae337e267ecb47f8e2610c308fd81743b7
> # EXP-Topic linerange-log/revset
> revset: add a followlines(file, fromline, toline[, rev]) revset

Also looks good. I'll queue this.

> --- a/mercurial/revset.py
> +++ b/mercurial/revset.py
> @@ -1065,6 +1065,52 @@ def _followfirst(repo, subset, x):
>      # of every revisions or files revisions.
>      return _follow(repo, subset, x, '_followfirst', followfirst=True)
>  
> +@predicate('followlines(file, fromline, toline[, rev=.])', safe=True)

I slightly prefer "fromline:toline" than "fromline, toline". I'll take a look
how that will complicate things and hopefully will send a patch before the
freeze.

> +def followlines(repo, subset, x):
> +    """Changesets modifying `file` in line range ('fromline', 'toline').
> +
> +    Line range corresponds to 'file' content at 'rev' and should hence be
> +    consistent with file size. If rev is not specified, working directory's
> +    parent is used.
> +    """
> +    from . import context  # avoid circular import issues
> +
> +    args = getargs(x, 3, 4, _("followlines takes at least three arguments"))
> +
> +    rev = '.'
> +    if len(args) == 4:
> +        revarg = getargsdict(args[3], 'followlines', 'rev')
> +        if 'rev' in revarg:
> +            revs = getset(repo, fullreposet(repo), revarg['rev'])
> +            if len(revs) != 1:
> +                raise error.ParseError(
> +                    _("followlines expects exactly one revision"))
> +            rev = revs.last()
> +
> +    pat = getstring(args[0], _("followlines requires a pattern"))
> +    if not matchmod.patkind(pat):
> +        fname = pathutil.canonpath(repo.root, repo.getcwd(), pat)
> +    else:
> +        m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=repo[rev])
> +        files = [f for f in repo[rev] if m(f)]
> +        if len(files) != 1:
> +            raise error.ParseError(_("followlines expects exactly one file"))
> +        fname = files[0]
> +
> +    try:
> +        fromline, toline = [int(getsymbol(a)) for a in args[1:3]]

Patch

diff --git a/mercurial/revset.py b/mercurial/revset.py
--- a/mercurial/revset.py
+++ b/mercurial/revset.py
@@ -1065,6 +1065,52 @@  def _followfirst(repo, subset, x):
     # of every revisions or files revisions.
     return _follow(repo, subset, x, '_followfirst', followfirst=True)
 
+@predicate('followlines(file, fromline, toline[, rev=.])', safe=True)
+def followlines(repo, subset, x):
+    """Changesets modifying `file` in line range ('fromline', 'toline').
+
+    Line range corresponds to 'file' content at 'rev' and should hence be
+    consistent with file size. If rev is not specified, working directory's
+    parent is used.
+    """
+    from . import context  # avoid circular import issues
+
+    args = getargs(x, 3, 4, _("followlines takes at least three arguments"))
+
+    rev = '.'
+    if len(args) == 4:
+        revarg = getargsdict(args[3], 'followlines', 'rev')
+        if 'rev' in revarg:
+            revs = getset(repo, fullreposet(repo), revarg['rev'])
+            if len(revs) != 1:
+                raise error.ParseError(
+                    _("followlines expects exactly one revision"))
+            rev = revs.last()
+
+    pat = getstring(args[0], _("followlines requires a pattern"))
+    if not matchmod.patkind(pat):
+        fname = pathutil.canonpath(repo.root, repo.getcwd(), pat)
+    else:
+        m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=repo[rev])
+        files = [f for f in repo[rev] if m(f)]
+        if len(files) != 1:
+            raise error.ParseError(_("followlines expects exactly one file"))
+        fname = files[0]
+
+    try:
+        fromline, toline = [int(getsymbol(a)) for a in args[1:3]]
+    except ValueError:
+        raise error.ParseError(_("line range bounds must be integers"))
+    if toline - fromline < 0:
+        raise error.ParseError(_("line range must be positive"))
+    if fromline < 1:
+        raise error.ParseError(_("fromline must be strictly positive"))
+    fromline -= 1
+
+    fctx = repo[rev].filectx(fname)
+    revs = (c.rev() for c in context.blockancestors(fctx, fromline, toline))
+    return subset & generatorset(revs, iterasc=False)
+
 @predicate('all()', safe=True)
 def getall(repo, subset, x):
     """All changesets, the same as ``0:tip``.
diff --git a/tests/test-annotate.t b/tests/test-annotate.t
--- a/tests/test-annotate.t
+++ b/tests/test-annotate.t
@@ -480,6 +480,127 @@  annotate removed file
   [255]
 #endif
 
+  $ hg revert --all --no-backup --quiet
+  $ hg id -n
+  20
+
+Test followlines() revset
+
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3, 5)'
+  16: baz:0
+  19: baz:3
+  20: baz:4
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3, 5, rev=20)'
+  16: baz:0
+  19: baz:3
+  20: baz:4
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3, 5, rev=.^)'
+  16: baz:0
+  19: baz:3
+  $ printf "0\n0\n" | cat - baz > baz1
+  $ mv baz1 baz
+  $ hg ci -m 'added two lines with 0'
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 5, 7)'
+  16: baz:0
+  19: baz:3
+  20: baz:4
+  $ echo 6 >> baz
+  $ hg ci -m 'added line 8'
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 5, 7)'
+  16: baz:0
+  19: baz:3
+  20: baz:4
+  $ sed 's/3/3+/' baz > baz.new
+  $ mv baz.new baz
+  $ hg ci -m 'baz:3->3+'
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 5, 7)'
+  16: baz:0
+  19: baz:3
+  20: baz:4
+  23: baz:3->3+
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 1, 2)'
+  21: added two lines with 0
+
+file patterns are okay
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines("path:baz", 1, 2)'
+  21: added two lines with 0
+
+renames are followed
+  $ hg mv baz qux
+  $ sed 's/4/4+/' qux > qux.new
+  $ mv qux.new qux
+  $ hg ci -m 'qux:4->4+'
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(qux, 5, 7)'
+  16: baz:0
+  19: baz:3
+  20: baz:4
+  23: baz:3->3+
+  24: qux:4->4+
+  $ hg up 23 --quiet
+
+merge
+  $ echo 7 >> baz
+  $ hg ci -m 'one more line, out of line range'
+  created new head
+  $ sed 's/3+/3-/' baz > baz.new
+  $ mv baz.new baz
+  $ hg ci -m 'baz:3+->3-'
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 5, 7)'
+  16: baz:0
+  19: baz:3
+  20: baz:4
+  23: baz:3->3+
+  26: baz:3+->3-
+  $ hg merge 24
+  merging baz and qux to qux
+  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ hg ci -m merge
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(qux, 5, 7)'
+  16: baz:0
+  19: baz:3
+  20: baz:4
+  23: baz:3->3+
+  24: qux:4->4+
+  26: baz:3+->3-
+  27: merge
+  $ hg up 24 --quiet
+  $ hg merge 26
+  merging qux and baz to qux
+  0 files updated, 1 files merged, 0 files removed, 0 files unresolved
+  (branch merge, don't forget to commit)
+  $ hg ci -m 'merge from other side'
+  created new head
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(qux, 5, 7)'
+  16: baz:0
+  19: baz:3
+  20: baz:4
+  23: baz:3->3+
+  24: qux:4->4+
+  26: baz:3+->3-
+  28: merge from other side
+  $ hg up 23 --quiet
+
+check error cases
+  $ hg log -r 'followlines(baz, 1, 2, rev=desc("b"))'
+  hg: parse error: followlines expects exactly one revision
+  [255]
+  $ hg log -r 'followlines("glob:*", 1, 2)'
+  hg: parse error: followlines expects exactly one file
+  [255]
+  $ hg log -r 'followlines(baz, x, 4)'
+  hg: parse error: line range bounds must be integers
+  [255]
+  $ hg log -r 'followlines(baz, 5, 4)'
+  hg: parse error: line range must be positive
+  [255]
+  $ hg log -r 'followlines(baz, 0, 4)'
+  hg: parse error: fromline must be strictly positive
+  [255]
+  $ hg log -r 'followlines(baz, 2, 40)'
+  abort: line range exceeds file size
+  [255]
+
 Test annotate with whitespace options
 
   $ cd ..