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.
--- a/mercurial/revset.py Wed Dec 28 23:03:37 2016 +0100
+++ b/mercurial/revset.py Wed Jan 04 16:47:49 2017 +0100
@@ -1068,6 +1068,52 @@
# 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``.
--- a/tests/test-annotate.t Wed Dec 28 23:03:37 2016 +0100
+++ b/tests/test-annotate.t Wed Jan 04 16:47:49 2017 +0100
@@ -480,6 +480,127 @@
[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 ..