slop-review

Version control

Nothing in the Rust knows what a git or a jj is. Grep it for "jj" or "git" and the only hits are in tests. Both profiles live in crates/core/default.toml, which is compiled into the binary with include_str! and parsed by the same parser that will read yours — it is the bottom layer of the real configuration rather than a fallback beside it, so it is always an accurate reference. --print-default-config writes it back out.

A profile is four commands, a marker directory and three revision expressions:

jj git
list commits jj log -r {revset} -T … git log --format=… {revset}
changed files jj diff -r {rev} -T … git diff-tree -r --name-status -M {rev}
files in a range jj diff --from {from} --to {to} -T … git diff --name-status -M {from} {to}
file at revision jj file show -r {rev} {path} git show {rev}:{path}
parent revision {rev}- {rev}^
work since a branch {branch}..@ {branch}..HEAD

{revset}, {rev}, {from}, {to}, {branch} and {path} are substituted into a single argument each. There is no shell, so a path with spaces in it is safe.

Each command also says how to read what it printed: a record_separator (default a newline) and an ordered list of patterns, the first match naming the fields. Patterns rather than a list of field names, because a list cannot express git's --name-status, which prints one path for a modification and two for a rename:

patterns = [
  '^(?<status>[RC])\d*\t(?<old_path>[^\t]+)\t(?<path>.+)$',
  '^(?<status>[A-Z])\d*\t(?<path>.+)$',
]

The names that mean something are id, short_id, author, timestamp, parents and description for log, and status, path and old_path for the two file listings. A record nothing matches is kept and reported rather than dropped.

The two log commands cut on \x1e and name fields with \x1f, because a commit description can contain anything a newline included. jj is asked for a change's source and target path separately, so a rename arrives as two paths and nothing has to unpick src/{a.rs => b.rs}.

The range listing is a command of its own rather than a union of the per-commit ones: a file added and then deleted inside the range is not part of the range's change at all. Reads use --ignore-working-copy under jj: reviewing must not mutate the repository being reviewed.

Debugging a profile

A profile you wrote yourself fails in ways that are invisible from outside: the command was right and the pattern matched nothing, or it matched and named a field nothing reads. So both the CLI and the window show the argv after substitution, the exit status, what came back and what was made of it.

slop-review vcs check
slop-review vcs check --vcs git -r 'HEAD~3..HEAD'
--- files ---
$ jj diff -r yswnlnmqmyyz --color never --ignore-working-copy -T 'self.status_char() ++ "\x1f" ++ …'
  exit 0, 64 bytes in 12ms
  raw: A\x1fadded.txt\x1fadded.txt\nD\x1fgone.txt\x1fgone.txt\nR\x1fsrc/a.txt\x1fsrc/b.txt\n
  3 records, 0 unrecognised
    old_path=added.txt  path=added.txt  status=A
    old_path=gone.txt  path=gone.txt  status=D
    old_path=src/a.txt  path=src/b.txt  status=R

Commands are printed quoted so they can be pasted straight back into a shell, and control characters in the same \x1f spelling a profile writes them in. The vcs button in the window shows the same thing for the last few runs of each command, plus which configuration layers applied and which were ignored; it carries a ! when something was.