feat(dispatch): walk subcommands and detect revset flag from help

Two real failures motivated this:

- `jjf bm main` (where `bm` is a user alias for `bookmark move`) ran
  `jj bm main -r <id>` — but bookmark move uses `--to`, not `-r`.
- `jjf tag set v0.2.0` errored with "doesn't take revisions" because
  jjf only inspected `jj tag --help` (parent) instead of walking
  into the `set` leaf, which does take `-r`.

Replace the boolean `supports_revisions()` (a substring match on
"REVSET") with `RevsetTarget { leaf, passthrough, flag }`:

- Walks multi-level subcommands by parsing each parent's `Commands:`
  block — including `[aliases: …]` annotations, so `jjf tag s` walks
  to `tag set`. Aliases like `bm` work transparently because `jj bm
  --help` already emits the resolved leaf's help.
- Parses the leaf's `Options:` block and picks the revset flag in
  priority order: `-r`/`--revision[s]` first, then `-t, --to`.
  `--from` is intentionally not a fallback (different semantics from
  "the revision the picker chose").
- Bypasses the picker entirely when the user already passed a revset
  flag in their args, so `jjf describe -r @-` runs verbatim instead
  of double-injecting `-r`.

`exec` and `command_line` now take `&RevsetTarget` and emit the
resolved flag; signatures thread through `tui.rs` and `main.rs`.

New unit tests cover the help parsers and command-line construction
against captured fixtures of `jj tag set --help`, `jj bookmark move
--help`, and `jj tag --help`.
This commit is contained in:
swaits committed 2026-05-04 12:58:14 -06:00
1 parent 1c4ae09646
commit 6ce9cb2b17
3 files changed
+471 -50

No files matched your search

+6 -9
View File
@@ -278,8 +278,7 @@ fn score_row(
pub fn run(
rows: Vec<Row>,
subcommand: &str,
passthrough: &[String],
target: &jj::RevsetTarget,
) -> Result<Option<Vec<String>>> {
install_panic_hook();
let mut tty = OpenOptions::new()
@@ -295,7 +294,7 @@ pub fn run(
tty.write_all(HIDE_CURSOR).context("hide cursor")?;
tty.flush().ok();
let result = run_loop(&mut tty, rows, subcommand, passthrough);
let result = run_loop(&mut tty, rows, target);
let _ = tty.write_all(SHOW_CURSOR);
let _ = tty.write_all(b"\x1b[?1049l");
@@ -320,15 +319,14 @@ fn install_panic_hook() {
fn run_loop(
tty: &mut File,
rows: Vec<Row>,
subcommand: &str,
passthrough: &[String],
target: &jj::RevsetTarget,
) -> Result<Option<Vec<String>>> {
let mut app = App::new(rows);
let (mut term_cols, mut term_rows) = crossterm::terminal::size().unwrap_or((80, 24));
// Chrome rows: input(1) + cmd_preview(1) + hint(1) = 3.
app.last_height = (term_rows as usize).saturating_sub(3);
loop {
render(tty, &mut app, term_rows, term_cols, subcommand, passthrough)?;
render(tty, &mut app, term_rows, term_cols, target)?;
if !event::poll(Duration::from_millis(250)).context("event poll failed")? {
continue;
}
@@ -454,8 +452,7 @@ fn render(
app: &mut App,
term_rows: u16,
term_cols: u16,
subcommand: &str,
passthrough: &[String],
target: &jj::RevsetTarget,
) -> Result<()> {
let viewport_y: u16 = 0;
let viewport_height = term_rows;
@@ -601,7 +598,7 @@ fn render(
let cmd_y = viewport_y + 1 + log_height as u16 + 1;
write!(buf, "\x1b[{};1H", cmd_y)?;
buf.extend_from_slice(ERASE_TO_EOL);
let cmd = jj::command_line(subcommand, passthrough, &app.pending_ids());
let cmd = jj::command_line(target, &app.pending_ids());
let cmd_max = (term_cols as usize).saturating_sub(2); // for "▶ "
let cmd_visible: String = if cmd.chars().count() <= cmd_max {
cmd