Is your feature request related to a problem? Please describe.
When I want to know which commits touched a few lines of a file (a
function, a config block), I have no direct way to do it in jj. jj file annotate only gives the last commit for each line. jj log -p <file>
shows every commit of the file, and I have to scan the diffs by hand.
Mercurial has followlines() and Git has git log -L for this.
Describe the solution you'd like
Mercurial's followlines() revset function
(https://repo.mercurial-scm.org/hg/help/revsets) seems right to me, and I'd
like the same in jj:
followlines(file, lines, [start], [descend])fileis a path,linesis a line number or a range like"12-25"(1-based, inclusive) in the content offileatstart.startdefaults to@. The result is the ancestors ofstartthat changed, added or removed lines in the block, plus the commit that created it. The block is followed as it moves through history: when a commit modifies a hunk that overlaps the block, the block is extended to the whole hunk on the parent side, so no line is lost.- Renames are followed through copy records.
descend=truewalks the descendants ofstartinstead, up to the visible heads.
Typical use:
jj log -p -r 'followlines("src/foo.rs", "120-135")'Describe alternatives you've considered
- Running
jj file annotaterepeatedly on older revisions: tedious, and it only finds the last change of each line. jj log -p <file>and reading the diffs: too much output on files that change often.git log -Lin a colocated repo: works, but outside jj, without revsets.
Additional context
- Proposal discussed in #10504. A contributor suggested opening this issue. There's a link to a working implementation in the discussion.
- Related: #9215 (line range option for
jj file annotate). The line range syntax should be the same for both;start-endis proposed there.
Activity
11Fair point, followlines is a Mercurial-ism, happy to rename it.
The closest existing function is diff_lines(text, [files]) (plus diff_lines_added() / diff_lines_removed()). Both pick commits by looking at their diff: diff_lines looks at what the lines say, this one looks at where they are. So maybe something in the same family:
diff_lines_at(file, lines): "commits whose diff touches the lines at this spot", and the link withdiff_linesis obvious.diff_line_range(file, lines): just names the range.modifies_lines(file, lines): still reads like the other predicates, closer to "commits that modify these lines".
One thing diff_lines doesn't do, though: it doesn't track anything, it only matches a pattern in each diff. This function follows the block as it moves: start-end is only valid for the starting commit, then the range gets shifted through each diff. That's what follow_lines was trying to say, so two more names that keep that idea:
track_lines(file, lines): "track" says it follows something that moves.lines_history(file, lines): describes the result rather than how it works, like "line history" in VS Code or IntelliJ.
I'd go with diff_lines_at or track_lines, but no strong opinion.
Other bikeshedding items:
- Range syntax:
x-y- Dashes may require quotes: while
x-ycan be parsed as a symbol,x-(= short forx-LAST_LINE) cannot. - Would
x..ywork better?
- Dashes may require quotes: while
- Boolean parameters:
descend=true- We don't have booleans in revset right now.
- Should we add separate functions for following ancestors and descendants instead?
- or
direction=ancestors|descendants? - This can be decided later when we add a reverse traversal. (We don't have a reusable component for reverse annotation yet.)
I'm not against a revset for this, but I also think an interactive jj file annotate is what I would want more often. I asked Claude to implement it. See https://github.com/jj-vcs/jj/compare/main...mz/oxqpwppkqzpk. Feel free to try and see if that does what you want next time you reach for follow_lines().
For the Range syntax, here are the proposed syntaxes:
start:endMercurialfollowlines(file, '12:25'),hg log -L 'file,12:25'But in Vim/Helix,:is used asfile_path:line:columnstart,endgit:git blame -L 12,25,git log -L 12,25:file; sed:sed 12,25pBut,is hard to tel apart from the argument separator infunc(x, 'y,z')start-endGitHub:#L12-L25; GitLab:#L12-25;cut -c12-25But-is also as a meaning of "minus"start..endBash/Rust:for i in 12..25But less common
Note that an alternative would be explicite arguments in the function signature:
follow_lines("my_path.rs", start=12, end=25)
About the descend=true parameter.
We may want to wait for the reverse traversal api later.
For the Range syntax, here are the proposed syntaxes:
start-endMercurialfollowlines(file, '12:25'),hg log -L 'file,12:25'But in Vim/Helix,:is used asfile_path:line:column
I think you meant start:end in this bullet point?
start..endbash:for i in 12..25But less common
There is also n..m in Rust, and our templating language already borrows Rust's lambda syntax, so following that syntax here would also be a good reason for this.
@martinvonz jj file annotate --tui path is interesting because it answers "why/how was this line last changed?". But it doesn't really answer "how did this block of code evolve?" — say you've got a data model and want to track how it changed over time. jj file annotate --tui path helps, but it only shaves off a few steps in the walk: jj file annotate path -> search for the block -> jj show xxx -> jj log -r xxx- (could be a
merge) -> jj file annotate w -> ...
To make this actually practical, I think we'd want jj file annotate --lines '12,25' --tui to zoom in on just what we care about, plus jj log -r 'follow_lines(path, "12-25")' to see the history of that block. This PR covers the latter. I've also got jj file annotate --lines ... working, but it shares some code with follow_lines, so I'll send that one after this lands.
Note: Mercurial has something interesting : log -r 'follow_lines(path, "start:end")' -p --lines "start-end" path, which shows the commits along with just the relevant patch chunk.
For the Range syntax, here are the proposed syntaxes:
start-endMercurialfollowlines(file, '12:25'),hg log -L 'file,12:25'But in Vim/Helix,:is used asfile_path:line:columnI think you meant
start:endin this bullet point?
Exactly, let me fix this.
10. There is also
n..min Rust, and our templating language already borrows Rust's lambda syntax, so following that syntax here would also be a good reason for this.
Ok, but a lot of jj users probably don't know Rust. That said, start..end works perfectly fine for me.
There is also
n..min Rust, and our templating language already borrows Rust's lambda syntax, so following that syntax here would also be a good reason for this.Ok, but a lot of
jjusers probably don't know Rust. That said,start..endworks perfectly fine for me.
I didn't mean that users need to know Rust, but rather that since this project uses Rust, we can take inspiration from it for decisions such as this. I gave the templating lambda syntax as an example. Plus, this is such a small point that there isn't even a reason to mention Rust; we just have to say "here is how you do a line range" and people will have to accept it, whether they know Rust or not.
13. > > There is also
n..min Rust, and our templating language already borrows Rust's lambda syntax, so following that syntax here would also be a good reason for this. > > > Ok, but a lot ofjjusers probably don't know Rust. That said,start..endworks perfectly fine for me.I didn't mean that users need to know Rust, but rather that since this project uses Rust, we can take inspiration from it for decisions such as this. I gave the templating lambda syntax as an example. Plus, this is such a small point that there isn't even a reason to mention Rust; we just have to say "here is how you do a line range" and people will have to accept it, whether they know Rust or not.
The more I think about it, the less I see a reason to use a special syntax for defining the line range. Explicit arguments are much better: follow_lines('my_file.rs', 12, 25). If you want to follow several different blocks, you can just use multiple follow_lines() calls.
Also FWIW, I think this function should probably be called
follow_linesin jj. We generally don't have any symbols (subcommands, functions, etc.) with words combined together. "Parity" or "copied" features from other VCSes is also not a goal of the project, which also means that IMO the function could be renamed to something else too (personally,follow_linesdoes not super clearly define what it's actually doing, if you aren't familiar with the Mercurial version or haven't read the docs for it).Also, for cross-linking: PR is up in #10512.