#10507

FR: `followlines()` revset function to find the commits that touched a block of lines

Openaleufroyopened 3d ago

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])
  • file is a path, lines is a line number or a range like "12-25" (1-based, inclusive) in the content of file at start.
  • start defaults to @. The result is the ancestors of start that 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=true walks the descendants of start instead, 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 annotate repeatedly 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 -L in 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-end is proposed there.

Activity

11
josephlou52d ago

Also FWIW, I think this function should probably be called follow_lines in 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_lines does 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.

aleufroy2d ago

Fair 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 with diff_lines is 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.

yuja2d ago

Other bikeshedding items:

  • Range syntax: x-y
    • Dashes may require quotes: while x-y can be parsed as a symbol, x- (= short for x-LAST_LINE) cannot.
    • Would x..y work better?
  • 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.)
martinvonz2d ago

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().

aleufroy2d ago

For the Range syntax, here are the proposed syntaxes:

  • start:end Mercurial followlines(file, '12:25'), hg log -L 'file,12:25' But in Vim/Helix, : is used as file_path:line:column
  • start,end git: git blame -L 12,25, git log -L 12,25:file; sed: sed 12,25p But , is hard to tel apart from the argument separator in func(x, 'y,z')
  • start-end GitHub: #L12-L25; GitLab: #L12-25; cut -c12-25 But - is also as a meaning of "minus"
  • start..end Bash/Rust: for i in 12..25 But less common

Note that an alternative would be explicite arguments in the function signature:

follow_lines("my_path.rs", start=12, end=25)

aleufroy2d ago

About the descend=true parameter.

We may want to wait for the reverse traversal api later.

josephlou52d ago

For the Range syntax, here are the proposed syntaxes:

  • start-end Mercurial followlines(file, '12:25'), hg log -L 'file,12:25' But in Vim/Helix, : is used as file_path:line:column

I think you meant start:end in this bullet point?

  • start..end bash: for i in 12..25 But 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.

aleufroy2d ago

@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.

aleufroy2d ago

@josephlou5

For the Range syntax, here are the proposed syntaxes:

  • start-end Mercurial followlines(file, '12:25'), hg log -L 'file,12:25' But in Vim/Helix, : is used as file_path:line:column

I think you meant start:end in this bullet point?

Exactly, let me fix this.

10. 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.

Ok, but a lot of jj users probably don't know Rust. That said, start..end works perfectly fine for me.

josephlou52d ago

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.

Ok, but a lot of jj users probably don't know Rust. That said, start..end works 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.

aleufroy2d ago

13. > > 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. > > > Ok, but a lot of jj users probably don't know Rust. That said, start..end works 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.