swaits 6ce9cb2b17 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`.
2026-05-04 12:58:14 -06:00
2026-04-29 19:49:11 -06:00
2026-04-29 19:49:11 -06:00
2026-04-29 23:38:20 -06:00
2026-04-29 23:38:20 -06:00
2026-04-29 23:38:20 -06:00
2026-04-29 19:49:11 -06:00
2026-04-29 23:38:20 -06:00
2026-04-29 18:12:40 -06:00

jjf

A fuzzy revision picker for jujutsu (jj).

Wraps any jj subcommand with an interactive picker over jj log. Type to filter, hit Enter, and jjf runs your command with -r '<id>' filled in from the revision you picked. The right pane shows a live jj show --summary of whatever's under the cursor; the bottom row shows the exact command that's about to run.

❯ vx                                          ┌────────────────────────────────
  ○  tovs chore(release): prepare 0.1.0       │  - Add LICENSE (MIT)
  @  xnkx feat(ux): more refinements          │  - Add README with install,
  ○  ssnu feat(ux): batch UX improvements ●──▶│   usage, keybindings…
  ○  lyxo fix(history): drop jjf entry        │
  ○  tqvr feat: two-column layout             │  M  Cargo.toml
  ○  yksz feat: default to 'show'             │  A  LICENSE
  ○  lsqz feat: broaden picker revset         │  A  README.md
  ○  nzwx feat: initial implementation
▶ jj describe -m 'fix typo' -r 'ss'
[8/8]  type filter · ↑↓/^N^P nav · tab select · enter run · ^U clear · esc quit

Install

cargo install jjf

Requires Rust 1.85+ (edition 2024) and jj 0.40+ on PATH.

Usage

jjf <jj-subcommand> [args...]

Some examples:

jjf describe -m 'fix: validate input'
jjf abandon
jjf edit
jjf squash
jjf rebase -d main

jjf runs jj log, lets you fuzzy-pick a revision (or several), then invokes the subcommand with -r '<id>' appended. Subcommands that don't accept revisions (status, log, git fetch, etc.) are detected upfront and rejected with a friendly message — no wasted picker cycle.

With shell integration installed (see below), jjf with no arguments defaults to jjf show so it doubles as a quick rev browser.

Smart prefix matching

Type the bold portion of any change ID shown in jj log and that revision is highlighted instantly. Falls back to fuzzy-matching the description text:

Query matches Score
Prefix of change_id.shortest() 1,000,000
Prefix of full short change ID (12) 100,000
Prefix of commit_id.shortest() 10,000
Prefix of full short commit ID (12) 1,000
Fuzzy match of description text nucleo score

Empty filter shows all revisions in original jj log order.

Keybindings

Key Action
Any printable char Append to filter
↑ / ↓, Ctrl-N / Ctrl-P, Ctrl-J / Ctrl-K Move cursor
PageUp / PageDown Page up / down
Home / End, Ctrl-A / Ctrl-E Jump to first / last
Tab Toggle selection on cursor row
Enter Confirm: selected rows, else cursor
Backspace, Ctrl-H Delete char from filter
Ctrl-U Clear filter
Ctrl-W Delete word from filter
Esc, Ctrl-C Cancel

Multi-select with Tab joins picks into a -r 'A|B|C' revset union. Subcommands that require exactly one revision (describe, edit) will surface jj's own error if multi-selected.

Shell integration

Add to your shell rc:

# bash
eval "$(jjf init bash)"

# zsh
eval "$(jjf init zsh)"

# fish
jjf init fish | source

# nushell — save once, source from your config.nu
jjf init nu | save -f ~/.config/nushell/jjf.nu
# then add:  source ~/.config/nushell/jjf.nu

With this in place:

  • History rewriting — jjf describe -m 'foo' records the resolved jj describe -m 'foo' -r '<id>' in your shell history rather than the jjf … invocation. Up-arrow recalls the resolved command, ready to re-run or edit.
  • jjf alone runs jjf show so it's a quick rev browser.
  • jjf init …, jjf -h, jjf --version bypass the picker and pass through to the binary directly.
Shell History append Original jjf … entry
bash history -s deleted via history -d
zsh print -s suppressed via zshaddhistory hook
fish builtin history append deleted via fish_postexec hook
nushell history import remains (sqlite history; no clean delete API)

Without shell integration, jjf still works — it just execs jj directly and your shell history shows the original jjf … invocation.

How it works

jjf invokes jj log --ignore-working-copy --color=always -r 'all()' with a custom template that emits per-revision metadata (short change ID, shortest unique change-ID prefix, short commit ID, shortest unique commit-ID prefix) plus a custom oneline payload, separated by \x1f sentinels.

The picker is a hand-rolled TUI rendered via direct ANSI to /dev/tty (no ratatui, no ansi-to-tui). Two-column layout when the terminal is at least 80 cols wide: picker on the left, jj show --summary of the cursor row on the right (cached per revision). On Enter, the resolved jj <sub> [args] -r '<id>' is either exec'd directly or printed to stdout (in --emit mode, used by the shell wrappers).

The --ignore-working-copy flag avoids snapshotting during the picker phase, which prevents races with prompt/editor integrations that auto-snapshot (e.g. starship's jj module).

Dependencies

Runtime crates: crossterm, nucleo-matcher, anyhow, libc. Total transitive dep count is around 50; release binary is ~700 KB stripped.

Acknowledgments

The idea for jjf came from a bash + fzf script of the same concept (also discussed on lobste.rs) by the author at oppi.li. jjf is an independent Rust implementation — none of the original script's code was viewed or used by the author — but the core insight (wrap any jj subcommand with an fzf-style picker over jj log) is theirs and worth crediting.

License

MIT — see LICENSE.

S
Description
Fuzzy revision picker for jujutsu (jj). Wraps any jj subcommand with an interactive picker over jj log, with a live jj show preview pane.
Readme MIT
92 KiB
0 Stars 1 Watchers 0 Forks
Languages
Rust 95.2%
Shell 3.9%
Nushell 0.9%