diff --git a/mercurial/revset.py b/mercurial/revset.py --- a/mercurial/revset.py +++ b/mercurial/revset.py @@ -1068,6 +1068,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 ..