changeset 31938:5e3b49defbff

revset: add a 'descend' argument to followlines to return descendants This is useful to follow changes in a block of lines forward in the history (for instance, when one wants to find out how a function evolved from a point in history). We added a 'descend' parameter to followlines(), which defaults to False. If True, followlines() returns descendants of startrev. Because context.blockdescendants() does not follow renames, these are not followed by the revset either, so history will end when a rename occurs (as can be seen in tests).
author Denis Laxalde <denis.laxalde@logilab.fr>
date Mon, 16 Jan 2017 09:24:47 +0100
parents 826e600605f6
children 604d31507f86
files mercurial/revset.py tests/test-annotate.t
diffstat 2 files changed, 40 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/revset.py	Mon Apr 10 15:11:36 2017 +0200
+++ b/mercurial/revset.py	Mon Jan 16 09:24:47 2017 +0100
@@ -901,17 +901,22 @@
     # of every revisions or files revisions.
     return _follow(repo, subset, x, '_followfirst', followfirst=True)
 
-@predicate('followlines(file, fromline:toline[, startrev=.])', safe=True)
+@predicate('followlines(file, fromline:toline[, startrev=., descend=False])',
+           safe=True)
 def followlines(repo, subset, x):
     """Changesets modifying `file` in line range ('fromline', 'toline').
 
     Line range corresponds to 'file' content at 'startrev' and should hence be
     consistent with file size. If startrev is not specified, working directory's
     parent is used.
+
+    By default, ancestors of 'startrev' are returned. If 'descend' is True,
+    descendants of 'startrev' are returned though renames are (currently) not
+    followed in this direction.
     """
     from . import context  # avoid circular import issues
 
-    args = getargsdict(x, 'followlines', 'file *lines startrev')
+    args = getargsdict(x, 'followlines', 'file *lines startrev descend')
     if len(args['lines']) != 1:
         raise error.ParseError(_("followlines requires a line range"))
 
@@ -939,9 +944,17 @@
     fromline, toline = util.processlinerange(fromline, toline)
 
     fctx = repo[rev].filectx(fname)
-    revs = (c.rev() for c, _linerange
-            in context.blockancestors(fctx, fromline, toline))
-    return subset & generatorset(revs, iterasc=False)
+    if args.get('descend', False):
+        rs = generatorset(
+            (c.rev() for c, _linerange
+             in context.blockdescendants(fctx, fromline, toline)),
+            iterasc=True)
+    else:
+        rs = generatorset(
+            (c.rev() for c, _linerange
+             in context.blockancestors(fctx, fromline, toline)),
+            iterasc=False)
+    return subset & rs
 
 @predicate('all()', safe=True)
 def getall(repo, subset, x):
--- a/tests/test-annotate.t	Mon Apr 10 15:11:36 2017 +0200
+++ b/tests/test-annotate.t	Mon Jan 16 09:24:47 2017 +0100
@@ -484,7 +484,9 @@
   $ hg id -n
   20
 
-Test followlines() revset
+Test followlines() revset; we usually check both followlines(pat, range) and
+followlines(pat, range, descend=True) to make sure both give the same result
+when they should.
 
   $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3:5)'
   16: baz:0
@@ -494,9 +496,11 @@
   16: baz:0
   19: baz:3
   20: baz:4
-  $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3:5, startrev=.^)'
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3:5, startrev=19)'
   16: baz:0
   19: baz:3
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3:5, startrev=19, descend=True)'
+  20: baz:4
   $ printf "0\n0\n" | cat - baz > baz1
   $ mv baz1 baz
   $ hg ci -m 'added two lines with 0'
@@ -504,12 +508,16 @@
   16: baz:0
   19: baz:3
   20: baz:4
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3:5, descend=True, startrev=19)'
+  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
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3:5, startrev=19, descend=True)'
+  20: baz:4
   $ sed 's/3/3+/' baz > baz.new
   $ mv baz.new baz
   $ hg ci -m 'baz:3->3+'
@@ -518,6 +526,9 @@
   19: baz:3
   20: baz:4
   23: baz:3->3+
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 3:5, startrev=19, descend=True)'
+  20: baz:4
+  23: baz:3->3+
   $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 1:2)'
   21: added two lines with 0
 
@@ -536,9 +547,13 @@
   20: baz:4
   23: baz:3->3+
   24: qux:4->4+
-  $ hg up 23 --quiet
+
+but are missed when following children
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 5:7, startrev=22, descend=True)'
+  23: baz:3->3+
 
 merge
+  $ hg up 23 --quiet
   $ echo 7 >> baz
   $ hg ci -m 'one more line, out of line range'
   created new head
@@ -581,6 +596,10 @@
   28: merge from other side
   $ hg up 23 --quiet
 
+we are missing the branch with rename when following children
+  $ hg log -T '{rev}: {desc}\n' -r 'followlines(baz, 5:7, startrev=25, descend=True)'
+  26: baz:3+->3-
+
 check error cases
   $ hg log -r 'followlines()'
   hg: parse error: followlines takes at least 1 positional arguments